diff --git a/crates/rust4d_audio/src/lib.rs b/crates/rust4d_audio/src/lib.rs index 0e913c1..c27612a 100644 --- a/crates/rust4d_audio/src/lib.rs +++ b/crates/rust4d_audio/src/lib.rs @@ -91,7 +91,9 @@ impl AudioEngine4D { /// Create a new audio engine pub fn new() -> Result { let manager = AudioManager::::new(AudioManagerSettings::default()) - .map_err(|e: kira::manager::backend::cpal::Error| AudioError::ManagerInit(e.to_string()))?; + .map_err(|e: kira::manager::backend::cpal::Error| { + AudioError::ManagerInit(e.to_string()) + })?; let mut engine = Self { manager, @@ -151,11 +153,10 @@ impl AudioEngine4D { /// Returns `AudioError::SoundIdOverflow` if the maximum number of sounds /// (2^64) has been reached. In practice this is unreachable. pub fn load_sound(&mut self, path: &str) -> Result { - let sound_data = StaticSoundData::from_file(path) - .map_err(|e| AudioError::LoadSound { - path: path.to_string(), - message: e.to_string(), - })?; + let sound_data = StaticSoundData::from_file(path).map_err(|e| AudioError::LoadSound { + path: path.to_string(), + message: e.to_string(), + })?; let id = self.next_sound_id; self.next_sound_id = self @@ -183,15 +184,13 @@ impl AudioEngine4D { let settings = StaticSoundSettings::new().output_destination(track); let sound_with_settings = sound_data.with_settings(settings); - let handle = self.manager + let handle = self + .manager .play(sound_with_settings) .map_err(|e| AudioError::PlaySound(e.to_string()))?; // Track the sound handle for stop_all/stop_bus support - self.active_sounds - .entry(bus) - .or_default() - .push(handle); + self.active_sounds.entry(bus).or_default().push(handle); Ok(()) } @@ -232,15 +231,13 @@ impl AudioEngine4D { let sound_with_settings = sound_data.with_settings(settings); - let handle = self.manager + let handle = self + .manager .play(sound_with_settings) .map_err(|e| AudioError::PlaySound(e.to_string()))?; // Track the sound handle for stop_all/stop_bus support - self.active_sounds - .entry(bus) - .or_default() - .push(handle); + self.active_sounds.entry(bus).or_default().push(handle); log::trace!( "Playing spatial sound at {:?}, volume: {:.2}, panning: {:.2}", @@ -266,7 +263,10 @@ impl AudioEngine4D { /// Set the volume of a specific bus pub fn set_bus_volume(&mut self, bus: AudioBus, volume: f32) { if let Some(track) = self.bus_tracks.get_mut(&bus) { - track.set_volume(Volume::Amplitude(volume.clamp(0.0, 1.0) as f64), Tween::default()); + track.set_volume( + Volume::Amplitude(volume.clamp(0.0, 1.0) as f64), + Tween::default(), + ); log::debug!("Set {:?} bus volume to {:.2}", bus, volume); } } diff --git a/crates/rust4d_audio/src/spatial.rs b/crates/rust4d_audio/src/spatial.rs index 84b8c6b..fdc957e 100644 --- a/crates/rust4d_audio/src/spatial.rs +++ b/crates/rust4d_audio/src/spatial.rs @@ -135,8 +135,7 @@ mod tests { #[test] fn test_attenuation_within_min_distance() { let listener = Vec4::ZERO; - let config = SpatialConfig::new(Vec4::new(0.5, 0.0, 0.0, 0.0)) - .with_min_distance(1.0); + let config = SpatialConfig::new(Vec4::new(0.5, 0.0, 0.0, 0.0)).with_min_distance(1.0); let attenuation = calculate_attenuation(listener, &config); assert!((attenuation - 1.0).abs() < 0.0001); } @@ -144,8 +143,7 @@ mod tests { #[test] fn test_attenuation_at_max_distance() { let listener = Vec4::ZERO; - let config = SpatialConfig::new(Vec4::new(50.0, 0.0, 0.0, 0.0)) - .with_max_distance(50.0); + let config = SpatialConfig::new(Vec4::new(50.0, 0.0, 0.0, 0.0)).with_max_distance(50.0); let attenuation = calculate_attenuation(listener, &config); assert!(attenuation < 0.0001); } @@ -153,8 +151,7 @@ mod tests { #[test] fn test_attenuation_beyond_max_distance() { let listener = Vec4::ZERO; - let config = SpatialConfig::new(Vec4::new(100.0, 0.0, 0.0, 0.0)) - .with_max_distance(50.0); + let config = SpatialConfig::new(Vec4::new(100.0, 0.0, 0.0, 0.0)).with_max_distance(50.0); let attenuation = calculate_attenuation(listener, &config); assert_eq!(attenuation, 0.0); } diff --git a/crates/rust4d_core/src/asset_cache.rs b/crates/rust4d_core/src/asset_cache.rs index 4cfa5f3..acb5ee3 100644 --- a/crates/rust4d_core/src/asset_cache.rs +++ b/crates/rust4d_core/src/asset_cache.rs @@ -151,10 +151,7 @@ impl AssetCache { // Check if already cached (deduplication by path) if let Some(&id) = self.path_index.get(&path) { - return Ok(AssetHandle { - id, - path, - }); + return Ok(AssetHandle { id, path }); } // Load from file @@ -280,11 +277,7 @@ impl AssetCache { log::info!("Hot-reloaded asset: {}", path.display()); } Err(err) => { - log::warn!( - "Failed to hot-reload asset {}: {}", - path.display(), - err - ); + log::warn!("Failed to hot-reload asset {}: {}", path.display(), err); } } } diff --git a/crates/rust4d_core/src/asset_error.rs b/crates/rust4d_core/src/asset_error.rs index 81923ba..976807c 100644 --- a/crates/rust4d_core/src/asset_error.rs +++ b/crates/rust4d_core/src/asset_error.rs @@ -2,8 +2,8 @@ //! //! Provides error handling for asset loading, caching, and hot-reload operations. -use std::io; use std::fmt; +use std::io; /// Error type for asset operations #[derive(Debug)] diff --git a/crates/rust4d_core/src/components.rs b/crates/rust4d_core/src/components.rs index f95f1b8..9f6141e 100644 --- a/crates/rust4d_core/src/components.rs +++ b/crates/rust4d_core/src/components.rs @@ -6,8 +6,8 @@ //! Existing types like `Transform4D`, `Material`, `ShapeRef`, and `DirtyFlags` //! are also used as ECS components directly (they satisfy Send + Sync + 'static). -use std::collections::HashSet; use rust4d_physics::BodyKey; +use std::collections::HashSet; // === Name Component === diff --git a/crates/rust4d_core/src/entity.rs b/crates/rust4d_core/src/entity.rs index 8a38086..93843e1 100644 --- a/crates/rust4d_core/src/entity.rs +++ b/crates/rust4d_core/src/entity.rs @@ -4,13 +4,13 @@ //! The Entity struct has been removed -- entities are now spawned directly as //! ECS component tuples via `world.spawn(...)` or `EntityTemplate::spawn_in(...)`. -use std::collections::HashSet; -use std::sync::Arc; +use crate::shapes::ShapeTemplate; +use crate::Transform4D; use bitflags::bitflags; use rust4d_math::ConvexShape4D; -use serde::{Serialize, Deserialize}; -use crate::Transform4D; -use crate::shapes::ShapeTemplate; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::sync::Arc; bitflags! { /// Flags indicating which parts of an entity have changed and need updating @@ -63,19 +63,29 @@ impl Material { } /// White material - pub const WHITE: Self = Self { base_color: [1.0, 1.0, 1.0, 1.0] }; + pub const WHITE: Self = Self { + base_color: [1.0, 1.0, 1.0, 1.0], + }; /// Gray material - pub const GRAY: Self = Self { base_color: [0.5, 0.5, 0.5, 1.0] }; + pub const GRAY: Self = Self { + base_color: [0.5, 0.5, 0.5, 1.0], + }; /// Red material - pub const RED: Self = Self { base_color: [1.0, 0.0, 0.0, 1.0] }; + pub const RED: Self = Self { + base_color: [1.0, 0.0, 0.0, 1.0], + }; /// Green material - pub const GREEN: Self = Self { base_color: [0.0, 1.0, 0.0, 1.0] }; + pub const GREEN: Self = Self { + base_color: [0.0, 1.0, 0.0, 1.0], + }; /// Blue material - pub const BLUE: Self = Self { base_color: [0.0, 0.0, 1.0, 1.0] }; + pub const BLUE: Self = Self { + base_color: [0.0, 0.0, 1.0, 1.0], + }; } /// Reference to a shape - either shared (Arc) or owned (Box) @@ -259,7 +269,9 @@ mod tests { ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(1.0, 2.0, 3.0, 4.0)), Material::RED, - ).with_name("my_cube").with_tag("dynamic"); + ) + .with_name("my_cube") + .with_tag("dynamic"); let mut world = crate::World::new(); let entity = template.spawn_in(&mut world); diff --git a/crates/rust4d_core/src/lib.rs b/crates/rust4d_core/src/lib.rs index 2ed7d97..b27e47b 100644 --- a/crates/rust4d_core/src/lib.rs +++ b/crates/rust4d_core/src/lib.rs @@ -11,38 +11,38 @@ //! - [`EntityTemplate`] - Serializable entity template //! - [`Scene`] - Loadable/saveable scene containing entities -mod transform; -mod entity; -mod world; +mod asset_cache; +mod asset_error; mod components; -mod shapes; +mod entity; mod scene; +mod scene_loader; mod scene_manager; -mod asset_error; -mod asset_cache; mod scene_transition; -mod scene_loader; mod scene_validator; +mod shapes; +mod transform; +mod world; -pub use transform::Transform4D; -pub use entity::{Material, ShapeRef, DirtyFlags, EntityTemplate}; -pub use world::{World, HierarchyError}; -pub use components::{Name, Tags, PhysicsBody, Parent, Children}; -pub use shapes::{ColliderHint, ShapeTemplate}; -pub use scene::{Scene, SceneLoadError, SceneSaveError, SceneError, ActiveScene}; -pub use scene_manager::SceneManager; +pub use asset_cache::{Asset, AssetCache, AssetHandle, AssetId}; pub use asset_error::AssetError; -pub use asset_cache::{AssetId, AssetHandle, Asset, AssetCache}; -pub use scene_transition::{SceneTransition, TransitionEffect, SlideDirection}; -pub use scene_loader::{SceneLoader, LoadResult}; +pub use components::{Children, Name, Parent, PhysicsBody, Tags}; +pub use entity::{DirtyFlags, EntityTemplate, Material, ShapeRef}; +pub use scene::{ActiveScene, Scene, SceneError, SceneLoadError, SceneSaveError}; +pub use scene_loader::{LoadResult, SceneLoader}; +pub use scene_manager::SceneManager; +pub use scene_transition::{SceneTransition, SlideDirection, TransitionEffect}; pub use scene_validator::{SceneValidator, ValidationError}; +pub use shapes::{ColliderHint, ShapeTemplate}; +pub use transform::Transform4D; +pub use world::{HierarchyError, World}; /// Type alias for entity handles (hecs::Entity) pub type EntityId = hecs::Entity; // Re-export commonly used types from rust4d_math for convenience -pub use rust4d_math::{Vec4, Rotor4, RotationPlane, ConvexShape4D, Tetrahedron}; -pub use rust4d_math::{Tesseract4D, Hyperplane4D}; +pub use rust4d_math::{ConvexShape4D, RotationPlane, Rotor4, Tetrahedron, Vec4}; +pub use rust4d_math::{Hyperplane4D, Tesseract4D}; // Re-export physics types for convenient access through rust4d_core pub use rust4d_physics::{BodyKey, PhysicsConfig, PhysicsWorld, RigidBody4D, StaticCollider}; diff --git a/crates/rust4d_core/src/scene.rs b/crates/rust4d_core/src/scene.rs index 5a7958d..c85417e 100644 --- a/crates/rust4d_core/src/scene.rs +++ b/crates/rust4d_core/src/scene.rs @@ -3,17 +3,17 @@ //! Provides Scene struct for loading/saving scenes from RON files. //! Scenes contain entity templates, physics settings, and player spawn info. -use serde::{Serialize, Deserialize}; -use std::path::Path; +use serde::{Deserialize, Serialize}; use std::fs; use std::io; +use std::path::Path; +use crate::components::PhysicsBody; use crate::entity::EntityTemplate; use crate::shapes::ShapeTemplate; -use crate::components::PhysicsBody; use crate::World; use rust4d_math::Vec4; -use rust4d_physics::{PhysicsConfig, RigidBody4D, StaticCollider, BodyType, PhysicsMaterial}; +use rust4d_physics::{BodyType, PhysicsConfig, PhysicsMaterial, RigidBody4D, StaticCollider}; /// A serializable scene containing entity templates /// @@ -48,8 +48,12 @@ impl Scene { pub fn load>(path: P) -> Result { let contents = fs::read_to_string(path.as_ref())?; let scene: Scene = ron::from_str(&contents)?; - log::debug!("Loaded scene '{}' with gravity={:?}, player_spawn={:?}", - scene.name, scene.gravity, scene.player_spawn); + log::debug!( + "Loaded scene '{}' with gravity={:?}, player_spawn={:?}", + scene.name, + scene.gravity, + scene.player_spawn + ); Ok(scene) } @@ -229,11 +233,18 @@ impl ActiveScene { /// Player body creation is NOT handled here -- use /// `rust4d_game::scene_helpers::create_player_body()` at the application layer. pub fn from_template(template: &Scene, physics_config: Option) -> Self { - log::debug!("from_template: physics_config={:?}, template.gravity={:?}", physics_config, template.gravity); + log::debug!( + "from_template: physics_config={:?}, template.gravity={:?}", + physics_config, + template.gravity + ); // Create world with physics let mut world = if let Some(config) = physics_config { - log::debug!("Using provided physics_config with gravity={}", config.gravity); + log::debug!( + "Using provided physics_config with gravity={}", + config.gravity + ); World::new().with_physics(config) } else if let Some(gravity) = template.gravity { log::debug!("Using template gravity={}", gravity); @@ -254,7 +265,14 @@ impl ActiveScene { if let Some(physics) = world.physics_mut() { if is_static { // Create bounded static collider for floor/walls (objects can fall off edges) - if let ShapeTemplate::Hyperplane { y, size, cell_size, thickness, .. } = &entity_template.shape { + if let ShapeTemplate::Hyperplane { + y, + size, + cell_size, + thickness, + .. + } = &entity_template.shape + { log::debug!("Adding bounded floor collider: y={}, size={}, cell_size={}, thickness={}", y, size, cell_size, thickness); physics.add_static_collider(StaticCollider::floor_bounded( @@ -295,7 +313,9 @@ impl ActiveScene { let entity_handle = entity_template.spawn_in(&mut world); if let Some(bk) = body_key { - let _ = world.ecs_mut_unchecked().insert_one(entity_handle, PhysicsBody(bk)); + let _ = world + .ecs_mut_unchecked() + .insert_one(entity_handle, PhysicsBody(bk)); } } @@ -342,8 +362,8 @@ impl ActiveScene { #[cfg(test)] mod tests { use super::*; - use crate::{Transform4D, Material}; use crate::shapes::ShapeTemplate; + use crate::{Material, Transform4D}; use rust4d_math::Vec4; #[test] @@ -389,7 +409,9 @@ mod tests { ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(1.0, 0.0, 0.0, 0.0)), Material::RED, - ).with_name("test_cube").with_tag("dynamic"); + ) + .with_name("test_cube") + .with_tag("dynamic"); scene.add_entity(entity); @@ -464,7 +486,13 @@ Scene( assert_eq!(scene.entities[0].name, Some("floor".to_string())); assert_eq!(scene.entities[0].tags, vec!["static"]); match &scene.entities[0].shape { - ShapeTemplate::Hyperplane { y, size, subdivisions, cell_size, thickness } => { + ShapeTemplate::Hyperplane { + y, + size, + subdivisions, + cell_size, + thickness, + } => { assert_eq!(*y, -2.0); assert_eq!(*size, 10.0); assert_eq!(*subdivisions, 10); @@ -491,7 +519,9 @@ Scene( ShapeTemplate::tesseract(2.0), Transform4D::from_position(Vec4::new(1.0, 2.0, 3.0, 4.0)), Material::RED, - ).with_name("my_cube").with_tag("dynamic"); + ) + .with_name("my_cube") + .with_tag("dynamic"); let mut world = crate::World::new(); let entity = template.spawn_in(&mut world); @@ -556,16 +586,14 @@ Scene( #[test] fn test_active_scene_with_physics() { - let scene = ActiveScene::new("Physics Scene") - .with_physics(PhysicsConfig::new(-20.0)); + let scene = ActiveScene::new("Physics Scene").with_physics(PhysicsConfig::new(-20.0)); assert!(scene.world.physics().is_some()); assert_eq!(scene.world.physics().unwrap().config.gravity, -20.0); } #[test] fn test_active_scene_with_player_spawn() { - let scene = ActiveScene::new("Spawn Scene") - .with_player_spawn([1.0, 2.0, 3.0, 4.0]); + let scene = ActiveScene::new("Spawn Scene").with_player_spawn([1.0, 2.0, 3.0, 4.0]); assert_eq!(scene.player_spawn, Some([1.0, 2.0, 3.0, 4.0])); } @@ -576,11 +604,14 @@ Scene( .with_gravity(-15.0) .with_player_spawn(0.0, 1.0, 5.0, 0.0); - template.add_entity(EntityTemplate::new( - ShapeTemplate::tesseract(2.0), - Transform4D::from_position(Vec4::new(1.0, 0.0, 0.0, 0.0)), - Material::RED, - ).with_name("cube")); + template.add_entity( + EntityTemplate::new( + ShapeTemplate::tesseract(2.0), + Transform4D::from_position(Vec4::new(1.0, 0.0, 0.0, 0.0)), + Material::RED, + ) + .with_name("cube"), + ); // Instantiate from template let active = ActiveScene::from_template(&template, None); @@ -604,10 +635,7 @@ Scene( let template = Scene::new("Template").with_gravity(-10.0); // Override physics config - let active = ActiveScene::from_template( - &template, - Some(PhysicsConfig::new(-30.0)), - ); + let active = ActiveScene::from_template(&template, Some(PhysicsConfig::new(-30.0))); // Should use overridden config, not template gravity assert_eq!(active.world.physics().unwrap().config.gravity, -30.0); @@ -615,8 +643,7 @@ Scene( #[test] fn test_active_scene_update() { - let mut scene = ActiveScene::new("Update Test") - .with_physics(PhysicsConfig::new(-20.0)); + let mut scene = ActiveScene::new("Update Test").with_physics(PhysicsConfig::new(-20.0)); // Just verify update doesn't panic scene.update(0.016); diff --git a/crates/rust4d_core/src/scene_loader.rs b/crates/rust4d_core/src/scene_loader.rs index a2fed63..f1719f3 100644 --- a/crates/rust4d_core/src/scene_loader.rs +++ b/crates/rust4d_core/src/scene_loader.rs @@ -5,7 +5,7 @@ //! and returns results via a channel, enabling non-blocking scene loading. use std::path::PathBuf; -use std::sync::mpsc::{channel, Sender, Receiver, TryRecvError}; +use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError}; use std::thread; use crate::scene::{Scene, SceneError}; diff --git a/crates/rust4d_core/src/scene_manager.rs b/crates/rust4d_core/src/scene_manager.rs index d858756..bacaf74 100644 --- a/crates/rust4d_core/src/scene_manager.rs +++ b/crates/rust4d_core/src/scene_manager.rs @@ -21,12 +21,12 @@ //! manager.update(dt); //! ``` -use std::collections::HashMap; -use crate::{Scene, World}; -use crate::scene::{SceneError, ActiveScene}; -use crate::scene_transition::{SceneTransition, TransitionEffect}; +use crate::scene::{ActiveScene, SceneError}; use crate::scene_loader::SceneLoader; +use crate::scene_transition::{SceneTransition, TransitionEffect}; +use crate::{Scene, World}; use rust4d_physics::PhysicsConfig; +use std::collections::HashMap; /// Manages multiple scenes with a stack for overlays /// @@ -114,7 +114,9 @@ impl SceneManager { /// The instantiated scene is stored but not automatically made active. /// Use `push_scene` to make it the current scene. pub fn instantiate(&mut self, template_name: &str) -> Result<(), SceneError> { - let template = self.templates.get(template_name) + let template = self + .templates + .get(template_name) .ok_or_else(|| SceneError::NotLoaded(template_name.to_string()))?; let active = ActiveScene::from_template(template, self.default_physics.clone()); @@ -161,7 +163,8 @@ impl SceneManager { /// Get a reference to the currently active scene (top of stack) pub fn active_scene(&self) -> Option<&ActiveScene> { - self.active_stack.last() + self.active_stack + .last() .and_then(|name| self.scenes.get(name)) } @@ -236,15 +239,9 @@ impl SceneManager { return Err(SceneError::NotLoaded(name.to_string())); } - let from = self.active_scene_name() - .unwrap_or("") - .to_string(); + let from = self.active_scene_name().unwrap_or("").to_string(); - self.transition = Some(SceneTransition::new( - from, - name.to_string(), - effect, - )); + self.transition = Some(SceneTransition::new(from, name.to_string(), effect)); Ok(()) } @@ -346,7 +343,7 @@ impl SceneManager { #[cfg(test)] mod tests { use super::*; - use crate::{ShapeRef, Material, DirtyFlags, Transform4D, Name}; + use crate::{DirtyFlags, Material, Name, ShapeRef, Transform4D}; use rust4d_math::Tesseract4D; fn spawn_test_entity(world: &mut World) -> hecs::Entity { @@ -368,8 +365,7 @@ mod tests { #[test] fn test_with_physics() { - let manager = SceneManager::new() - .with_physics(PhysicsConfig::new(-20.0)); + let manager = SceneManager::new().with_physics(PhysicsConfig::new(-20.0)); assert!(manager.default_physics.is_some()); assert_eq!(manager.default_physics.unwrap().gravity, -20.0); } @@ -551,12 +547,10 @@ mod tests { #[test] fn test_update() { - let mut manager = SceneManager::new() - .with_physics(PhysicsConfig::new(-20.0)); + let mut manager = SceneManager::new().with_physics(PhysicsConfig::new(-20.0)); // Create scene with physics - let scene = ActiveScene::new("Test") - .with_physics(PhysicsConfig::new(-20.0)); + let scene = ActiveScene::new("Test").with_physics(PhysicsConfig::new(-20.0)); manager.register_active_scene("test", scene); manager.push_scene("test").unwrap(); @@ -569,8 +563,7 @@ mod tests { let mut manager = SceneManager::new(); // Create and register a template - let template = Scene::new("My Template") - .with_gravity(-15.0); + let template = Scene::new("My Template").with_gravity(-15.0); manager.register_template(template); // Should be able to retrieve it @@ -636,10 +629,7 @@ mod tests { fn test_switch_with_transition_not_loaded() { let mut manager = SceneManager::new(); - let result = manager.switch_to_with_transition( - "nonexistent", - TransitionEffect::Instant, - ); + let result = manager.switch_to_with_transition("nonexistent", TransitionEffect::Instant); assert!(result.is_err()); match result { Err(SceneError::NotLoaded(name)) => assert_eq!(name, "nonexistent"), @@ -657,7 +647,9 @@ mod tests { manager.push_scene("scene1").unwrap(); // Instant transition should complete in one update - manager.switch_to_with_transition("scene2", TransitionEffect::Instant).unwrap(); + manager + .switch_to_with_transition("scene2", TransitionEffect::Instant) + .unwrap(); let completed = manager.update_transition(); assert!(completed); assert!(!manager.is_transitioning()); @@ -676,12 +668,14 @@ mod tests { assert!(!manager.is_transitioning()); // Start transition - manager.switch_to_with_transition( - "test", - TransitionEffect::Fade { - duration: std::time::Duration::from_secs(10), - }, - ).unwrap(); + manager + .switch_to_with_transition( + "test", + TransitionEffect::Fade { + duration: std::time::Duration::from_secs(10), + }, + ) + .unwrap(); assert!(manager.is_transitioning()); } diff --git a/crates/rust4d_core/src/scene_transition.rs b/crates/rust4d_core/src/scene_transition.rs index a6d92f6..ac34e91 100644 --- a/crates/rust4d_core/src/scene_transition.rs +++ b/crates/rust4d_core/src/scene_transition.rs @@ -171,11 +171,8 @@ mod tests { #[test] fn test_instant_alpha_is_one() { - let transition = SceneTransition::new( - "a".to_string(), - "b".to_string(), - TransitionEffect::Instant, - ); + let transition = + SceneTransition::new("a".to_string(), "b".to_string(), TransitionEffect::Instant); assert_eq!(transition.alpha(), 1.0); } @@ -283,7 +280,11 @@ mod tests { // Right after creation, progress should be near 0 let initial_progress = transition.progress(); - assert!(initial_progress <= 0.1, "Initial progress too high: {}", initial_progress); + assert!( + initial_progress <= 0.1, + "Initial progress too high: {}", + initial_progress + ); // After updating with some time passed, progress should have increased std::thread::sleep(Duration::from_millis(50)); diff --git a/crates/rust4d_core/src/scene_validator.rs b/crates/rust4d_core/src/scene_validator.rs index 1d80974..ff22158 100644 --- a/crates/rust4d_core/src/scene_validator.rs +++ b/crates/rust4d_core/src/scene_validator.rs @@ -127,7 +127,7 @@ mod tests { use super::*; use crate::entity::EntityTemplate; use crate::shapes::ShapeTemplate; - use crate::{Transform4D, Material}; + use crate::{Material, Transform4D}; fn make_valid_scene() -> Scene { let mut scene = Scene::new("Valid Scene") @@ -220,7 +220,9 @@ mod tests { let errors = SceneValidator::validate(&scene); assert!( - !errors.iter().any(|e| matches!(e, ValidationError::UnreasonableGravity(_))), + !errors + .iter() + .any(|e| matches!(e, ValidationError::UnreasonableGravity(_))), "Did not expect gravity error, got: {:?}", errors ); @@ -228,8 +230,7 @@ mod tests { #[test] fn test_extreme_spawn_position_detected() { - let mut scene = Scene::new("Far Away") - .with_player_spawn(99999.0, 0.0, 0.0, 0.0); + let mut scene = Scene::new("Far Away").with_player_spawn(99999.0, 0.0, 0.0, 0.0); scene.add_entity(EntityTemplate::new( ShapeTemplate::tesseract(1.0), Transform4D::identity(), @@ -238,7 +239,9 @@ mod tests { let errors = SceneValidator::validate(&scene); assert!( - errors.contains(&ValidationError::ExtremeSpawnPosition([99999.0, 0.0, 0.0, 0.0])), + errors.contains(&ValidationError::ExtremeSpawnPosition([ + 99999.0, 0.0, 0.0, 0.0 + ])), "Expected ExtremeSpawnPosition, got: {:?}", errors ); @@ -249,7 +252,9 @@ mod tests { let scene = make_valid_scene(); let errors = SceneValidator::validate(&scene); assert!( - !errors.iter().any(|e| matches!(e, ValidationError::ExtremeSpawnPosition(_))), + !errors + .iter() + .any(|e| matches!(e, ValidationError::ExtremeSpawnPosition(_))), "Did not expect spawn error, got: {:?}", errors ); @@ -272,10 +277,17 @@ mod tests { .with_player_spawn(50000.0, 0.0, 0.0, 0.0); let errors = SceneValidator::validate(&scene); - assert!(errors.len() >= 3, "Expected at least 3 errors, got {}: {:?}", errors.len(), errors); + assert!( + errors.len() >= 3, + "Expected at least 3 errors, got {}: {:?}", + errors.len(), + errors + ); assert!(errors.contains(&ValidationError::EmptyScene)); assert!(errors.contains(&ValidationError::UnreasonableGravity(-9999.0))); - assert!(errors.contains(&ValidationError::ExtremeSpawnPosition([50000.0, 0.0, 0.0, 0.0]))); + assert!(errors.contains(&ValidationError::ExtremeSpawnPosition([ + 50000.0, 0.0, 0.0, 0.0 + ]))); } #[test] @@ -295,7 +307,9 @@ mod tests { let errors = SceneValidator::validate(&scene); assert!( - !errors.iter().any(|e| matches!(e, ValidationError::DuplicateName(_))), + !errors + .iter() + .any(|e| matches!(e, ValidationError::DuplicateName(_))), "Unnamed entities should not trigger duplicate name: {:?}", errors ); @@ -316,9 +330,10 @@ mod tests { "Entity 'bar' has no shape" ); assert!(format!("{}", ValidationError::UnreasonableGravity(-5000.0)).contains("-5000")); - assert!( - format!("{}", ValidationError::ExtremeSpawnPosition([1.0, 2.0, 3.0, 4.0])) - .contains("1, 2, 3, 4") - ); + assert!(format!( + "{}", + ValidationError::ExtremeSpawnPosition([1.0, 2.0, 3.0, 4.0]) + ) + .contains("1, 2, 3, 4")); } } diff --git a/crates/rust4d_core/src/shapes.rs b/crates/rust4d_core/src/shapes.rs index 6803d60..1e9df0d 100644 --- a/crates/rust4d_core/src/shapes.rs +++ b/crates/rust4d_core/src/shapes.rs @@ -7,8 +7,8 @@ //! All shapes are created in **local space** (centered at origin or with bottom at y=0). //! The entity transform is used to position them in world space. -use serde::{Serialize, Deserialize}; -use rust4d_math::{Tesseract4D, Hyperplane4D, ConvexShape4D, primitives}; +use rust4d_math::{primitives, ConvexShape4D, Hyperplane4D, Tesseract4D}; +use serde::{Deserialize, Serialize}; /// Serializable shape template /// @@ -121,17 +121,27 @@ impl ShapeTemplate { /// Shapes are created in local space. The entity transform positions them in world space. pub fn create_shape(&self) -> Box { match self { - ShapeTemplate::Tesseract { size } => { - Box::new(Tesseract4D::new(*size)) - } - ShapeTemplate::Hyperplane { size, subdivisions, cell_size, thickness, .. } => { + ShapeTemplate::Tesseract { size } => Box::new(Tesseract4D::new(*size)), + ShapeTemplate::Hyperplane { + size, + subdivisions, + cell_size, + thickness, + .. + } => { // Note: `y` is not passed to the shape constructor - it's used for physics only. // The visual mesh is created at y=0 (local space) and positioned by entity transform. - Box::new(Hyperplane4D::new(*size, *subdivisions as usize, *cell_size, *thickness)) - } - ShapeTemplate::Hypersphere { radius, subdivisions } => { - Box::new(primitives::hypersphere(*radius, *subdivisions)) + Box::new(Hyperplane4D::new( + *size, + *subdivisions as usize, + *cell_size, + *thickness, + )) } + ShapeTemplate::Hypersphere { + radius, + subdivisions, + } => Box::new(primitives::hypersphere(*radius, *subdivisions)), ShapeTemplate::Pentachoron { circumradius } => { Box::new(primitives::pentachoron(*circumradius)) } @@ -144,15 +154,23 @@ impl ShapeTemplate { ShapeTemplate::Hexacosichoron { circumradius } => { Box::new(primitives::hexacosichoron(*circumradius)) } - ShapeTemplate::Spherinder { radius, half_height, subdivisions } => { - Box::new(primitives::spherinder(*radius, *half_height, *subdivisions)) - } - ShapeTemplate::Cubinder { radius, half_size, segments } => { - Box::new(primitives::cubinder(*radius, *half_size, *segments)) - } - ShapeTemplate::Duocylinder { radius_xy, radius_zw, segments } => { - Box::new(primitives::duocylinder(*radius_xy, *radius_zw, *segments, *segments)) - } + ShapeTemplate::Spherinder { + radius, + half_height, + subdivisions, + } => Box::new(primitives::spherinder(*radius, *half_height, *subdivisions)), + ShapeTemplate::Cubinder { + radius, + half_size, + segments, + } => Box::new(primitives::cubinder(*radius, *half_size, *segments)), + ShapeTemplate::Duocylinder { + radius_xy, + radius_zw, + segments, + } => Box::new(primitives::duocylinder( + *radius_xy, *radius_zw, *segments, *segments, + )), } } @@ -163,23 +181,30 @@ impl ShapeTemplate { pub fn bounding_radius(&self) -> f32 { match self { ShapeTemplate::Tesseract { size } => size * 0.5 * 2.0, // half-diagonal = (s/2)·√4 - ShapeTemplate::Hyperplane { size, cell_size, thickness, .. } => { - (2.0 * size * size + cell_size * cell_size + thickness * thickness).sqrt() - } + ShapeTemplate::Hyperplane { + size, + cell_size, + thickness, + .. + } => (2.0 * size * size + cell_size * cell_size + thickness * thickness).sqrt(), ShapeTemplate::Hypersphere { radius, .. } => *radius, ShapeTemplate::Pentachoron { circumradius } | ShapeTemplate::Hexadecachoron { circumradius } | ShapeTemplate::Icositetrachoron { circumradius } | ShapeTemplate::Hexacosichoron { circumradius } => *circumradius, - ShapeTemplate::Spherinder { radius, half_height, .. } => { - (radius * radius + half_height * half_height).sqrt() - } - ShapeTemplate::Cubinder { radius, half_size, .. } => { - (radius * radius + 2.0 * half_size * half_size).sqrt() - } - ShapeTemplate::Duocylinder { radius_xy, radius_zw, .. } => { - (radius_xy * radius_xy + radius_zw * radius_zw).sqrt() - } + ShapeTemplate::Spherinder { + radius, + half_height, + .. + } => (radius * radius + half_height * half_height).sqrt(), + ShapeTemplate::Cubinder { + radius, half_size, .. + } => (radius * radius + 2.0 * half_size * half_size).sqrt(), + ShapeTemplate::Duocylinder { + radius_xy, + radius_zw, + .. + } => (radius_xy * radius_xy + radius_zw * radius_zw).sqrt(), } } @@ -191,11 +216,15 @@ impl ShapeTemplate { pub fn collider_hint(&self) -> ColliderHint { match self { ShapeTemplate::Hypersphere { radius, .. } => ColliderHint::Sphere { radius: *radius }, - ShapeTemplate::Hexacosichoron { circumradius } => { - ColliderHint::Sphere { radius: *circumradius } - } - ShapeTemplate::Tesseract { size } => ColliderHint::Aabb { half_extent: size * 0.5 }, - other => ColliderHint::Aabb { half_extent: other.bounding_radius() * std::f32::consts::FRAC_1_SQRT_2 }, + ShapeTemplate::Hexacosichoron { circumradius } => ColliderHint::Sphere { + radius: *circumradius, + }, + ShapeTemplate::Tesseract { size } => ColliderHint::Aabb { + half_extent: size * 0.5, + }, + other => ColliderHint::Aabb { + half_extent: other.bounding_radius() * std::f32::consts::FRAC_1_SQRT_2, + }, } } @@ -209,28 +238,55 @@ impl ShapeTemplate { /// The `y` parameter specifies the Y-level for the physics collider. /// The visual mesh is created in local space (y=0) and should be positioned /// using the entity transform. - pub fn hyperplane(y: f32, size: f32, subdivisions: u32, cell_size: f32, thickness: f32) -> Self { - ShapeTemplate::Hyperplane { y, size, subdivisions, cell_size, thickness } + pub fn hyperplane( + y: f32, + size: f32, + subdivisions: u32, + cell_size: f32, + thickness: f32, + ) -> Self { + ShapeTemplate::Hyperplane { + y, + size, + subdivisions, + cell_size, + thickness, + } } /// Create a hypersphere template at default quality pub fn hypersphere(radius: f32) -> Self { - ShapeTemplate::Hypersphere { radius, subdivisions: 2 } + ShapeTemplate::Hypersphere { + radius, + subdivisions: 2, + } } /// Create a spherinder template at default quality pub fn spherinder(radius: f32, half_height: f32) -> Self { - ShapeTemplate::Spherinder { radius, half_height, subdivisions: 2 } + ShapeTemplate::Spherinder { + radius, + half_height, + subdivisions: 2, + } } /// Create a cubinder template at default quality pub fn cubinder(radius: f32, half_size: f32) -> Self { - ShapeTemplate::Cubinder { radius, half_size, segments: 24 } + ShapeTemplate::Cubinder { + radius, + half_size, + segments: 24, + } } /// Create a duocylinder template at default quality pub fn duocylinder(radius_xy: f32, radius_zw: f32) -> Self { - ShapeTemplate::Duocylinder { radius_xy, radius_zw, segments: 24 } + ShapeTemplate::Duocylinder { + radius_xy, + radius_zw, + segments: 24, + } } } @@ -310,7 +366,10 @@ mod tests { // Scene files may omit resolution fields; they get sane defaults. let s: ShapeTemplate = ron::from_str("(type: \"Hypersphere\", radius: 2.0)").unwrap(); match s { - ShapeTemplate::Hypersphere { radius, subdivisions } => { + ShapeTemplate::Hypersphere { + radius, + subdivisions, + } => { assert_eq!(radius, 2.0); assert_eq!(subdivisions, 2); } @@ -353,7 +412,13 @@ mod tests { let deserialized: ShapeTemplate = ron::from_str(&serialized).unwrap(); match deserialized { - ShapeTemplate::Hyperplane { y, size, subdivisions, cell_size, thickness } => { + ShapeTemplate::Hyperplane { + y, + size, + subdivisions, + cell_size, + thickness, + } => { assert_eq!(y, -2.0); assert_eq!(size, 4.0); assert_eq!(subdivisions, 4); diff --git a/crates/rust4d_core/src/transform.rs b/crates/rust4d_core/src/transform.rs index 57b81a3..4e26b7a 100644 --- a/crates/rust4d_core/src/transform.rs +++ b/crates/rust4d_core/src/transform.rs @@ -2,8 +2,8 @@ //! //! A Transform4D represents the position, rotation, and scale of an entity in 4D space. -use rust4d_math::{Vec4, Rotor4}; -use serde::{Serialize, Deserialize}; +use rust4d_math::{Rotor4, Vec4}; +use serde::{Deserialize, Serialize}; /// A 4D transform with position, rotation, and uniform scale #[derive(Clone, Copy, Debug, Serialize, Deserialize)] @@ -172,7 +172,11 @@ mod tests { let t = Transform4D::from_position_rotation(Vec4::ZERO, rotor); let p = Vec4::X; let transformed = t.transform_point(p); - assert!(vec_approx_eq(transformed, Vec4::Y), "Expected Y, got {:?}", transformed); + assert!( + vec_approx_eq(transformed, Vec4::Y), + "Expected Y, got {:?}", + transformed + ); } #[test] @@ -187,8 +191,11 @@ mod tests { // X * 2 = (2, 0, 0, 0), rotated 90° in XY = (0, 2, 0, 0), + (10, 0, 0, 0) = (10, 2, 0, 0) let p = Vec4::X; let transformed = t.transform_point(p); - assert!(vec_approx_eq(transformed, Vec4::new(10.0, 2.0, 0.0, 0.0)), - "Expected (10, 2, 0, 0), got {:?}", transformed); + assert!( + vec_approx_eq(transformed, Vec4::new(10.0, 2.0, 0.0, 0.0)), + "Expected (10, 2, 0, 0), got {:?}", + transformed + ); } #[test] diff --git a/crates/rust4d_core/src/world.rs b/crates/rust4d_core/src/world.rs index 48bda11..8c1d326 100644 --- a/crates/rust4d_core/src/world.rs +++ b/crates/rust4d_core/src/world.rs @@ -4,11 +4,11 @@ //! It provides side-tables for name lookups, tag indexing, and physics integration, //! and stores hierarchy as Parent/Children components. -use std::collections::{HashMap, HashSet, VecDeque}; -use std::fmt; use crate::components::*; use crate::{DirtyFlags, Transform4D}; use rust4d_physics::{PhysicsConfig, PhysicsWorld}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::fmt; /// Error type for hierarchy operations #[derive(Debug, Clone, PartialEq, Eq)] @@ -113,7 +113,10 @@ impl World { let name_str = name.0.clone(); drop(name); if let Some(_old) = self.name_index.insert(name_str.clone(), entity) { - log::warn!("Name '{}' already exists in world; overwriting index entry", name_str); + log::warn!( + "Name '{}' already exists in world; overwriting index entry", + name_str + ); } } @@ -193,7 +196,10 @@ impl World { if let Some(ref mut physics) = self.physics_world { let result = physics.remove_body(body_key); if result.is_none() { - log::warn!("Physics body {:?} not found during entity despawn cleanup", body_key); + log::warn!( + "Physics body {:?} not found during entity despawn cleanup", + body_key + ); } } } @@ -207,7 +213,9 @@ impl World { } // Orphan all children (remove Parent component, add them to roots) - let child_list: Vec = self.ecs.get::<&Children>(entity) + let child_list: Vec = self + .ecs + .get::<&Children>(entity) .ok() .map(|c| c.0.clone()) .unwrap_or_default(); @@ -253,7 +261,11 @@ impl World { /// /// Returns `Some(())` on success, `None` if the entity doesn't exist /// or doesn't have a Name component. - pub fn rename_entity(&mut self, entity: hecs::Entity, new_name: impl Into) -> Option<()> { + pub fn rename_entity( + &mut self, + entity: hecs::Entity, + new_name: impl Into, + ) -> Option<()> { // Remove old name from index let old_name = self.ecs.get::<&Name>(entity).ok().map(|n| n.0.clone())?; self.name_index.remove(&old_name); @@ -267,7 +279,10 @@ impl World { // Add new name to index if let Some(_old) = self.name_index.insert(new_name.clone(), entity) { - log::warn!("Name '{}' already exists in world; overwriting index entry", new_name); + log::warn!( + "Name '{}' already exists in world; overwriting index entry", + new_name + ); } Some(()) @@ -289,7 +304,8 @@ impl World { /// /// Uses the tag index for O(1) lookup instead of scanning. pub fn get_by_tag(&self, tag: &str) -> Vec { - self.tag_index.get(tag) + self.tag_index + .get(tag) .map(|set| set.iter().copied().collect()) .unwrap_or_default() } @@ -335,7 +351,8 @@ impl World { // Sync entity transforms from their physics bodies if let Some(ref physics) = self.physics_world { for (_entity, (transform, body, dirty)) in - self.ecs.query_mut::<(&mut Transform4D, &PhysicsBody, &mut DirtyFlags)>() + self.ecs + .query_mut::<(&mut Transform4D, &PhysicsBody, &mut DirtyFlags)>() { if let Some(phys_body) = physics.get_body(body.0) { if transform.position != phys_body.position { @@ -375,7 +392,8 @@ impl World { /// Get an entity's children pub fn children_of(&self, entity: hecs::Entity) -> Vec { - self.ecs.get::<&Children>(entity) + self.ecs + .get::<&Children>(entity) .ok() .map(|c| c.0.clone()) .unwrap_or_default() @@ -383,7 +401,8 @@ impl World { /// Check if an entity has any children pub fn has_children(&self, entity: hecs::Entity) -> bool { - self.ecs.get::<&Children>(entity) + self.ecs + .get::<&Children>(entity) .ok() .is_some_and(|c| !c.0.is_empty()) } @@ -399,7 +418,11 @@ impl World { /// that parent (reparenting). Returns an error if either entity does not /// exist, if the relationship would create a cycle, or if the child is /// already a child of the specified parent. - pub fn add_child(&mut self, parent: hecs::Entity, child: hecs::Entity) -> Result<(), HierarchyError> { + pub fn add_child( + &mut self, + parent: hecs::Entity, + child: hecs::Entity, + ) -> Result<(), HierarchyError> { // Validate both entities exist if !self.ecs.contains(parent) || !self.ecs.contains(child) { return Err(HierarchyError::InvalidEntity); @@ -488,7 +511,10 @@ impl World { while let Ok(parent) = self.ecs.get::<&Parent>(current) { depth += 1; if depth > MAX_DEPTH { - log::error!("world_transform: depth limit ({}) exceeded for entity, possible cycle", MAX_DEPTH); + log::error!( + "world_transform: depth limit ({}) exceeded for entity, possible cycle", + MAX_DEPTH + ); break; } let parent_entity = parent.0; @@ -631,7 +657,6 @@ impl World { } } - // Check all Children components for stale/non-existent references for (entity, children) in self.ecs.query::<&Children>().iter() { for &child in &children.0 { @@ -698,7 +723,9 @@ impl World { // PhysicsBody requires Transform4D for (entity, _body) in self.ecs.query::<&PhysicsBody>().iter() { if self.ecs.get::<&Transform4D>(entity).is_err() { - let name = self.ecs.get::<&Name>(entity) + let name = self + .ecs + .get::<&Name>(entity) .map(|n| format!(" (\"{}\")", n.0)) .unwrap_or_default(); warnings.push(format!( @@ -712,7 +739,9 @@ impl World { for (entity, children) in self.ecs.query::<&Children>().iter() { for &child in &children.0 { if self.ecs.contains(child) && self.ecs.get::<&Parent>(child).is_err() { - let name = self.ecs.get::<&Name>(entity) + let name = self + .ecs + .get::<&Name>(entity) .map(|n| format!(" (\"{}\")", n.0)) .unwrap_or_default(); warnings.push(format!( @@ -726,7 +755,9 @@ impl World { // Parent component but no Transform4D (hierarchy nodes usually need transforms) for (entity, _parent) in self.ecs.query::<&Parent>().iter() { if self.ecs.get::<&Transform4D>(entity).is_err() { - let name = self.ecs.get::<&Name>(entity) + let name = self + .ecs + .get::<&Name>(entity) .map(|n| format!(" (\"{}\")", n.0)) .unwrap_or_default(); warnings.push(format!( @@ -779,7 +810,8 @@ mod tests { fn spawn_tagged_entity(world: &mut World, name: &str, tags: &[&str]) -> hecs::Entity { let tesseract = Tesseract4D::new(2.0); - let tag_set: std::collections::HashSet = tags.iter().map(|t| t.to_string()).collect(); + let tag_set: std::collections::HashSet = + tags.iter().map(|t| t.to_string()).collect(); world.spawn(( ShapeRef::shared(tesseract), Transform4D::identity(), @@ -823,7 +855,10 @@ mod tests { let handle = spawn_test_entity(&mut world); { - let mut material = world.ecs_mut_unchecked().get::<&mut Material>(handle).unwrap(); + let mut material = world + .ecs_mut_unchecked() + .get::<&mut Material>(handle) + .unwrap(); *material = Material::RED; } @@ -901,8 +936,8 @@ mod tests { #[test] fn test_world_with_physics() { - use rust4d_physics::RigidBody4D; use rust4d_math::Vec4; + use rust4d_physics::RigidBody4D; // Create a world with physics enabled (no gravity for predictable test) let config = PhysicsConfig::new(0.0); @@ -938,15 +973,18 @@ mod tests { // Entity transform should now reflect the physics body position let transform = world.ecs().get::<&Transform4D>(entity_handle).unwrap(); - assert!((transform.position.x - 10.0).abs() < 0.2, - "Expected ~10.0, got {}", transform.position.x); + assert!( + (transform.position.x - 10.0).abs() < 0.2, + "Expected ~10.0, got {}", + transform.position.x + ); assert!((transform.position.y - 5.0).abs() < 0.001); } #[test] fn test_physics_sync_with_gravity() { - use rust4d_physics::RigidBody4D; use rust4d_math::Vec4; + use rust4d_physics::RigidBody4D; // Create a world with gravity (default config) let mut world = World::new().with_physics(PhysicsConfig::default()); @@ -1127,13 +1165,19 @@ mod tests { // Manually mark one as dirty { - let mut dirty = world.ecs_mut_unchecked().get::<&mut DirtyFlags>(key1).unwrap(); + let mut dirty = world + .ecs_mut_unchecked() + .get::<&mut DirtyFlags>(key1) + .unwrap(); *dirty |= DirtyFlags::TRANSFORM; } // Note: dirty_count won't track this since we used ecs_mut_unchecked // Count dirty entities via query - let dirty_count = world.ecs().query::<&DirtyFlags>().iter() + let dirty_count = world + .ecs() + .query::<&DirtyFlags>() + .iter() .filter(|(_, d)| !d.is_empty()) .count(); assert_eq!(dirty_count, 1); @@ -1141,8 +1185,8 @@ mod tests { #[test] fn test_physics_sync_marks_dirty() { - use rust4d_physics::RigidBody4D; use rust4d_math::Vec4; + use rust4d_physics::RigidBody4D; // Create a world with physics enabled (no gravity for predictable test) let config = PhysicsConfig::new(0.0); @@ -1178,8 +1222,8 @@ mod tests { #[test] fn test_physics_sync_no_change_not_dirty() { - use rust4d_physics::RigidBody4D; use rust4d_math::Vec4; + use rust4d_physics::RigidBody4D; // Create a world with physics (no gravity, no velocity = no movement) let config = PhysicsConfig::new(0.0); @@ -1259,16 +1303,10 @@ mod tests { assert!(world.add_child(a, b).is_ok()); // B -> A would create a cycle - assert_eq!( - world.add_child(b, a), - Err(HierarchyError::CyclicHierarchy) - ); + assert_eq!(world.add_child(b, a), Err(HierarchyError::CyclicHierarchy)); // Self-parenting should also be rejected - assert_eq!( - world.add_child(a, a), - Err(HierarchyError::CyclicHierarchy) - ); + assert_eq!(world.add_child(a, a), Err(HierarchyError::CyclicHierarchy)); } #[test] @@ -1283,10 +1321,7 @@ mod tests { assert!(world.add_child(b, c).is_ok()); // C -> A would create a cycle (A is ancestor of C) - assert_eq!( - world.add_child(c, a), - Err(HierarchyError::CyclicHierarchy) - ); + assert_eq!(world.add_child(c, a), Err(HierarchyError::CyclicHierarchy)); } #[test] @@ -1346,10 +1381,16 @@ mod tests { world.add_child(parent, child).unwrap(); let wt = world.world_transform(child).unwrap(); - assert!((wt.position.x - 11.0).abs() < 0.001, - "Expected x=11.0, got {}", wt.position.x); - assert!((wt.position.y - 2.0).abs() < 0.001, - "Expected y=2.0, got {}", wt.position.y); + assert!( + (wt.position.x - 11.0).abs() < 0.001, + "Expected x=11.0, got {}", + wt.position.x + ); + assert!( + (wt.position.y - 2.0).abs() < 0.001, + "Expected y=2.0, got {}", + wt.position.y + ); } #[test] @@ -1358,7 +1399,8 @@ mod tests { // Parent with scale 2 at origin let tesseract = Tesseract4D::new(2.0); - let mut parent_transform = Transform4D::from_position(rust4d_math::Vec4::new(0.0, 0.0, 0.0, 0.0)); + let mut parent_transform = + Transform4D::from_position(rust4d_math::Vec4::new(0.0, 0.0, 0.0, 0.0)); parent_transform.scale = 2.0; let parent = world.spawn(( ShapeRef::shared(tesseract), @@ -1373,8 +1415,11 @@ mod tests { world.add_child(parent, child).unwrap(); let wt = world.world_transform(child).unwrap(); - assert!((wt.position.x - 2.0).abs() < 0.001, - "Expected x=2.0, got {}", wt.position.x); + assert!( + (wt.position.x - 2.0).abs() < 0.001, + "Expected x=2.0, got {}", + wt.position.x + ); } #[test] @@ -1484,9 +1529,9 @@ mod tests { world.add_child(a, b).unwrap(); world.add_child(b, c).unwrap(); - assert!(world.is_ancestor(a, b)); // A is ancestor of B - assert!(world.is_ancestor(a, c)); // A is ancestor of C (transitive) - assert!(world.is_ancestor(b, c)); // B is ancestor of C + assert!(world.is_ancestor(a, b)); // A is ancestor of B + assert!(world.is_ancestor(a, c)); // A is ancestor of C (transitive) + assert!(world.is_ancestor(b, c)); // B is ancestor of C assert!(!world.is_ancestor(c, a)); // C is NOT ancestor of A assert!(!world.is_ancestor(a, a)); // Not ancestor of self assert!(!world.is_ancestor(a, d)); // D is unrelated @@ -1579,8 +1624,11 @@ mod tests { world.add_child(parent, child).unwrap(); let wt = world.world_transform(child).unwrap(); - assert!((wt.position.x - 16.0).abs() < 0.001, - "Expected x=16.0, got {}", wt.position.x); + assert!( + (wt.position.x - 16.0).abs() < 0.001, + "Expected x=16.0, got {}", + wt.position.x + ); } #[test] @@ -1788,7 +1836,7 @@ mod tests { #[test] fn test_despawn_cleans_up_physics_body() { - use rust4d_physics::{PhysicsMaterial, BodyType}; + use rust4d_physics::{BodyType, PhysicsMaterial}; let config = crate::PhysicsConfig::new(-9.81); let mut world = World::new().with_physics(config); @@ -1799,9 +1847,10 @@ mod tests { let body = crate::RigidBody4D::new_aabb( rust4d_math::Vec4::new(0.0, 5.0, 0.0, 0.0), rust4d_math::Vec4::new(0.5, 0.5, 0.5, 0.5), - ).with_body_type(BodyType::Dynamic) - .with_mass(1.0) - .with_material(PhysicsMaterial::RUBBER); + ) + .with_body_type(BodyType::Dynamic) + .with_mass(1.0) + .with_material(PhysicsMaterial::RUBBER); physics.add_body(body) }; @@ -1831,10 +1880,15 @@ mod tests { // Manually set Parent to nonexistent entity (bypassing add_child) let fake_parent = hecs::Entity::DANGLING; - let _ = world.ecs_mut_unchecked().insert_one(child, Parent(fake_parent)); + let _ = world + .ecs_mut_unchecked() + .insert_one(child, Parent(fake_parent)); let issues = world.validate_hierarchy(); - assert!(!issues.is_empty(), "Should detect parent pointing to nonexistent entity"); + assert!( + !issues.is_empty(), + "Should detect parent pointing to nonexistent entity" + ); } #[test] @@ -1847,7 +1901,10 @@ mod tests { let _ = world.ecs_mut_unchecked().insert_one(child, Parent(parent)); let issues = world.validate_hierarchy(); - assert!(!issues.is_empty(), "Should detect child not listed in parent's Children"); + assert!( + !issues.is_empty(), + "Should detect child not listed in parent's Children" + ); } #[test] @@ -1865,7 +1922,8 @@ mod tests { let issues = world.validate_hierarchy(); assert!( issues.iter().any(|i| i.contains("non-existent")), - "Should detect stale child reference; got: {:?}", issues + "Should detect stale child reference; got: {:?}", + issues ); } @@ -1880,12 +1938,15 @@ mod tests { world.add_child(parent_a, child).unwrap(); // Now manually add child to parent_b's Children without updating child's Parent - let _ = world.ecs_mut_unchecked().insert_one(parent_b, Children(vec![child])); + let _ = world + .ecs_mut_unchecked() + .insert_one(parent_b, Children(vec![child])); let issues = world.validate_hierarchy(); assert!( issues.iter().any(|i| i.contains("child's Parent")), - "Should detect child-parent mismatch; got: {:?}", issues + "Should detect child-parent mismatch; got: {:?}", + issues ); } @@ -1893,14 +1954,17 @@ mod tests { fn test_validate_component_schemas_physics_no_transform() { let mut world = World::new(); // Spawn entity with only PhysicsBody, no Transform4D - let _entity = world.ecs_mut_unchecked().spawn(( - PhysicsBody(crate::BodyKey::default()), - )); + let _entity = world + .ecs_mut_unchecked() + .spawn((PhysicsBody(crate::BodyKey::default()),)); let warnings = world.validate_component_schemas(); assert!( - warnings.iter().any(|w| w.contains("PhysicsBody but no Transform4D")), - "Should detect PhysicsBody without Transform4D; got: {:?}", warnings + warnings + .iter() + .any(|w| w.contains("PhysicsBody but no Transform4D")), + "Should detect PhysicsBody without Transform4D; got: {:?}", + warnings ); } @@ -1911,12 +1975,17 @@ mod tests { let child = spawn_test_entity(&mut world); // Manually add child to parent's Children without setting Parent on child - let _ = world.ecs_mut_unchecked().insert_one(parent, Children(vec![child])); + let _ = world + .ecs_mut_unchecked() + .insert_one(parent, Children(vec![child])); let warnings = world.validate_component_schemas(); assert!( - warnings.iter().any(|w| w.contains("lacks a Parent component")), - "Should detect child without Parent; got: {:?}", warnings + warnings + .iter() + .any(|w| w.contains("lacks a Parent component")), + "Should detect child without Parent; got: {:?}", + warnings ); } @@ -1930,8 +1999,8 @@ mod tests { let warnings = world.validate_component_schemas(); assert!( warnings.is_empty(), - "Valid world should have no schema warnings; got: {:?}", warnings + "Valid world should have no schema warnings; got: {:?}", + warnings ); } - } diff --git a/crates/rust4d_core/tests/physics_integration.rs b/crates/rust4d_core/tests/physics_integration.rs index 0973441..a5ae0a5 100644 --- a/crates/rust4d_core/tests/physics_integration.rs +++ b/crates/rust4d_core/tests/physics_integration.rs @@ -7,13 +7,13 @@ //! 4. Dirty flags trigger geometry rebuild use rust4d_core::{ - ActiveScene, Scene, World, EntityTemplate, Transform4D, Material, ShapeRef, - ShapeTemplate, DirtyFlags, PhysicsBody, Name, + ActiveScene, DirtyFlags, EntityTemplate, Material, Name, PhysicsBody, Scene, ShapeRef, + ShapeTemplate, Transform4D, World, }; +use rust4d_math::{Tesseract4D, Vec4}; use rust4d_physics::{ - PhysicsConfig, PhysicsWorld, RigidBody4D, BodyType, StaticCollider, PhysicsMaterial, + BodyType, PhysicsConfig, PhysicsMaterial, PhysicsWorld, RigidBody4D, StaticCollider, }; -use rust4d_math::{Vec4, Tesseract4D}; // ==================== Scene Loading Tests ==================== @@ -30,14 +30,16 @@ fn test_scene_dynamic_entity_has_physics_body() { Material::WHITE, ) .with_name("tesseract") - .with_tag("dynamic") + .with_tag("dynamic"), ); // Instantiate the scene let active = ActiveScene::from_template(&scene, None); // Get the entity - let entity_handle = active.world.get_by_name("tesseract") + let entity_handle = active + .world + .get_by_name("tesseract") .expect("Tesseract entity should exist"); // Verify physics body was created @@ -50,13 +52,12 @@ fn test_scene_dynamic_entity_has_physics_body() { // Verify the body exists in the physics world let physics = active.world.physics().expect("World should have physics"); let body_key = body_comp.unwrap().0; - let body = physics.get_body(body_key).expect("Physics body should exist"); + let body = physics + .get_body(body_key) + .expect("Physics body should exist"); // Verify body type is Dynamic - assert!( - !body.is_static(), - "Body should not be static" - ); + assert!(!body.is_static(), "Body should not be static"); assert!( body.affected_by_gravity(), "Dynamic body should be affected by gravity" @@ -75,7 +76,7 @@ fn test_scene_static_floor_has_collider() { Material::GRAY, ) .with_name("floor") - .with_tag("static") + .with_tag("static"), ); let active = ActiveScene::from_template(&scene, None); @@ -140,16 +141,15 @@ fn test_dynamic_body_lands_on_floor() { // Body should be near the floor (radius 0.5, floor at 0, so center at ~0.5) assert!( body.position.y < 1.0, - "Body should have fallen. Y={}", body.position.y + "Body should have fallen. Y={}", + body.position.y ); assert!( body.position.y > -1.0, - "Body should be above floor. Y={}", body.position.y - ); - assert!( - body.grounded, - "Body should be grounded after settling" + "Body should be above floor. Y={}", + body.position.y ); + assert!(body.grounded, "Body should be grounded after settling"); } /// Test bounded floor collision (the specific bug scenario) @@ -159,17 +159,17 @@ fn test_aabb_body_lands_on_bounded_floor() { // Add a bounded floor at y=-2 (matching default.ron) physics.add_static_collider(StaticCollider::floor_bounded( - -2.0, // y (surface level) - 10.0, // half_size_xz - 5.0, // half_size_w - 5.0, // thickness (minimum) + -2.0, // y (surface level) + 10.0, // half_size_xz + 5.0, // half_size_w + 5.0, // thickness (minimum) PhysicsMaterial::CONCRETE, )); // Add an AABB body at y=0 with half_extent=1 (matching tesseract in default.ron) let body = RigidBody4D::new_aabb( - Vec4::new(0.0, 0.0, 0.0, 0.0), // position - Vec4::new(1.0, 1.0, 1.0, 1.0), // half_extents + Vec4::new(0.0, 0.0, 0.0, 0.0), // position + Vec4::new(1.0, 1.0, 1.0, 1.0), // half_extents ) .with_body_type(BodyType::Dynamic) .with_mass(10.0) @@ -190,18 +190,22 @@ fn test_aabb_body_lands_on_bounded_floor() { // Body should have fallen from y=0 assert!( body.position.y < initial_y, - "Body should have fallen. Initial: {}, Final: {}", initial_y, body.position.y + "Body should have fallen. Initial: {}, Final: {}", + initial_y, + body.position.y ); // Body center should be at approximately y=-1 (bottom at y=-2, floor surface at y=-2) // With half_extent.y=1, center at y=-1 means bottom is at y=-2 (floor surface) assert!( body.position.y > -2.0, - "Body should be above floor. Y={}", body.position.y + "Body should be above floor. Y={}", + body.position.y ); assert!( body.position.y < 0.0, - "Body should be below starting position. Y={}", body.position.y + "Body should be below starting position. Y={}", + body.position.y ); // Body should be grounded @@ -245,7 +249,8 @@ fn test_entity_transform_syncs_from_physics() { let transform = world.ecs().get::<&Transform4D>(entity_handle).unwrap(); assert!( transform.position.y < 10.0, - "Entity should have moved. Y={}", transform.position.y + "Entity should have moved. Y={}", + transform.position.y ); // Entity should be marked dirty @@ -273,7 +278,7 @@ fn test_scene_dynamic_entity_falls_to_floor() { Material::GRAY, ) .with_name("floor") - .with_tag("static") + .with_tag("static"), ); // Add tesseract at y=0 @@ -284,7 +289,7 @@ fn test_scene_dynamic_entity_falls_to_floor() { Material::WHITE, ) .with_name("tesseract") - .with_tag("dynamic") + .with_tag("dynamic"), ); // Instantiate scene @@ -292,7 +297,13 @@ fn test_scene_dynamic_entity_falls_to_floor() { // Get initial tesseract position let entity_handle = active.world.get_by_name("tesseract").unwrap(); - let initial_y = active.world.ecs().get::<&Transform4D>(entity_handle).unwrap().position.y; + let initial_y = active + .world + .ecs() + .get::<&Transform4D>(entity_handle) + .unwrap() + .position + .y; // Simulate 2 seconds (120 frames at 60fps) for _ in 0..120 { @@ -301,32 +312,43 @@ fn test_scene_dynamic_entity_falls_to_floor() { // Get final position let entity_handle = active.world.get_by_name("tesseract").unwrap(); - let final_y = active.world.ecs().get::<&Transform4D>(entity_handle).unwrap().position.y; + let final_y = active + .world + .ecs() + .get::<&Transform4D>(entity_handle) + .unwrap() + .position + .y; // Tesseract should have fallen assert!( final_y < initial_y, - "Tesseract should have fallen. Initial: {}, Final: {}", initial_y, final_y + "Tesseract should have fallen. Initial: {}, Final: {}", + initial_y, + final_y ); // Tesseract should be near the floor (center at ~-1, bottom at -2) assert!( final_y > -2.0, - "Tesseract should be above floor surface. Y={}", final_y + "Tesseract should be above floor surface. Y={}", + final_y ); assert!( final_y < 0.0, - "Tesseract should be below starting position. Y={}", final_y + "Tesseract should be below starting position. Y={}", + final_y ); // Verify physics body is grounded let physics = active.world.physics().unwrap(); - let body_comp = active.world.ecs().get::<&PhysicsBody>(entity_handle).unwrap(); + let body_comp = active + .world + .ecs() + .get::<&PhysicsBody>(entity_handle) + .unwrap(); let body = physics.get_body(body_comp.0).unwrap(); - assert!( - body.grounded, - "Tesseract physics body should be grounded" - ); + assert!(body.grounded, "Tesseract physics body should be grounded"); } /// Test with actual scene file (requires scenes/default.ron to exist) @@ -343,15 +365,27 @@ fn test_load_default_scene_file() { let mut active = ActiveScene::from_template(&scene, None); // Verify tesseract entity exists and has physics body - let entity_handle = active.world.get_by_name("tesseract") + let entity_handle = active + .world + .get_by_name("tesseract") .expect("Tesseract entity should exist in default scene"); assert!( - active.world.ecs().get::<&PhysicsBody>(entity_handle).is_ok(), + active + .world + .ecs() + .get::<&PhysicsBody>(entity_handle) + .is_ok(), "Tesseract should have physics body" ); - let initial_y = active.world.ecs().get::<&Transform4D>(entity_handle).unwrap().position.y; + let initial_y = active + .world + .ecs() + .get::<&Transform4D>(entity_handle) + .unwrap() + .position + .y; // Simulate 2 seconds for _ in 0..120 { @@ -360,12 +394,20 @@ fn test_load_default_scene_file() { // Get final position let entity_handle = active.world.get_by_name("tesseract").unwrap(); - let final_y = active.world.ecs().get::<&Transform4D>(entity_handle).unwrap().position.y; + let final_y = active + .world + .ecs() + .get::<&Transform4D>(entity_handle) + .unwrap() + .position + .y; // Tesseract should have fallen assert!( final_y < initial_y, - "Tesseract should have fallen from {} to near floor. Final: {}", initial_y, final_y + "Tesseract should have fallen from {} to near floor. Final: {}", + initial_y, + final_y ); } @@ -378,7 +420,11 @@ fn test_kinematic_body_falls_off_w_edge() { // Add bounded floor: W extends from -5 to +5 physics.add_static_collider(StaticCollider::floor_bounded( - -2.0, 10.0, 5.0, 5.0, PhysicsMaterial::CONCRETE, + -2.0, + 10.0, + 5.0, + 5.0, + PhysicsMaterial::CONCRETE, )); // Add kinematic sphere at center, resting on floor, with gravity enabled @@ -393,7 +439,10 @@ fn test_kinematic_body_falls_off_w_edge() { } // Body should be grounded - assert!(physics.body_is_grounded(key), "Body should be grounded at center"); + assert!( + physics.body_is_grounded(key), + "Body should be grounded at center" + ); let start_y = physics.body_position(key).unwrap().y; // Move body to W=6 (outside floor's W bounds of -5 to +5) @@ -403,10 +452,17 @@ fn test_kinematic_body_falls_off_w_edge() { } let pos = physics.body_position(key).unwrap(); - assert!(pos.w > 5.0, "Body should have moved off W edge. W={}", pos.w); + assert!( + pos.w > 5.0, + "Body should have moved off W edge. W={}", + pos.w + ); - assert!(!physics.body_is_grounded(key), - "Body should NOT be grounded when off W edge. W={}", pos.w); + assert!( + !physics.body_is_grounded(key), + "Body should NOT be grounded when off W edge. W={}", + pos.w + ); // Continue stepping - body should fall for _ in 0..60 { @@ -415,8 +471,12 @@ fn test_kinematic_body_falls_off_w_edge() { } let final_pos = physics.body_position(key).unwrap(); - assert!(final_pos.y < start_y, - "Body should fall when off W edge. Start Y={}, Final Y={}", start_y, final_pos.y); + assert!( + final_pos.y < start_y, + "Body should fall when off W edge. Start Y={}, Final Y={}", + start_y, + final_pos.y + ); } /// Print detailed state for debugging @@ -426,16 +486,17 @@ fn test_physics_step_trace() { // Add bounded floor at y=-2 physics.add_static_collider(StaticCollider::floor_bounded( - -2.0, 10.0, 5.0, 5.0, PhysicsMaterial::CONCRETE, + -2.0, + 10.0, + 5.0, + 5.0, + PhysicsMaterial::CONCRETE, )); // Add AABB body at y=0 - let body = RigidBody4D::new_aabb( - Vec4::new(0.0, 0.0, 0.0, 0.0), - Vec4::new(1.0, 1.0, 1.0, 1.0), - ) - .with_body_type(BodyType::Dynamic) - .with_mass(10.0); + let body = RigidBody4D::new_aabb(Vec4::new(0.0, 0.0, 0.0, 0.0), Vec4::new(1.0, 1.0, 1.0, 1.0)) + .with_body_type(BodyType::Dynamic) + .with_mass(10.0); let key = physics.add_body(body); @@ -453,8 +514,10 @@ fn test_physics_step_trace() { } let body = physics.get_body(key).unwrap(); - println!("Final: pos.y={:.4}, vel.y={:.4}, grounded={}", - body.position.y, body.velocity.y, body.grounded); + println!( + "Final: pos.y={:.4}, vel.y={:.4}, grounded={}", + body.position.y, body.velocity.y, body.grounded + ); // Should be falling assert!(body.position.y < 0.0, "Body should have fallen"); @@ -489,7 +552,8 @@ fn test_remove_entity_cleans_up_physics_body() { "Physics body should exist before entity removal" ); assert_eq!( - world.physics().unwrap().body_count(), 1, + world.physics().unwrap().body_count(), + 1, "Should have exactly 1 physics body" ); @@ -503,7 +567,8 @@ fn test_remove_entity_cleans_up_physics_body() { "Physics body should be removed when entity is removed" ); assert_eq!( - world.physics().unwrap().body_count(), 0, + world.physics().unwrap().body_count(), + 0, "Should have 0 physics bodies after entity removal" ); } @@ -547,6 +612,8 @@ fn test_remove_entity_world_without_physics() { // Remove should work fine let removed = world.despawn(entity_key); - assert!(removed, "Entity should be removed even without physics world"); + assert!( + removed, + "Entity should be removed even without physics world" + ); } - diff --git a/crates/rust4d_game/src/character_controller.rs b/crates/rust4d_game/src/character_controller.rs index 09c6649..d4454c6 100644 --- a/crates/rust4d_game/src/character_controller.rs +++ b/crates/rust4d_game/src/character_controller.rs @@ -91,7 +91,7 @@ impl CharacterController4D { #[cfg(test)] mod tests { use super::*; - use rust4d_physics::{PhysicsConfig, RigidBody4D, BodyType, PhysicsMaterial, StaticCollider}; + use rust4d_physics::{BodyType, PhysicsConfig, PhysicsMaterial, RigidBody4D, StaticCollider}; /// Create a test physics world with a floor at y=0 fn test_world_with_floor() -> PhysicsWorld { @@ -185,8 +185,16 @@ mod tests { physics.step(1.0); let pos = physics.body_position(body_key).unwrap(); - assert!((pos.x - 2.0).abs() < 0.01, "Expected x=2.0 (move_speed), got {}", pos.x); - assert!((pos.w - 5.0).abs() < 0.01, "Expected w=5.0 (w_move_speed), got {}", pos.w); + assert!( + (pos.x - 2.0).abs() < 0.01, + "Expected x=2.0 (move_speed), got {}", + pos.x + ); + assert!( + (pos.w - 5.0).abs() < 0.01, + "Expected w=5.0 (w_move_speed), got {}", + pos.w + ); } #[test] @@ -196,9 +204,8 @@ mod tests { // orthogonal after speed scaling, for ANY speed configuration. // World-axis anisotropic scaling violated this. let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); - let body_key = physics.add_body( - RigidBody4D::new_sphere(Vec4::ZERO, 0.5).with_body_type(BodyType::Kinematic), - ); + let body_key = physics + .add_body(RigidBody4D::new_sphere(Vec4::ZERO, 0.5).with_body_type(BodyType::Kinematic)); let config = CharacterConfig { move_speed: 3.0, w_move_speed: 2.0, // anisotropic, like config/default.toml @@ -235,12 +242,18 @@ mod tests { // Step to get grounded physics.step(0.016); - assert!(controller.is_grounded(&physics), "Should be grounded after step near floor"); + assert!( + controller.is_grounded(&physics), + "Should be grounded after step near floor" + ); // Jump should succeed let jumped = controller.jump(&mut physics); assert!(jumped, "Jump should succeed when grounded"); - assert!(!controller.is_grounded(&physics), "Should not be grounded after jump"); + assert!( + !controller.is_grounded(&physics), + "Should not be grounded after jump" + ); } #[test] @@ -251,7 +264,10 @@ mod tests { let controller = CharacterController4D::new(body_key, CharacterConfig::default()); - assert!(!controller.is_grounded(&physics), "Should not be grounded in air"); + assert!( + !controller.is_grounded(&physics), + "Should not be grounded in air" + ); let jumped = controller.jump(&mut physics); assert!(!jumped, "Jump should fail when airborne"); @@ -351,7 +367,11 @@ mod tests { let controller = CharacterController4D::new( body_key, - CharacterConfig { move_speed: 5.0, w_move_speed: 5.0, jump_velocity: 8.0 }, + CharacterConfig { + move_speed: 5.0, + w_move_speed: 5.0, + jump_velocity: 8.0, + }, ); // First apply some movement diff --git a/crates/rust4d_game/src/events.rs b/crates/rust4d_game/src/events.rs index 8a348b4..550494e 100644 --- a/crates/rust4d_game/src/events.rs +++ b/crates/rust4d_game/src/events.rs @@ -69,7 +69,10 @@ impl EventBus { handler(event); } }); - self.handlers.entry(type_id).or_default().push((id, wrapper)); + self.handlers + .entry(type_id) + .or_default() + .push((id, wrapper)); HandlerId(type_id, id) } @@ -250,7 +253,11 @@ mod tests { assert_eq!(bus.queued_count(), 0); bus.dispatch(); - assert_eq!(count.load(Ordering::Relaxed), 0, "No events should have been dispatched after clear"); + assert_eq!( + count.load(Ordering::Relaxed), + 0, + "No events should have been dispatched after clear" + ); } #[test] @@ -317,7 +324,11 @@ mod tests { // Events should not be delivered bus.emit(DamageEvent { amount: 10 }); bus.dispatch(); - assert_eq!(count.load(Ordering::Relaxed), 0, "Removed handler should not be called"); + assert_eq!( + count.load(Ordering::Relaxed), + 0, + "Removed handler should not be called" + ); } #[test] @@ -355,8 +366,16 @@ mod tests { bus.emit(DamageEvent { amount: 10 }); bus.dispatch(); - assert_eq!(count1.load(Ordering::Relaxed), 0, "Removed handler should not fire"); - assert_eq!(count2.load(Ordering::Relaxed), 1, "Remaining handler should fire"); + assert_eq!( + count1.load(Ordering::Relaxed), + 0, + "Removed handler should not fire" + ); + assert_eq!( + count2.load(Ordering::Relaxed), + 1, + "Remaining handler should fire" + ); } #[test] diff --git a/crates/rust4d_game/src/lib.rs b/crates/rust4d_game/src/lib.rs index 8f91149..20ba34a 100644 --- a/crates/rust4d_game/src/lib.rs +++ b/crates/rust4d_game/src/lib.rs @@ -13,7 +13,7 @@ pub mod fsm; pub mod scene_helpers; pub mod tween; -pub use character_controller::{CharacterController4D, CharacterConfig}; +pub use character_controller::{CharacterConfig, CharacterController4D}; pub use events::{EventBus, HandlerId}; pub use fsm::StateMachine; pub use tween::{EasingFunction, Tween, TweenId, TweenManager, TweenState}; diff --git a/crates/rust4d_game/src/scene_helpers.rs b/crates/rust4d_game/src/scene_helpers.rs index f514f7d..a6f077c 100644 --- a/crates/rust4d_game/src/scene_helpers.rs +++ b/crates/rust4d_game/src/scene_helpers.rs @@ -12,7 +12,9 @@ use rust4d_physics::{BodyKey, BodyType, PhysicsMaterial, PhysicsWorld, RigidBody /// /// Returns the player spawn position as a `Vec4`, or `None` if no spawn is defined. pub fn find_player_spawn(scene: &Scene) -> Option { - scene.player_spawn.map(|s| Vec4::new(s[0], s[1], s[2], s[3])) + scene + .player_spawn + .map(|s| Vec4::new(s[0], s[1], s[2], s[3])) } /// Create a player body in the physics world @@ -29,11 +31,7 @@ pub fn find_player_spawn(scene: &Scene) -> Option { /// /// # Returns /// The `BodyKey` for the newly created player body. -pub fn create_player_body( - physics: &mut PhysicsWorld, - position: Vec4, - radius: f32, -) -> BodyKey { +pub fn create_player_body(physics: &mut PhysicsWorld, position: Vec4, radius: f32) -> BodyKey { let body = RigidBody4D::new_sphere(position, radius) .with_body_type(BodyType::Kinematic) .with_gravity(true) // Kinematic but needs gravity for jumping/falling @@ -49,8 +47,7 @@ mod tests { #[test] fn test_find_player_spawn_some() { - let scene = Scene::new("Test") - .with_player_spawn(1.0, 2.0, 3.0, 4.0); + let scene = Scene::new("Test").with_player_spawn(1.0, 2.0, 3.0, 4.0); let spawn = find_player_spawn(&scene); assert!(spawn.is_some()); @@ -112,8 +109,14 @@ mod tests { physics.step(0.1); let body = physics.get_body(key).unwrap(); - assert!(body.position.y < 10.0, "Player body should fall with gravity"); - assert!(body.velocity.y < 0.0, "Player body should have downward velocity"); + assert!( + body.position.y < 10.0, + "Player body should fall with gravity" + ); + assert!( + body.velocity.y < 0.0, + "Player body should have downward velocity" + ); } #[test] @@ -129,8 +132,7 @@ mod tests { #[test] fn test_find_player_spawn_with_zeros() { - let scene = Scene::new("Test") - .with_player_spawn(0.0, 0.0, 0.0, 0.0); + let scene = Scene::new("Test").with_player_spawn(0.0, 0.0, 0.0, 0.0); let spawn = find_player_spawn(&scene); assert!(spawn.is_some()); @@ -141,22 +143,28 @@ mod tests { /// T5: create_player_body + CharacterController4D round-trip #[test] fn test_create_player_body_with_character_controller_round_trip() { - use crate::{CharacterController4D, CharacterConfig}; + use crate::{CharacterConfig, CharacterController4D}; use rust4d_physics::StaticCollider; let mut physics = PhysicsWorld::with_config(PhysicsConfig::new(-20.0)); - physics.add_static_collider(StaticCollider::floor(0.0, rust4d_physics::PhysicsMaterial::CONCRETE)); + physics.add_static_collider(StaticCollider::floor( + 0.0, + rust4d_physics::PhysicsMaterial::CONCRETE, + )); // Create player body via scene_helpers let spawn = Vec4::new(0.0, 1.0, 0.0, 0.0); let key = create_player_body(&mut physics, spawn, 0.5); // Wrap in CharacterController4D - let controller = CharacterController4D::new(key, CharacterConfig { - move_speed: 5.0, - w_move_speed: 5.0, - jump_velocity: 10.0, - }); + let controller = CharacterController4D::new( + key, + CharacterConfig { + move_speed: 5.0, + w_move_speed: 5.0, + jump_velocity: 10.0, + }, + ); // Verify initial position matches spawn let pos = controller.position(&physics).expect("Body should exist"); @@ -167,11 +175,14 @@ mod tests { physics.step(0.016); // Position should have changed - let pos_after = controller.position(&physics).expect("Body should still exist"); + let pos_after = controller + .position(&physics) + .expect("Body should still exist"); assert!( (pos_after.x - spawn.x).abs() > 0.01, "X position should change after movement. Before: {}, After: {}", - spawn.x, pos_after.x + spawn.x, + pos_after.x ); // Step physics until grounded diff --git a/crates/rust4d_game/src/tween.rs b/crates/rust4d_game/src/tween.rs index d7bbca9..f3e2512 100644 --- a/crates/rust4d_game/src/tween.rs +++ b/crates/rust4d_game/src/tween.rs @@ -114,7 +114,9 @@ impl EasingFunction { Some(Self::EaseInOutQuad) } "ease_in_cubic" | "easeincubic" | "in_cubic" | "incubic" => Some(Self::EaseInCubic), - "ease_out_cubic" | "easeoutcubic" | "out_cubic" | "outcubic" => Some(Self::EaseOutCubic), + "ease_out_cubic" | "easeoutcubic" | "out_cubic" | "outcubic" => { + Some(Self::EaseOutCubic) + } "ease_in_out_cubic" | "easeinoutcubic" | "in_out_cubic" | "inoutcubic" => { Some(Self::EaseInOutCubic) } @@ -824,13 +826,7 @@ mod tests { for _ in 0..5 { let entity = world.spawn((rust4d_core::Transform4D::identity(),)); - manager.tween_position( - entity, - Vec4::ZERO, - Vec4::X, - 1.0, - EasingFunction::Linear, - ); + manager.tween_position(entity, Vec4::ZERO, Vec4::X, 1.0, EasingFunction::Linear); } assert_eq!(manager.active_count(), 5); diff --git a/crates/rust4d_input/src/camera_controller.rs b/crates/rust4d_input/src/camera_controller.rs index d7a1d47..a18134c 100644 --- a/crates/rust4d_input/src/camera_controller.rs +++ b/crates/rust4d_input/src/camera_controller.rs @@ -21,15 +21,15 @@ pub struct CameraController { right: bool, up: bool, down: bool, - ana: bool, // Q - move toward +W (ana) - kata: bool, // E - move toward -W (kata) + ana: bool, // Q - move toward +W (ana) + kata: bool, // E - move toward -W (kata) // Jump state (for physics-based movement) jump_pressed: bool, // Mouse state mouse_pressed: bool, - w_rotation_mode: bool, // Right-click held + w_rotation_mode: bool, // Right-click held pending_yaw: f32, pending_pitch: f32, @@ -42,7 +42,7 @@ pub struct CameraController { pub w_move_speed: f32, pub mouse_sensitivity: f32, pub w_rotation_sensitivity: f32, - pub smoothing_half_life: f32, // Exponential smoothing half-life in seconds + pub smoothing_half_life: f32, // Exponential smoothing half-life in seconds pub smoothing_enabled: bool, } @@ -76,10 +76,10 @@ impl CameraController { move_speed: 3.0, w_move_speed: 2.0, - mouse_sensitivity: 0.002, // Standard FPS sensitivity + mouse_sensitivity: 0.002, // Standard FPS sensitivity w_rotation_sensitivity: 0.005, - smoothing_half_life: 0.05, // 50ms half-life when enabled - smoothing_enabled: false, // Disabled by default for responsive FPS feel + smoothing_half_life: 0.05, // 50ms half-life when enabled + smoothing_enabled: false, // Disabled by default for responsive FPS feel } } @@ -88,12 +88,30 @@ impl CameraController { let pressed = state == ElementState::Pressed; match key { - KeyCode::KeyW => { self.forward = pressed; true } - KeyCode::KeyS => { self.backward = pressed; true } - KeyCode::KeyA => { self.left = pressed; true } - KeyCode::KeyD => { self.right = pressed; true } - KeyCode::KeyQ => { self.ana = pressed; true } - KeyCode::KeyE => { self.kata = pressed; true } + KeyCode::KeyW => { + self.forward = pressed; + true + } + KeyCode::KeyS => { + self.backward = pressed; + true + } + KeyCode::KeyA => { + self.left = pressed; + true + } + KeyCode::KeyD => { + self.right = pressed; + true + } + KeyCode::KeyQ => { + self.ana = pressed; + true + } + KeyCode::KeyE => { + self.kata = pressed; + true + } KeyCode::Space => { self.up = pressed; // Also track jump for physics mode @@ -102,7 +120,10 @@ impl CameraController { } true } - KeyCode::ShiftLeft | KeyCode::ShiftRight => { self.down = pressed; true } + KeyCode::ShiftLeft | KeyCode::ShiftRight => { + self.down = pressed; + true + } _ => false, } } @@ -132,7 +153,12 @@ impl CameraController { /// /// When `cursor_captured` is true, free look is enabled (no click required). /// Returns the camera position for debug display. - pub fn update(&mut self, camera: &mut C, dt: f32, cursor_captured: bool) -> Vec4 { + pub fn update( + &mut self, + camera: &mut C, + dt: f32, + cursor_captured: bool, + ) -> Vec4 { // Calculate movement deltas let fwd = (self.forward as i32 - self.backward as i32) as f32; let rgt = (self.right as i32 - self.left as i32) as f32; @@ -149,8 +175,10 @@ impl CameraController { // Exponential smoothing: new = old * factor + input * (1 - factor) // factor = 2^(-dt / half_life), so smaller half_life = faster response let smooth_factor = 2.0f32.powf(-dt / self.smoothing_half_life); - self.smooth_yaw = self.smooth_yaw * smooth_factor + self.pending_yaw * (1.0 - smooth_factor); - self.smooth_pitch = self.smooth_pitch * smooth_factor + self.pending_pitch * (1.0 - smooth_factor); + self.smooth_yaw = + self.smooth_yaw * smooth_factor + self.pending_yaw * (1.0 - smooth_factor); + self.smooth_pitch = + self.smooth_pitch * smooth_factor + self.pending_pitch * (1.0 - smooth_factor); (self.smooth_yaw, self.smooth_pitch) } else { // No smoothing - use raw input @@ -187,8 +215,14 @@ impl CameraController { /// Check if any movement keys are pressed pub fn is_moving(&self) -> bool { - self.forward || self.backward || self.left || self.right - || self.up || self.down || self.ana || self.kata + self.forward + || self.backward + || self.left + || self.right + || self.up + || self.down + || self.ana + || self.kata } /// Toggle input smoothing on/off diff --git a/crates/rust4d_input/src/lib.rs b/crates/rust4d_input/src/lib.rs index 9aeb38a..d01fdd8 100644 --- a/crates/rust4d_input/src/lib.rs +++ b/crates/rust4d_input/src/lib.rs @@ -5,4 +5,4 @@ mod camera_controller; -pub use camera_controller::{CameraController, CameraControl}; +pub use camera_controller::{CameraControl, CameraController}; diff --git a/crates/rust4d_math/src/hyperplane.rs b/crates/rust4d_math/src/hyperplane.rs index 2ff6cab..204d6c3 100644 --- a/crates/rust4d_math/src/hyperplane.rs +++ b/crates/rust4d_math/src/hyperplane.rs @@ -11,7 +11,10 @@ //! We model it as a grid of "pillars" - each pillar is a rectangular prism //! extending in W, decomposed into tetrahedra. -use crate::{Vec4, shape::{ConvexShape4D, Tetrahedron}}; +use crate::{ + shape::{ConvexShape4D, Tetrahedron}, + Vec4, +}; use std::collections::HashSet; /// A hyperplane floor in local space - pure geometry without colors @@ -46,12 +49,7 @@ impl Hyperplane4D { /// * `grid_size` - Number of cells along each axis /// * `w_extent` - Half-extent in W dimension (for slicing visibility) /// * `thickness` - Y thickness (bottom at y=0, top at y=thickness) - pub fn new( - size: f32, - grid_size: usize, - w_extent: f32, - thickness: f32, - ) -> Self { + pub fn new(size: f32, grid_size: usize, w_extent: f32, thickness: f32) -> Self { let mut vertices = Vec::new(); let mut tetrahedra = Vec::new(); @@ -144,10 +142,30 @@ impl Hyperplane4D { /// Decompose a single cell (mini-tesseract) into tetrahedra using Kuhn triangulation fn decompose_cell_to_tetrahedra(base_idx: usize) -> Vec { let permutations = [ - [0, 1, 2, 3], [0, 1, 3, 2], [0, 2, 1, 3], [0, 2, 3, 1], [0, 3, 1, 2], [0, 3, 2, 1], - [1, 0, 2, 3], [1, 0, 3, 2], [1, 2, 0, 3], [1, 2, 3, 0], [1, 3, 0, 2], [1, 3, 2, 0], - [2, 0, 1, 3], [2, 0, 3, 1], [2, 1, 0, 3], [2, 1, 3, 0], [2, 3, 0, 1], [2, 3, 1, 0], - [3, 0, 1, 2], [3, 0, 2, 1], [3, 1, 0, 2], [3, 1, 2, 0], [3, 2, 0, 1], [3, 2, 1, 0], + [0, 1, 2, 3], + [0, 1, 3, 2], + [0, 2, 1, 3], + [0, 2, 3, 1], + [0, 3, 1, 2], + [0, 3, 2, 1], + [1, 0, 2, 3], + [1, 0, 3, 2], + [1, 2, 0, 3], + [1, 2, 3, 0], + [1, 3, 0, 2], + [1, 3, 2, 0], + [2, 0, 1, 3], + [2, 0, 3, 1], + [2, 1, 0, 3], + [2, 1, 3, 0], + [2, 3, 0, 1], + [2, 3, 1, 0], + [3, 0, 1, 2], + [3, 0, 2, 1], + [3, 1, 0, 2], + [3, 1, 2, 0], + [3, 2, 0, 1], + [3, 2, 1, 0], ]; let mut simplices = Vec::with_capacity(24); @@ -218,8 +236,11 @@ mod tests { // Check that all vertices are in local space: y=0 to y=thickness for v in plane.vertices() { - assert!(v.y >= 0.0 && v.y <= 0.1, - "Vertex Y should be between 0 and thickness, got {}", v.y); + assert!( + v.y >= 0.0 && v.y <= 0.1, + "Vertex Y should be between 0 and thickness, got {}", + v.y + ); } } diff --git a/crates/rust4d_math/src/interpolation.rs b/crates/rust4d_math/src/interpolation.rs index a85ca81..eeeac42 100644 --- a/crates/rust4d_math/src/interpolation.rs +++ b/crates/rust4d_math/src/interpolation.rs @@ -3,7 +3,7 @@ //! This module provides the [`Interpolatable`] trait for types that support //! linear interpolation (lerp), which is foundational for animation and tweening. -use crate::{Vec4, Rotor4}; +use crate::{Rotor4, Vec4}; /// Trait for types that can be linearly interpolated /// @@ -88,7 +88,9 @@ impl Interpolatable for Rotor4 { // If dot is negative, negate one rotor to take the shorter path let (b_s, b_xy, b_xz, b_xw, b_yz, b_yw, b_zw, b_p) = if dot < 0.0 { dot = -dot; - (-b.s, -b.b_xy, -b.b_xz, -b.b_xw, -b.b_yz, -b.b_yw, -b.b_zw, -b.p) + ( + -b.s, -b.b_xy, -b.b_xz, -b.b_xw, -b.b_yz, -b.b_yw, -b.b_zw, -b.p, + ) } else { (b.s, b.b_xy, b.b_xz, b.b_xw, b.b_yz, b.b_yw, b.b_zw, b.p) }; @@ -131,15 +133,16 @@ impl Interpolatable for Rotor4 { b_yw: scale_a * a.b_yw + scale_b * b_yw, b_zw: scale_a * a.b_zw + scale_b * b_zw, p: scale_a * a.p + scale_b * b_p, - }.normalize() + } + .normalize() } } #[cfg(test)] mod tests { use super::*; - use std::f32::consts::PI; use crate::RotationPlane; + use std::f32::consts::PI; const EPSILON: f32 = 0.001; @@ -203,7 +206,12 @@ mod tests { let v = Vec4::X; let expected = b.rotate(v); let actual = at_one.rotate(v); - assert!(vec_approx_eq(actual, expected), "expected {:?}, got {:?}", expected, actual); + assert!( + vec_approx_eq(actual, expected), + "expected {:?}, got {:?}", + expected, + actual + ); } #[test] @@ -219,8 +227,18 @@ mod tests { // 45 degrees in XY should put X at (cos45, sin45, 0, 0) let expected_x = (PI / 4.0).cos(); let expected_y = (PI / 4.0).sin(); - assert!(approx_eq(rotated.x, expected_x), "x: {} vs {}", rotated.x, expected_x); - assert!(approx_eq(rotated.y, expected_y), "y: {} vs {}", rotated.y, expected_y); + assert!( + approx_eq(rotated.x, expected_x), + "x: {} vs {}", + rotated.x, + expected_x + ); + assert!( + approx_eq(rotated.y, expected_y), + "y: {} vs {}", + rotated.y, + expected_y + ); } #[test] @@ -237,8 +255,18 @@ mod tests { // Should be at -45 degrees, not 135 degrees let expected_x = (PI / 4.0).cos(); let expected_y = -(PI / 4.0).sin(); - assert!(approx_eq(rotated.x, expected_x), "x: {} vs {}", rotated.x, expected_x); - assert!(approx_eq(rotated.y, expected_y), "y: {} vs {}", rotated.y, expected_y); + assert!( + approx_eq(rotated.x, expected_x), + "x: {} vs {}", + rotated.x, + expected_x + ); + assert!( + approx_eq(rotated.y, expected_y), + "y: {} vs {}", + rotated.y, + expected_y + ); } #[test] diff --git a/crates/rust4d_math/src/lib.rs b/crates/rust4d_math/src/lib.rs index ef518ae..76be56d 100644 --- a/crates/rust4d_math/src/lib.rs +++ b/crates/rust4d_math/src/lib.rs @@ -22,23 +22,23 @@ //! 16-cell, 24-cell, 600-cell) and the curved shapes (hypersphere, //! spherinder, cubinder, duocylinder) as watertight boundary meshes. -mod vec4; -mod rotor4; +pub mod hyperplane; +pub mod interpolation; pub mod mat4; pub mod mesh4d; pub mod primitives; pub mod ray; +mod rotor4; pub mod shape; pub mod tesseract; -pub mod hyperplane; -pub mod interpolation; +mod vec4; -pub use mesh4d::{Mesh4D, MeshError}; -pub use vec4::Vec4; -pub use rotor4::{Rotor4, RotationPlane}; +pub use hyperplane::Hyperplane4D; +pub use interpolation::Interpolatable; pub use mat4::Mat4; +pub use mesh4d::{Mesh4D, MeshError}; pub use ray::Ray4D; +pub use rotor4::{RotationPlane, Rotor4}; pub use shape::{ConvexShape4D, Tetrahedron}; pub use tesseract::Tesseract4D; -pub use hyperplane::Hyperplane4D; -pub use interpolation::Interpolatable; +pub use vec4::Vec4; diff --git a/crates/rust4d_math/src/mat4.rs b/crates/rust4d_math/src/mat4.rs index d60144a..f782a0c 100644 --- a/crates/rust4d_math/src/mat4.rs +++ b/crates/rust4d_math/src/mat4.rs @@ -241,7 +241,6 @@ pub fn scale(s: Vec4) -> Mat4 { /// 2. Remove mutual projections between columns #[allow(clippy::needless_range_loop)] // index pairs (i, j) into mt are clearest here pub fn ortho_iterate(mut m: Mat4) -> Mat4 { - // Normalize columns for i in 0..4 { let col = get_column(m, i); @@ -358,27 +357,36 @@ mod tests { // Y should go to Z let y = Vec4::new(0.0, 1.0, 0.0, 0.0); let result = transform(m, y); - assert!(vec_approx_eq(result, Vec4::new(0.0, 0.0, 1.0, 0.0)), - "Y should become Z, got {:?}", result); + assert!( + vec_approx_eq(result, Vec4::new(0.0, 0.0, 1.0, 0.0)), + "Y should become Z, got {:?}", + result + ); // Z should go to -Y let z = Vec4::new(0.0, 0.0, 1.0, 0.0); let result = transform(m, z); - assert!(vec_approx_eq(result, Vec4::new(0.0, -1.0, 0.0, 0.0)), - "Z should become -Y, got {:?}", result); + assert!( + vec_approx_eq(result, Vec4::new(0.0, -1.0, 0.0, 0.0)), + "Z should become -Y, got {:?}", + result + ); // X should be unchanged let x = Vec4::new(1.0, 0.0, 0.0, 0.0); let result = transform(m, x); - assert!(vec_approx_eq(result, x), - "X should be unchanged, got {:?}", result); + assert!( + vec_approx_eq(result, x), + "X should be unchanged, got {:?}", + result + ); } #[test] fn test_skip_y_preserves_y_axis() { - use std::f32::consts::FRAC_PI_4; - use crate::Rotor4; use crate::RotationPlane; + use crate::Rotor4; + use std::f32::consts::FRAC_PI_4; // Create a 3D rotation (using YZ plane which affects Y and Z) let r = Rotor4::from_plane_angle(RotationPlane::YZ, FRAC_PI_4); @@ -391,15 +399,18 @@ mod tests { let y = Vec4::new(0.0, 1.0, 0.0, 0.0); let result = transform(skip_m, y); - assert!(vec_approx_eq(result, y), - "Y axis should be preserved after skip_y, got {:?}", result); + assert!( + vec_approx_eq(result, y), + "Y axis should be preserved after skip_y, got {:?}", + result + ); } #[test] fn test_skip_y_remaps_rotation() { - use std::f32::consts::FRAC_PI_2; - use crate::Rotor4; use crate::RotationPlane; + use crate::Rotor4; + use std::f32::consts::FRAC_PI_2; // Create a 90° rotation in XY plane (affects X and Y) let r = Rotor4::from_plane_angle(RotationPlane::XY, FRAC_PI_2); @@ -408,29 +419,38 @@ mod tests { // Original: X→Y, Y→-X let x = Vec4::new(1.0, 0.0, 0.0, 0.0); let original_result = transform(m, x); - assert!(vec_approx_eq(original_result, Vec4::new(0.0, 1.0, 0.0, 0.0)), - "Original: X should become Y, got {:?}", original_result); + assert!( + vec_approx_eq(original_result, Vec4::new(0.0, 1.0, 0.0, 0.0)), + "Original: X should become Y, got {:?}", + original_result + ); // After SkipY: rotation is now in XZ plane (indices 0,2) let skip_m = skip_y(m); // X should now go to Z (not Y) let result = transform(skip_m, x); - assert!(vec_approx_eq(result, Vec4::new(0.0, 0.0, 1.0, 0.0)), - "After skip_y: X should become Z, got {:?}", result); + assert!( + vec_approx_eq(result, Vec4::new(0.0, 0.0, 1.0, 0.0)), + "After skip_y: X should become Z, got {:?}", + result + ); // Y should be unchanged let y = Vec4::new(0.0, 1.0, 0.0, 0.0); let result = transform(skip_m, y); - assert!(vec_approx_eq(result, y), - "After skip_y: Y should be unchanged, got {:?}", result); + assert!( + vec_approx_eq(result, y), + "After skip_y: Y should be unchanged, got {:?}", + result + ); } #[test] fn test_skip_y_xz_becomes_xw() { - use std::f32::consts::FRAC_PI_2; - use crate::Rotor4; use crate::RotationPlane; + use crate::Rotor4; + use std::f32::consts::FRAC_PI_2; // Create a 90° rotation in XZ plane let r = Rotor4::from_plane_angle(RotationPlane::XZ, FRAC_PI_2); @@ -443,8 +463,11 @@ mod tests { // X should go to W let x = Vec4::new(1.0, 0.0, 0.0, 0.0); let result = transform(skip_m, x); - assert!(vec_approx_eq(result, Vec4::new(0.0, 0.0, 0.0, 1.0)), - "After skip_y(XZ rotation): X should become W, got {:?}", result); + assert!( + vec_approx_eq(result, Vec4::new(0.0, 0.0, 0.0, 1.0)), + "After skip_y(XZ rotation): X should become W, got {:?}", + result + ); } #[test] @@ -471,8 +494,12 @@ mod tests { let result1 = transform(composed, v); let result2 = transform(r90, v); - assert!(vec_approx_eq(result1, result2), - "Composed: {:?}, Direct: {:?}", result1, result2); + assert!( + vec_approx_eq(result1, result2), + "Composed: {:?}, Direct: {:?}", + result1, + result2 + ); } #[test] @@ -480,8 +507,10 @@ mod tests { let m = plane_rotation(0.5, 1, 2); let col0 = get_column(m, 0); - assert!(vec_approx_eq(col0, Vec4::new(1.0, 0.0, 0.0, 0.0)), - "Column 0 should be X axis for YZ rotation"); + assert!( + vec_approx_eq(col0, Vec4::new(1.0, 0.0, 0.0, 0.0)), + "Column 0 should be X axis for YZ rotation" + ); } #[test] @@ -548,8 +577,10 @@ mod tests { let result = ortho_iterate(m); // Should still be approximately equal - assert!(mat_approx_eq(m, result), - "ortho_iterate should preserve orthogonal matrices"); + assert!( + mat_approx_eq(m, result), + "ortho_iterate should preserve orthogonal matrices" + ); } #[test] @@ -565,7 +596,11 @@ mod tests { let col0 = get_column(result, 0); let col1 = get_column(result, 1); let dot = col0.dot(col1); - assert!(dot.abs() < 0.01, "Columns should be orthogonal after ortho_iterate, dot = {}", dot); + assert!( + dot.abs() < 0.01, + "Columns should be orthogonal after ortho_iterate, dot = {}", + dot + ); // Column lengths should be near 1 assert!((col0.length() - 1.0).abs() < 0.01); @@ -580,8 +615,11 @@ mod tests { // Should rotate X to Y let result = transform(m, from); - assert!(vec_approx_eq(result, to), - "from_to_rotation(X, Y) * X should equal Y, got {:?}", result); + assert!( + vec_approx_eq(result, to), + "from_to_rotation(X, Y) * X should equal Y, got {:?}", + result + ); } #[test] @@ -593,7 +631,11 @@ mod tests { // Result should rotate `from` to `to` let result = transform(m, from); let dot = result.dot(to); - assert!(dot > 0.99, "from_to_rotation should rotate from to to, dot = {}", dot); + assert!( + dot > 0.99, + "from_to_rotation should rotate from to to, dot = {}", + dot + ); } #[test] @@ -603,7 +645,11 @@ mod tests { // Should be close to identity (no rotation needed) let result = transform(m, v); - assert!(vec_approx_eq(result, v), "Identity rotation failed: {:?}", result); + assert!( + vec_approx_eq(result, v), + "Identity rotation failed: {:?}", + result + ); } #[test] @@ -618,8 +664,13 @@ mod tests { for (from, to) in &cases { let m = from_to_rotation(*from, *to); let result = transform(m, *from); - assert!(vec_approx_eq(result, *to), - "180° rotation failed: from={:?} to={:?}, got {:?}", from, to, result); + assert!( + vec_approx_eq(result, *to), + "180° rotation failed: from={:?} to={:?}, got {:?}", + from, + to, + result + ); } } @@ -640,21 +691,37 @@ mod tests { let to = -from; let m = from_to_rotation(from, to); let result = transform(m, from); - assert!(vec_approx_eq(result, to), - "180° diagonal rotation failed: got {:?}, expected {:?}", result, to); + assert!( + vec_approx_eq(result, to), + "180° diagonal rotation failed: got {:?}, expected {:?}", + result, + to + ); } /// Compute determinant of a 4x4 matrix using cofactor expansion fn determinant(m: Mat4) -> f32 { - let a = m[0][0]; let b = m[1][0]; let c = m[2][0]; let d = m[3][0]; - let e = m[0][1]; let f = m[1][1]; let g = m[2][1]; let h = m[3][1]; - let i = m[0][2]; let j = m[1][2]; let k = m[2][2]; let l = m[3][2]; - let mm = m[0][3]; let n = m[1][3]; let o = m[2][3]; let p = m[3][3]; - - a * (f*(k*p - l*o) - g*(j*p - l*n) + h*(j*o - k*n)) - -b * (e*(k*p - l*o) - g*(i*p - l*mm) + h*(i*o - k*mm)) - +c * (e*(j*p - l*n) - f*(i*p - l*mm) + h*(i*n - j*mm)) - -d * (e*(j*o - k*n) - f*(i*o - k*mm) + g*(i*n - j*mm)) + let a = m[0][0]; + let b = m[1][0]; + let c = m[2][0]; + let d = m[3][0]; + let e = m[0][1]; + let f = m[1][1]; + let g = m[2][1]; + let h = m[3][1]; + let i = m[0][2]; + let j = m[1][2]; + let k = m[2][2]; + let l = m[3][2]; + let mm = m[0][3]; + let n = m[1][3]; + let o = m[2][3]; + let p = m[3][3]; + + a * (f * (k * p - l * o) - g * (j * p - l * n) + h * (j * o - k * n)) + - b * (e * (k * p - l * o) - g * (i * p - l * mm) + h * (i * o - k * mm)) + + c * (e * (j * p - l * n) - f * (i * p - l * mm) + h * (i * n - j * mm)) + - d * (e * (j * o - k * n) - f * (i * o - k * mm) + g * (i * n - j * mm)) } #[test] @@ -662,16 +729,23 @@ mod tests { // T1 from review: verify det = +1 (proper rotation, not reflection) let cases: Vec<(Vec4, Vec4)> = vec![ (Vec4::X, Vec4::Y), - (Vec4::X, -Vec4::X), // anti-parallel + (Vec4::X, -Vec4::X), // anti-parallel (Vec4::Y, -Vec4::Y), - (Vec4::new(1.0, 1.0, 1.0, 1.0).normalized(), - Vec4::new(-1.0, -1.0, -1.0, -1.0).normalized()), + ( + Vec4::new(1.0, 1.0, 1.0, 1.0).normalized(), + Vec4::new(-1.0, -1.0, -1.0, -1.0).normalized(), + ), ]; for (from, to) in &cases { let m = from_to_rotation(*from, *to); let det = determinant(m); - assert!((det - 1.0).abs() < 0.01, - "from_to_rotation({:?}, {:?}) should have det=1, got {}", from, to, det); + assert!( + (det - 1.0).abs() < 0.01, + "from_to_rotation({:?}, {:?}) should have det=1, got {}", + from, + to, + det + ); } } @@ -682,7 +756,9 @@ mod tests { set_column(&mut m, 2, Vec4::new(0.0, 0.0, 1e-12, 0.0)); // Near-zero column let result = ortho_iterate(m); // Should return identity rather than a partially normalized matrix - assert!(mat_approx_eq(result, IDENTITY), - "Degenerate ortho_iterate should return identity"); + assert!( + mat_approx_eq(result, IDENTITY), + "Degenerate ortho_iterate should return identity" + ); } } diff --git a/crates/rust4d_math/src/mesh4d.rs b/crates/rust4d_math/src/mesh4d.rs index 64cd56e..decd8ba 100644 --- a/crates/rust4d_math/src/mesh4d.rs +++ b/crates/rust4d_math/src/mesh4d.rs @@ -26,7 +26,7 @@ //! Gram determinants (correct for any embedding in 4D); used heavily by //! the primitive test suites to pin constructions against closed forms -use crate::{ConvexShape4D, Mat4, Tetrahedron, Vec4, mat4}; +use crate::{mat4, ConvexShape4D, Mat4, Tetrahedron, Vec4}; /// A general 4D tetrahedral mesh: vertices + tetrahedral cells. /// @@ -59,7 +59,10 @@ impl std::fmt::Display for MeshError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MeshError::IndexOutOfBounds { tet, index } => { - write!(f, "tetrahedron {tet} references out-of-bounds vertex {index}") + write!( + f, + "tetrahedron {tet} references out-of-bounds vertex {index}" + ) } MeshError::DegenerateCell { tet } => { write!(f, "tetrahedron {tet} has repeated vertex indices") @@ -81,7 +84,10 @@ impl Mesh4D { /// Use [`Mesh4D::validate`] afterwards if the data comes from an /// untrusted source (e.g. a file). pub fn from_parts(vertices: Vec, tetrahedra: Vec) -> Self { - Self { vertices, tetrahedra } + Self { + vertices, + tetrahedra, + } } /// Create an empty mesh with pre-allocated capacity. @@ -240,18 +246,25 @@ impl Mesh4D { let mut min = first; let mut max = first; for v in &self.vertices[1..] { - min = Vec4::new(min.x.min(v.x), min.y.min(v.y), min.z.min(v.z), min.w.min(v.w)); - max = Vec4::new(max.x.max(v.x), max.y.max(v.y), max.z.max(v.z), max.w.max(v.w)); + min = Vec4::new( + min.x.min(v.x), + min.y.min(v.y), + min.z.min(v.z), + min.w.min(v.w), + ); + max = Vec4::new( + max.x.max(v.x), + max.y.max(v.y), + max.z.max(v.z), + max.w.max(v.w), + ); } Some((min, max)) } /// Radius of the smallest origin-centered ball containing all vertices. pub fn bounding_radius(&self) -> f32 { - self.vertices - .iter() - .map(|v| v.length()) - .fold(0.0, f32::max) + self.vertices.iter().map(|v| v.length()).fold(0.0, f32::max) } /// 3-volume of a single cell. @@ -427,10 +440,7 @@ mod tests { #[test] fn test_validate_catches_out_of_bounds() { - let m = Mesh4D::from_parts( - vec![Vec4::ZERO], - vec![Tetrahedron::new([0, 0, 0, 7])], - ); + let m = Mesh4D::from_parts(vec![Vec4::ZERO], vec![Tetrahedron::new([0, 0, 0, 7])]); assert!(matches!( m.validate(), Err(MeshError::IndexOutOfBounds { index: 7, .. }) @@ -439,11 +449,11 @@ mod tests { #[test] fn test_validate_catches_degenerate() { - let m = Mesh4D::from_parts( - vec![Vec4::ZERO; 4], - vec![Tetrahedron::new([0, 1, 2, 2])], - ); - assert!(matches!(m.validate(), Err(MeshError::DegenerateCell { tet: 0 }))); + let m = Mesh4D::from_parts(vec![Vec4::ZERO; 4], vec![Tetrahedron::new([0, 1, 2, 2])]); + assert!(matches!( + m.validate(), + Err(MeshError::DegenerateCell { tet: 0 }) + )); } #[test] diff --git a/crates/rust4d_math/src/primitives/curved.rs b/crates/rust4d_math/src/primitives/curved.rs index 2a9e550..b8f8696 100644 --- a/crates/rust4d_math/src/primitives/curved.rs +++ b/crates/rust4d_math/src/primitives/curved.rs @@ -43,10 +43,8 @@ pub fn hypersphere(radius: f32, subdivisions: u32) -> Mesh4D { let mut mesh = super::polytopes::hexadecachoron(radius); for _ in 0..subdivisions { - let mut refined = Mesh4D::with_capacity( - mesh.vertex_count() * 4, - mesh.tetrahedron_count() * 8, - ); + let mut refined = + Mesh4D::with_capacity(mesh.vertex_count() * 4, mesh.tetrahedron_count() * 8); for tet in mesh.tetrahedra() { // Gather the 10 points of the subdivision (4 corners + 6 edge // midpoints reprojected to the sphere). Midpoints are computed @@ -150,9 +148,20 @@ pub fn cubinder(radius: f32, half_size: f32, segments: u32) -> Mesh4D { (radius * theta.cos(), radius * theta.sin()) }; let vert = |pool: &mut VertexPool, ring: usize, corner: usize| -> usize { - let key = (if ring == CENTER { u32::MAX } else { (ring % segments) as u32 }, corner as u32); + let key = ( + if ring == CENTER { + u32::MAX + } else { + (ring % segments) as u32 + }, + corner as u32, + ); let (z, w) = square[corner]; - let (x, y) = if ring == CENTER { (0.0, 0.0) } else { circle_point(ring) }; + let (x, y) = if ring == CENTER { + (0.0, 0.0) + } else { + circle_point(ring) + }; pool.get(key, Vec4::new(x, y, z, w)) }; @@ -164,7 +173,10 @@ pub fn cubinder(radius: f32, half_size: f32, segments: u32) -> Mesh4D { for tri in [[0usize, 1, 2], [0, 2, 3]] { let bottom = tri.map(|c| vert(&mut pool, i, c)); let top = tri.map(|c| vert(&mut pool, i + 1, c)); - split_prism([bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], &mut tets); + split_prism( + [bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], + &mut tets, + ); } } @@ -183,7 +195,10 @@ pub fn cubinder(radius: f32, half_size: f32, segments: u32) -> Mesh4D { vert(&mut pool, i, c1), vert(&mut pool, i + 1, c1), ]; - split_prism([bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], &mut tets); + split_prism( + [bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], + &mut tets, + ); } } @@ -212,23 +227,24 @@ pub fn duocylinder(r1: f32, r2: f32, segments1: u32, segments2: u32) -> Mesh4D { // j on circle 2 (or AXIS2). The Clifford torus is the (i, j) grid; // each solid-torus piece fans toward its own axis circle. const AXIS: u32 = u32::MAX; - let vert = |pool: &mut VertexPool, i: usize, j: usize, on_axis_1: bool, on_axis_2: bool| -> usize { - let ki = if on_axis_1 { AXIS } else { (i % n1) as u32 }; - let kj = if on_axis_2 { AXIS } else { (j % n2) as u32 }; - let (x, y) = if on_axis_1 { - (0.0, 0.0) - } else { - let a = (i % n1) as f32 / n1 as f32 * std::f32::consts::TAU; - (r1 * a.cos(), r1 * a.sin()) - }; - let (z, w) = if on_axis_2 { - (0.0, 0.0) - } else { - let b = (j % n2) as f32 / n2 as f32 * std::f32::consts::TAU; - (r2 * b.cos(), r2 * b.sin()) + let vert = + |pool: &mut VertexPool, i: usize, j: usize, on_axis_1: bool, on_axis_2: bool| -> usize { + let ki = if on_axis_1 { AXIS } else { (i % n1) as u32 }; + let kj = if on_axis_2 { AXIS } else { (j % n2) as u32 }; + let (x, y) = if on_axis_1 { + (0.0, 0.0) + } else { + let a = (i % n1) as f32 / n1 as f32 * std::f32::consts::TAU; + (r1 * a.cos(), r1 * a.sin()) + }; + let (z, w) = if on_axis_2 { + (0.0, 0.0) + } else { + let b = (j % n2) as f32 / n2 as f32 * std::f32::consts::TAU; + (r2 * b.cos(), r2 * b.sin()) + }; + pool.get((ki, kj), Vec4::new(x, y, z, w)) }; - pool.get((ki, kj), Vec4::new(x, y, z, w)) - }; let mut tets: Vec = Vec::new(); @@ -246,7 +262,10 @@ pub fn duocylinder(r1: f32, r2: f32, segments1: u32, segments2: u32) -> Mesh4D { vert(&mut pool, i + 1, j, false, false), vert(&mut pool, i + 1, j + 1, false, false), ]; - split_prism([bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], &mut tets); + split_prism( + [bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], + &mut tets, + ); } } @@ -263,7 +282,10 @@ pub fn duocylinder(r1: f32, r2: f32, segments1: u32, segments2: u32) -> Mesh4D { vert(&mut pool, i, j + 1, false, false), vert(&mut pool, i + 1, j + 1, false, false), ]; - split_prism([bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], &mut tets); + split_prism( + [bottom[0], bottom[1], bottom[2], top[0], top[1], top[2]], + &mut tets, + ); } } @@ -288,7 +310,10 @@ struct VertexPool { impl VertexPool { fn new() -> Self { - Self { vertices: Vec::new(), index: HashMap::new() } + Self { + vertices: Vec::new(), + index: HashMap::new(), + } } fn get(&mut self, key: (u32, u32), position: Vec4) -> usize { @@ -305,20 +330,49 @@ impl VertexPool { fn icosphere(radius: f32, subdivisions: u32) -> (Vec<[f32; 3]>, Vec<[usize; 3]>) { let phi = (1.0 + 5.0f32.sqrt()) / 2.0; let mut verts: Vec<[f32; 3]> = vec![ - [-1.0, phi, 0.0], [1.0, phi, 0.0], [-1.0, -phi, 0.0], [1.0, -phi, 0.0], - [0.0, -1.0, phi], [0.0, 1.0, phi], [0.0, -1.0, -phi], [0.0, 1.0, -phi], - [phi, 0.0, -1.0], [phi, 0.0, 1.0], [-phi, 0.0, -1.0], [-phi, 0.0, 1.0], + [-1.0, phi, 0.0], + [1.0, phi, 0.0], + [-1.0, -phi, 0.0], + [1.0, -phi, 0.0], + [0.0, -1.0, phi], + [0.0, 1.0, phi], + [0.0, -1.0, -phi], + [0.0, 1.0, -phi], + [phi, 0.0, -1.0], + [phi, 0.0, 1.0], + [-phi, 0.0, -1.0], + [-phi, 0.0, 1.0], ]; let mut tris: Vec<[usize; 3]> = vec![ - [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], - [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], - [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], - [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], + [0, 11, 5], + [0, 5, 1], + [0, 1, 7], + [0, 7, 10], + [0, 10, 11], + [1, 5, 9], + [5, 11, 4], + [11, 10, 2], + [10, 7, 6], + [7, 1, 8], + [3, 9, 4], + [3, 4, 2], + [3, 2, 6], + [3, 6, 8], + [3, 8, 9], + [4, 9, 5], + [2, 4, 11], + [6, 2, 10], + [8, 6, 7], + [9, 8, 1], ]; let project = |v: [f32; 3]| -> [f32; 3] { let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt(); - [v[0] / len * radius, v[1] / len * radius, v[2] / len * radius] + [ + v[0] / len * radius, + v[1] / len * radius, + v[2] / len * radius, + ] }; for v in &mut verts { *v = project(*v); @@ -380,12 +434,21 @@ mod tests { let v1 = hypersphere(1.0, 1).surface_volume() as f64; let v2 = hypersphere(1.0, 2).surface_volume() as f64; let v3 = hypersphere(1.0, 3).surface_volume() as f64; - assert!(v1 < v2 && v2 < v3 && v3 < exact, "monotone from below: {v1} {v2} {v3} {exact}"); - assert!((exact - v3) / exact < 0.05, "d=3 within 5% of 2π²: {v3} vs {exact}"); + assert!( + v1 < v2 && v2 < v3 && v3 < exact, + "monotone from below: {v1} {v2} {v3} {exact}" + ); + assert!( + (exact - v3) / exact < 0.05, + "d=3 within 5% of 2π²: {v3} vs {exact}" + ); // Each subdivision halves edge length; an O(h²) scheme must shrink // the error by ~4× per level. Pin convergence *order*, not just size. let ratio = (exact - v2) / (exact - v3); - assert!(ratio > 3.0, "expected quadratic convergence, error ratio {ratio}"); + assert!( + ratio > 3.0, + "expected quadratic convergence, error ratio {ratio}" + ); } #[test] diff --git a/crates/rust4d_math/src/primitives/extrude.rs b/crates/rust4d_math/src/primitives/extrude.rs index 02fe1c3..58bf836 100644 --- a/crates/rust4d_math/src/primitives/extrude.rs +++ b/crates/rust4d_math/src/primitives/extrude.rs @@ -85,10 +85,10 @@ pub fn split_prism(prism: [usize; 6], out: &mut Vec) { /// refine the 16-cell boundary toward S³. pub const TET_SUBDIVISION: [[usize; 4]; 8] = [ // Corner cells - [0, 4, 5, 6], // v0, m01, m02, m03 - [1, 4, 7, 8], // v1, m01, m12, m13 - [2, 5, 7, 9], // v2, m02, m12, m23 - [3, 6, 8, 9], // v3, m03, m13, m23 + [0, 4, 5, 6], // v0, m01, m02, m03 + [1, 4, 7, 8], // v1, m01, m12, m13 + [2, 5, 7, 9], // v2, m02, m12, m23 + [3, 6, 8, 9], // v3, m03, m13, m23 // Central octahedron around diagonal m01(4) – m23(9). // Equator cycle: m02(5) – m03(6) – m13(8) – m12(7). [4, 9, 5, 6], @@ -176,7 +176,10 @@ mod tests { // (3,4,5) plane contains 2 triangles (the quad rule splits it the // same way from both sides) → internal pairings exist. let (paired, _unpaired) = m.face_pairing(); - assert!(paired >= 2, "stacked prisms must share their interface faces"); + assert!( + paired >= 2, + "stacked prisms must share their interface faces" + ); } #[test] diff --git a/crates/rust4d_math/src/primitives/polytopes.rs b/crates/rust4d_math/src/primitives/polytopes.rs index 0e41c25..2ae29b4 100644 --- a/crates/rust4d_math/src/primitives/polytopes.rs +++ b/crates/rust4d_math/src/primitives/polytopes.rs @@ -165,16 +165,15 @@ pub fn icositetrachoron(circumradius: f32) -> Mesh4D { ); // Cell centroid; opposite vertex pairs satisfy a + b = 2c. - let centroid = cell - .iter() - .fold(Vec4::ZERO, |acc, &i| acc + vertices[i]) - * (1.0 / 6.0); + let centroid = cell.iter().fold(Vec4::ZERO, |acc, &i| acc + vertices[i]) * (1.0 / 6.0); let antipode = |a: usize| -> usize { *cell .iter() .find(|&&b| { - b != a && (vertices[a] + vertices[b] - centroid * 2.0).length() < 1e-4 * circumradius.max(1.0) + b != a + && (vertices[a] + vertices[b] - centroid * 2.0).length() + < 1e-4 * circumradius.max(1.0) }) .expect("octahedral cell vertex must have an antipode") }; @@ -182,7 +181,11 @@ pub fn icositetrachoron(circumradius: f32) -> Mesh4D { // Axis pair (a, a'), equator cycle e0 → e1 → e0' → e1'. let a = cell[0]; let a2 = antipode(a); - let equator: Vec = cell.iter().copied().filter(|&v| v != a && v != a2).collect(); + let equator: Vec = cell + .iter() + .copied() + .filter(|&v| v != a && v != a2) + .collect(); let e0 = equator[0]; let e0p = antipode(e0); let e1 = *equator.iter().find(|&&v| v != e0 && v != e0p).unwrap(); @@ -237,7 +240,11 @@ pub fn hexacosichoron(circumradius: f32) -> Mesh4D { for (dst, &src) in perm.iter().enumerate() { let mag = base[src]; if mag != 0.0 { - let s = if (bits >> sign_slot) & 1 == 0 { 1.0 } else { -1.0 }; + let s = if (bits >> sign_slot) & 1 == 0 { + 1.0 + } else { + -1.0 + }; v[dst] = s * mag; sign_slot += 1; } else { @@ -253,9 +260,8 @@ pub fn hexacosichoron(circumradius: f32) -> Mesh4D { let edge = 1.0 / PHI; let edge2_lo = (edge * edge) * 0.999; let edge2_hi = (edge * edge) * 1.001; - let dist2 = |a: &[f64; 4], b: &[f64; 4]| -> f64 { - (0..4).map(|k| (a[k] - b[k]) * (a[k] - b[k])).sum() - }; + let dist2 = + |a: &[f64; 4], b: &[f64; 4]| -> f64 { (0..4).map(|k| (a[k] - b[k]) * (a[k] - b[k])).sum() }; let n = raw.len(); let mut neighbors: Vec> = vec![Vec::with_capacity(12); n]; @@ -418,8 +424,16 @@ mod tests { fn test_hexacosichoron_structure() { let m = hexacosichoron(1.0); m.validate().unwrap(); - assert_eq!(m.vertex_count(), 120, "binary icosahedral group has order 120"); - assert_eq!(m.tetrahedron_count(), 600, "the 600-cell must have 600 cells"); + assert_eq!( + m.vertex_count(), + 120, + "binary icosahedral group has order 120" + ); + assert_eq!( + m.tetrahedron_count(), + 600, + "the 600-cell must have 600 cells" + ); assert!(m.is_watertight(), "600-cell boundary must be watertight"); for v in m.vertices() { assert!((v.length() - 1.0).abs() < 1e-5); @@ -449,17 +463,28 @@ mod tests { incident[i] += 1; } } - assert!(incident.iter().all(|&c| c == 20), "each vertex must touch 20 cells"); + assert!( + incident.iter().all(|&c| c == 20), + "each vertex must touch 20 cells" + ); } #[test] fn test_polytopes_scale_with_circumradius() { - for ctor in [pentachoron, hexadecachoron, icositetrachoron, hexacosichoron] { + for ctor in [ + pentachoron, + hexadecachoron, + icositetrachoron, + hexacosichoron, + ] { let small = ctor(1.0); let big = ctor(2.0); // Boundary 3-volume scales with r³ let ratio = big.surface_volume() / small.surface_volume(); - assert!((ratio - 8.0).abs() < 1e-2, "volume must scale as r³, got {ratio}"); + assert!( + (ratio - 8.0).abs() < 1e-2, + "volume must scale as r³, got {ratio}" + ); } } } diff --git a/crates/rust4d_math/src/rotor4.rs b/crates/rust4d_math/src/rotor4.rs index 6220976..f4857c6 100644 --- a/crates/rust4d_math/src/rotor4.rs +++ b/crates/rust4d_math/src/rotor4.rs @@ -8,9 +8,9 @@ //! - 6 bivectors (one for each plane) //! - 1 pseudoscalar (4-vector) -use bytemuck::{Pod, Zeroable}; -use serde::{Serialize, Deserialize}; use crate::Vec4; +use bytemuck::{Pod, Zeroable}; +use serde::{Deserialize, Serialize}; /// The 6 rotation planes in 4D space #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -141,8 +141,8 @@ impl Rotor4 { let b_zw = a.z * b.w - a.w * b.z; // Magnitude of the bivector - let mag_sq = b_xy * b_xy + b_xz * b_xz + b_xw * b_xw - + b_yz * b_yz + b_yw * b_yw + b_zw * b_zw; + let mag_sq = + b_xy * b_xy + b_xz * b_xz + b_xw * b_xw + b_yz * b_yz + b_yw * b_yw + b_zw * b_zw; if mag_sq < 1e-10 { // Vectors are parallel, no rotation plane @@ -276,25 +276,25 @@ impl Rotor4 { let new_x = rv_e1 * s + rv_e2 * b12 + rv_e3 * b13 + rv_e4 * b14 // from e_i * e_1i + rv_e123 * b23 + rv_e124 * b24 + rv_e134 * b34 // from e_1jk * e_jk - - rv_e234 * p; // from e_234 * e_1234 = -e_1 + - rv_e234 * p; // from e_234 * e_1234 = -e_1 // e2 coefficient: let new_y = rv_e2 * s - rv_e1 * b12 + rv_e3 * b23 + rv_e4 * b24 // from e_i * e_2i - rv_e123 * b13 - rv_e124 * b14 + rv_e234 * b34 // from e_2jk * e_jk - + rv_e134 * p; // from e_134 * e_1234 = e_2 + + rv_e134 * p; // from e_134 * e_1234 = e_2 // e3 coefficient: let new_z = rv_e3 * s - rv_e1 * b13 - rv_e2 * b23 + rv_e4 * b34 // from e_i * e_3i + rv_e123 * b12 - rv_e134 * b14 - rv_e234 * b24 // from e_3jk * e_jk - - rv_e124 * p; // from e_124 * e_1234 = -e_3 + - rv_e124 * p; // from e_124 * e_1234 = -e_3 // e4 coefficient: let new_w = rv_e4 * s - rv_e1 * b14 - rv_e2 * b24 - rv_e3 * b34 // from e_i * e_4i + rv_e124 * b12 + rv_e134 * b13 + rv_e234 * b23 // from e_4jk * e_jk - + rv_e123 * p; // from e_123 * e_1234 = e_4 + + rv_e123 * p; // from e_123 * e_1234 = e_4 Vec4::new(new_x, new_y, new_z, new_w) } @@ -319,46 +319,53 @@ impl Rotor4 { + a.p * b.p; // XY bivector - let b_xy = a.s * b.b_xy + a.b_xy * b.s - - a.b_xz * b.b_yz + a.b_yz * b.b_xz - - a.b_xw * b.b_yw + a.b_yw * b.b_xw - - a.b_zw * b.p - a.p * b.b_zw; + let b_xy = a.s * b.b_xy + a.b_xy * b.s - a.b_xz * b.b_yz + a.b_yz * b.b_xz + - a.b_xw * b.b_yw + + a.b_yw * b.b_xw + - a.b_zw * b.p + - a.p * b.b_zw; // XZ bivector - let b_xz = a.s * b.b_xz + a.b_xz * b.s - + a.b_xy * b.b_yz - a.b_yz * b.b_xy - - a.b_xw * b.b_zw + a.b_zw * b.b_xw - + a.b_yw * b.p + a.p * b.b_yw; + let b_xz = + a.s * b.b_xz + a.b_xz * b.s + a.b_xy * b.b_yz - a.b_yz * b.b_xy - a.b_xw * b.b_zw + + a.b_zw * b.b_xw + + a.b_yw * b.p + + a.p * b.b_yw; // XW bivector - let b_xw = a.s * b.b_xw + a.b_xw * b.s - + a.b_xy * b.b_yw - a.b_yw * b.b_xy - + a.b_xz * b.b_zw - a.b_zw * b.b_xz - - a.b_yz * b.p - a.p * b.b_yz; + let b_xw = a.s * b.b_xw + a.b_xw * b.s + a.b_xy * b.b_yw - a.b_yw * b.b_xy + + a.b_xz * b.b_zw + - a.b_zw * b.b_xz + - a.b_yz * b.p + - a.p * b.b_yz; // YZ bivector - let b_yz = a.s * b.b_yz + a.b_yz * b.s - - a.b_xy * b.b_xz + a.b_xz * b.b_xy - - a.b_yw * b.b_zw + a.b_zw * b.b_yw - - a.b_xw * b.p - a.p * b.b_xw; + let b_yz = a.s * b.b_yz + a.b_yz * b.s - a.b_xy * b.b_xz + a.b_xz * b.b_xy + - a.b_yw * b.b_zw + + a.b_zw * b.b_yw + - a.b_xw * b.p + - a.p * b.b_xw; // YW bivector - let b_yw = a.s * b.b_yw + a.b_yw * b.s - - a.b_xy * b.b_xw + a.b_xw * b.b_xy - + a.b_yz * b.b_zw - a.b_zw * b.b_yz - + a.b_xz * b.p + a.p * b.b_xz; + let b_yw = + a.s * b.b_yw + a.b_yw * b.s - a.b_xy * b.b_xw + a.b_xw * b.b_xy + a.b_yz * b.b_zw + - a.b_zw * b.b_yz + + a.b_xz * b.p + + a.p * b.b_xz; // ZW bivector - let b_zw = a.s * b.b_zw + a.b_zw * b.s - - a.b_xz * b.b_xw + a.b_xw * b.b_xz - - a.b_yz * b.b_yw + a.b_yw * b.b_yz - - a.b_xy * b.p - a.p * b.b_xy; + let b_zw = a.s * b.b_zw + a.b_zw * b.s - a.b_xz * b.b_xw + a.b_xw * b.b_xz + - a.b_yz * b.b_yw + + a.b_yw * b.b_yz + - a.b_xy * b.p + - a.p * b.b_xy; // Pseudoscalar - let p = a.s * b.p + a.p * b.s - + a.b_xy * b.b_zw + a.b_zw * b.b_xy - - a.b_xz * b.b_yw - a.b_yw * b.b_xz - + a.b_xw * b.b_yz + a.b_yz * b.b_xw; + let p = a.s * b.p + a.p * b.s + a.b_xy * b.b_zw + a.b_zw * b.b_xy + - a.b_xz * b.b_yw + - a.b_yw * b.b_xz + + a.b_xw * b.b_yz + + a.b_yz * b.b_xw; Self { s, @@ -421,12 +428,20 @@ mod tests { // Rotating X by 90° in XY plane should give Y let v = Vec4::X; let rotated = r.rotate(v); - assert!(vec_approx_eq(rotated, Vec4::Y), "Expected Y, got {:?}", rotated); + assert!( + vec_approx_eq(rotated, Vec4::Y), + "Expected Y, got {:?}", + rotated + ); // Rotating Y by 90° in XY plane should give -X let v = Vec4::Y; let rotated = r.rotate(v); - assert!(vec_approx_eq(rotated, -Vec4::X), "Expected -X, got {:?}", rotated); + assert!( + vec_approx_eq(rotated, -Vec4::X), + "Expected -X, got {:?}", + rotated + ); } #[test] @@ -436,7 +451,11 @@ mod tests { // Rotating X by 90° in XZ plane should give Z let v = Vec4::X; let rotated = r.rotate(v); - assert!(vec_approx_eq(rotated, Vec4::Z), "Expected Z, got {:?}", rotated); + assert!( + vec_approx_eq(rotated, Vec4::Z), + "Expected Z, got {:?}", + rotated + ); } #[test] @@ -446,7 +465,11 @@ mod tests { // Rotating Z by 90° in ZW plane should give W let v = Vec4::Z; let rotated = r.rotate(v); - assert!(vec_approx_eq(rotated, Vec4::W), "Expected W, got {:?}", rotated); + assert!( + vec_approx_eq(rotated, Vec4::W), + "Expected W, got {:?}", + rotated + ); } #[test] @@ -474,7 +497,11 @@ mod tests { let composed = r.compose(&r_inv); // Should be close to identity - assert!(approx_eq(composed.normalize().s, 1.0), "Expected identity, got {:?}", composed); + assert!( + approx_eq(composed.normalize().s, 1.0), + "Expected identity, got {:?}", + composed + ); } #[test] @@ -485,7 +512,11 @@ mod tests { let v = Vec4::new(1.0, 2.0, 3.0, 4.0); let rotated = composed.normalize().rotate(v); - assert!(vec_approx_eq(v, rotated), "Expected original, got {:?}", rotated); + assert!( + vec_approx_eq(v, rotated), + "Expected original, got {:?}", + rotated + ); } #[test] @@ -505,10 +536,30 @@ mod tests { let m = r.to_matrix(); // Should be identity matrix - assert!(approx_eq(m[0][0], 1.0) && approx_eq(m[0][1], 0.0) && approx_eq(m[0][2], 0.0) && approx_eq(m[0][3], 0.0)); - assert!(approx_eq(m[1][0], 0.0) && approx_eq(m[1][1], 1.0) && approx_eq(m[1][2], 0.0) && approx_eq(m[1][3], 0.0)); - assert!(approx_eq(m[2][0], 0.0) && approx_eq(m[2][1], 0.0) && approx_eq(m[2][2], 1.0) && approx_eq(m[2][3], 0.0)); - assert!(approx_eq(m[3][0], 0.0) && approx_eq(m[3][1], 0.0) && approx_eq(m[3][2], 0.0) && approx_eq(m[3][3], 1.0)); + assert!( + approx_eq(m[0][0], 1.0) + && approx_eq(m[0][1], 0.0) + && approx_eq(m[0][2], 0.0) + && approx_eq(m[0][3], 0.0) + ); + assert!( + approx_eq(m[1][0], 0.0) + && approx_eq(m[1][1], 1.0) + && approx_eq(m[1][2], 0.0) + && approx_eq(m[1][3], 0.0) + ); + assert!( + approx_eq(m[2][0], 0.0) + && approx_eq(m[2][1], 0.0) + && approx_eq(m[2][2], 1.0) + && approx_eq(m[2][3], 0.0) + ); + assert!( + approx_eq(m[3][0], 0.0) + && approx_eq(m[3][1], 0.0) + && approx_eq(m[3][2], 0.0) + && approx_eq(m[3][3], 1.0) + ); } #[test] @@ -518,15 +569,27 @@ mod tests { // X should be unchanged let rotated_x = r.rotate(Vec4::X); - assert!(vec_approx_eq(rotated_x, Vec4::X), "X should be unchanged, got {:?}", rotated_x); + assert!( + vec_approx_eq(rotated_x, Vec4::X), + "X should be unchanged, got {:?}", + rotated_x + ); // Y should go to Z let rotated_y = r.rotate(Vec4::Y); - assert!(vec_approx_eq(rotated_y, Vec4::Z), "Y should become Z, got {:?}", rotated_y); + assert!( + vec_approx_eq(rotated_y, Vec4::Z), + "Y should become Z, got {:?}", + rotated_y + ); // Z should go to -Y let rotated_z = r.rotate(Vec4::Z); - assert!(vec_approx_eq(rotated_z, -Vec4::Y), "Z should become -Y, got {:?}", rotated_z); + assert!( + vec_approx_eq(rotated_z, -Vec4::Y), + "Z should become -Y, got {:?}", + rotated_z + ); } #[test] @@ -543,10 +606,26 @@ mod tests { let w = composed.rotate(Vec4::W); // Check lengths are preserved - assert!(approx_eq(x.length(), 1.0), "X length not preserved: {}", x.length()); - assert!(approx_eq(y.length(), 1.0), "Y length not preserved: {}", y.length()); - assert!(approx_eq(z.length(), 1.0), "Z length not preserved: {}", z.length()); - assert!(approx_eq(w.length(), 1.0), "W length not preserved: {}", w.length()); + assert!( + approx_eq(x.length(), 1.0), + "X length not preserved: {}", + x.length() + ); + assert!( + approx_eq(y.length(), 1.0), + "Y length not preserved: {}", + y.length() + ); + assert!( + approx_eq(z.length(), 1.0), + "Z length not preserved: {}", + z.length() + ); + assert!( + approx_eq(w.length(), 1.0), + "W length not preserved: {}", + w.length() + ); // Check orthogonality (dot products should be 0) assert!(approx_eq(x.dot(y), 0.0), "X.Y not orthogonal: {}", x.dot(y)); @@ -565,17 +644,28 @@ mod tests { let r_roll_w = Rotor4::from_plane_angle(RotationPlane::ZW, 0.2); let r_roll_xw = Rotor4::from_plane_angle(RotationPlane::XW, 0.1); - let composed = r_roll_xw.compose(&r_roll_w.compose(&r_pitch.compose(&r_yaw))).normalize(); + let composed = r_roll_xw + .compose(&r_roll_w.compose(&r_pitch.compose(&r_yaw))) + .normalize(); // Verify it's still a unit rotor - assert!(approx_eq(composed.magnitude(), 1.0), "Composed rotor not unit: {}", composed.magnitude()); + assert!( + approx_eq(composed.magnitude(), 1.0), + "Composed rotor not unit: {}", + composed.magnitude() + ); // Verify rotation preserves lengths (use slightly larger epsilon for accumulated error) let v = Vec4::new(1.0, 2.0, 3.0, 4.0); let rotated = composed.rotate(v); let length_error = (v.length() - rotated.length()).abs(); - assert!(length_error < 0.001, - "Length not preserved: {} vs {} (error: {})", v.length(), rotated.length(), length_error); + assert!( + length_error < 0.001, + "Length not preserved: {} vs {} (error: {})", + v.length(), + rotated.length(), + length_error + ); } #[test] @@ -599,8 +689,12 @@ mod tests { m[0][3] * v.x + m[1][3] * v.y + m[2][3] * v.z + m[3][3] * v.w, ); - assert!(vec_approx_eq(rotated_rotor, rotated_matrix), - "Rotor and matrix give different results: {:?} vs {:?}", rotated_rotor, rotated_matrix); + assert!( + vec_approx_eq(rotated_rotor, rotated_matrix), + "Rotor and matrix give different results: {:?} vs {:?}", + rotated_rotor, + rotated_matrix + ); } #[test] @@ -616,12 +710,22 @@ mod tests { // Check that column vectors are orthonormal for i in 0..4 { let col_i = Vec4::new(m[i][0], m[i][1], m[i][2], m[i][3]); - assert!(approx_eq(col_i.length(), 1.0), "Column {} not unit length", i); + assert!( + approx_eq(col_i.length(), 1.0), + "Column {} not unit length", + i + ); - for j in (i+1)..4 { + for j in (i + 1)..4 { let col_j = Vec4::new(m[j][0], m[j][1], m[j][2], m[j][3]); let dot = col_i.dot(col_j); - assert!(approx_eq(dot, 0.0), "Columns {} and {} not orthogonal: dot = {}", i, j, dot); + assert!( + approx_eq(dot, 0.0), + "Columns {} and {} not orthogonal: dot = {}", + i, + j, + dot + ); } } } @@ -640,8 +744,12 @@ mod tests { let rotated_composed = composed.rotate(v); let rotated_expected = expected.rotate(v); - assert!(vec_approx_eq(rotated_composed, rotated_expected), - "Same-plane composition failed: {:?} vs {:?}", rotated_composed, rotated_expected); + assert!( + vec_approx_eq(rotated_composed, rotated_expected), + "Same-plane composition failed: {:?} vs {:?}", + rotated_composed, + rotated_expected + ); } #[test] @@ -660,8 +768,12 @@ mod tests { let composed = r_yz.compose(&r_xz); let result = composed.rotate(v); - assert!(vec_approx_eq(step2, result), - "Sequential {:?} vs composed {:?}", step2, result); + assert!( + vec_approx_eq(step2, result), + "Sequential {:?} vs composed {:?}", + step2, + result + ); } #[test] @@ -673,13 +785,18 @@ mod tests { println!("r_xz: s={}, b_xz={}", r_xz.s, r_xz.b_xz); println!("r_yz: s={}, b_yz={}", r_yz.s, r_yz.b_yz); - println!("composed: s={}, b_xy={}, b_xz={}, b_yz={}, p={}", - composed.s, composed.b_xy, composed.b_xz, composed.b_yz, composed.p); + println!( + "composed: s={}, b_xy={}, b_xz={}, b_yz={}, p={}", + composed.s, composed.b_xy, composed.b_xz, composed.b_yz, composed.p + ); println!("composed magnitude: {}", composed.magnitude()); // The composed rotor should be a unit rotor - assert!(approx_eq(composed.magnitude(), 1.0), - "Composed rotor not unit: {}", composed.magnitude()); + assert!( + approx_eq(composed.magnitude(), 1.0), + "Composed rotor not unit: {}", + composed.magnitude() + ); } #[test] @@ -695,10 +812,22 @@ mod tests { // Print the rotation matrix let m = composed.to_matrix(); println!("Rotation matrix for composed rotor:"); - println!("[{:6.3} {:6.3} {:6.3} {:6.3}]", m[0][0], m[0][1], m[0][2], m[0][3]); - println!("[{:6.3} {:6.3} {:6.3} {:6.3}]", m[1][0], m[1][1], m[1][2], m[1][3]); - println!("[{:6.3} {:6.3} {:6.3} {:6.3}]", m[2][0], m[2][1], m[2][2], m[2][3]); - println!("[{:6.3} {:6.3} {:6.3} {:6.3}]", m[3][0], m[3][1], m[3][2], m[3][3]); + println!( + "[{:6.3} {:6.3} {:6.3} {:6.3}]", + m[0][0], m[0][1], m[0][2], m[0][3] + ); + println!( + "[{:6.3} {:6.3} {:6.3} {:6.3}]", + m[1][0], m[1][1], m[1][2], m[1][3] + ); + println!( + "[{:6.3} {:6.3} {:6.3} {:6.3}]", + m[2][0], m[2][1], m[2][2], m[2][3] + ); + println!( + "[{:6.3} {:6.3} {:6.3} {:6.3}]", + m[3][0], m[3][1], m[3][2], m[3][3] + ); // The correct matrix should have: // Column 0 (what X maps to): (0, -1, 0, 0) based on GA calculation @@ -710,6 +839,9 @@ mod tests { println!("X via composed: {:?}", x_via_composed); // The first column of the matrix tells us where X goes - println!("Matrix column 0: ({}, {}, {}, {})", m[0][0], m[1][0], m[2][0], m[3][0]); + println!( + "Matrix column 0: ({}, {}, {}, {})", + m[0][0], m[1][0], m[2][0], m[3][0] + ); } } diff --git a/crates/rust4d_math/src/tesseract.rs b/crates/rust4d_math/src/tesseract.rs index bd11f75..0f7d08a 100644 --- a/crates/rust4d_math/src/tesseract.rs +++ b/crates/rust4d_math/src/tesseract.rs @@ -5,7 +5,10 @@ //! //! For cross-section rendering, we decompose it into tetrahedra (3-simplices). -use crate::{Vec4, shape::{ConvexShape4D, Tetrahedron}}; +use crate::{ + shape::{ConvexShape4D, Tetrahedron}, + Vec4, +}; use std::collections::HashSet; /// A tesseract (4D hypercube) - pure geometry without colors @@ -31,21 +34,21 @@ impl Tesseract4D { // Using binary counting: vertex i has coordinates based on bits of i let vertices = [ Vec4::new(-h, -h, -h, -h), // 0 = 0b0000 - Vec4::new( h, -h, -h, -h), // 1 = 0b0001 - Vec4::new(-h, h, -h, -h), // 2 = 0b0010 - Vec4::new( h, h, -h, -h), // 3 = 0b0011 - Vec4::new(-h, -h, h, -h), // 4 = 0b0100 - Vec4::new( h, -h, h, -h), // 5 = 0b0101 - Vec4::new(-h, h, h, -h), // 6 = 0b0110 - Vec4::new( h, h, h, -h), // 7 = 0b0111 - Vec4::new(-h, -h, -h, h), // 8 = 0b1000 - Vec4::new( h, -h, -h, h), // 9 = 0b1001 - Vec4::new(-h, h, -h, h), // 10 = 0b1010 - Vec4::new( h, h, -h, h), // 11 = 0b1011 - Vec4::new(-h, -h, h, h), // 12 = 0b1100 - Vec4::new( h, -h, h, h), // 13 = 0b1101 - Vec4::new(-h, h, h, h), // 14 = 0b1110 - Vec4::new( h, h, h, h), // 15 = 0b1111 + Vec4::new(h, -h, -h, -h), // 1 = 0b0001 + Vec4::new(-h, h, -h, -h), // 2 = 0b0010 + Vec4::new(h, h, -h, -h), // 3 = 0b0011 + Vec4::new(-h, -h, h, -h), // 4 = 0b0100 + Vec4::new(h, -h, h, -h), // 5 = 0b0101 + Vec4::new(-h, h, h, -h), // 6 = 0b0110 + Vec4::new(h, h, h, -h), // 7 = 0b0111 + Vec4::new(-h, -h, -h, h), // 8 = 0b1000 + Vec4::new(h, -h, -h, h), // 9 = 0b1001 + Vec4::new(-h, h, -h, h), // 10 = 0b1010 + Vec4::new(h, h, -h, h), // 11 = 0b1011 + Vec4::new(-h, -h, h, h), // 12 = 0b1100 + Vec4::new(h, -h, h, h), // 13 = 0b1101 + Vec4::new(-h, h, h, h), // 14 = 0b1110 + Vec4::new(h, h, h, h), // 15 = 0b1111 ]; // Compute tetrahedra decomposition using Kuhn triangulation @@ -102,10 +105,30 @@ impl Tesseract4D { fn compute_tetrahedra() -> Vec { // Generate all permutations of [0, 1, 2, 3] for Kuhn triangulation let permutations = [ - [0, 1, 2, 3], [0, 1, 3, 2], [0, 2, 1, 3], [0, 2, 3, 1], [0, 3, 1, 2], [0, 3, 2, 1], - [1, 0, 2, 3], [1, 0, 3, 2], [1, 2, 0, 3], [1, 2, 3, 0], [1, 3, 0, 2], [1, 3, 2, 0], - [2, 0, 1, 3], [2, 0, 3, 1], [2, 1, 0, 3], [2, 1, 3, 0], [2, 3, 0, 1], [2, 3, 1, 0], - [3, 0, 1, 2], [3, 0, 2, 1], [3, 1, 0, 2], [3, 1, 2, 0], [3, 2, 0, 1], [3, 2, 1, 0], + [0, 1, 2, 3], + [0, 1, 3, 2], + [0, 2, 1, 3], + [0, 2, 3, 1], + [0, 3, 1, 2], + [0, 3, 2, 1], + [1, 0, 2, 3], + [1, 0, 3, 2], + [1, 2, 0, 3], + [1, 2, 3, 0], + [1, 3, 0, 2], + [1, 3, 2, 0], + [2, 0, 1, 3], + [2, 0, 3, 1], + [2, 1, 0, 3], + [2, 1, 3, 0], + [2, 3, 0, 1], + [2, 3, 1, 0], + [3, 0, 1, 2], + [3, 0, 2, 1], + [3, 1, 0, 2], + [3, 1, 2, 0], + [3, 2, 0, 1], + [3, 2, 1, 0], ]; // Generate 5-cells from permutations @@ -260,7 +283,7 @@ mod tests { let mut tet_edges: HashSet<(usize, usize)> = HashSet::new(); for tet in t.tetrahedra() { for i in 0..4 { - for j in (i+1)..4 { + for j in (i + 1)..4 { let (v0, v1) = if tet.indices[i] < tet.indices[j] { (tet.indices[i], tet.indices[j]) } else { @@ -273,11 +296,15 @@ mod tests { // Check that all tesseract edges are covered for i in 0usize..16 { - for j in (i+1)..16 { + for j in (i + 1)..16 { if (i ^ j).count_ones() == 1 { // This is a tesseract edge - assert!(tet_edges.contains(&(i, j)), - "Tesseract edge ({}, {}) not in any tetrahedron", i, j); + assert!( + tet_edges.contains(&(i, j)), + "Tesseract edge ({}, {}) not in any tetrahedron", + i, + j + ); } } } diff --git a/crates/rust4d_math/src/vec4.rs b/crates/rust4d_math/src/vec4.rs index 0343f7d..d378a79 100644 --- a/crates/rust4d_math/src/vec4.rs +++ b/crates/rust4d_math/src/vec4.rs @@ -1,7 +1,7 @@ //! 4D Vector type use bytemuck::{Pod, Zeroable}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; /// 4D Vector with x, y, z, w components /// The w component represents the 4th spatial dimension (ana/kata) @@ -15,11 +15,36 @@ pub struct Vec4 { } impl Vec4 { - pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0, w: 0.0 }; - pub const X: Self = Self { x: 1.0, y: 0.0, z: 0.0, w: 0.0 }; - pub const Y: Self = Self { x: 0.0, y: 1.0, z: 0.0, w: 0.0 }; - pub const Z: Self = Self { x: 0.0, y: 0.0, z: 1.0, w: 0.0 }; - pub const W: Self = Self { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }; + pub const ZERO: Self = Self { + x: 0.0, + y: 0.0, + z: 0.0, + w: 0.0, + }; + pub const X: Self = Self { + x: 1.0, + y: 0.0, + z: 0.0, + w: 0.0, + }; + pub const Y: Self = Self { + x: 0.0, + y: 1.0, + z: 0.0, + w: 0.0, + }; + pub const Z: Self = Self { + x: 0.0, + y: 0.0, + z: 1.0, + w: 0.0, + }; + pub const W: Self = Self { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0, + }; /// Create a new Vec4 #[inline] @@ -275,12 +300,7 @@ impl std::ops::Mul for f32 { type Output = Vec4; #[inline] fn mul(self, vec: Vec4) -> Vec4 { - Vec4::new( - self * vec.x, - self * vec.y, - self * vec.z, - self * vec.w, - ) + Vec4::new(self * vec.x, self * vec.y, self * vec.z, self * vec.w) } } @@ -511,14 +531,22 @@ mod tests { let x = Vec4::X; let y = Vec4::Y; let angle = x.angle_to(y); - assert!((angle - FRAC_PI_2).abs() < 0.0001, "Orthogonal vectors should have π/2 angle, got {}", angle); + assert!( + (angle - FRAC_PI_2).abs() < 0.0001, + "Orthogonal vectors should have π/2 angle, got {}", + angle + ); } #[test] fn test_angle_to_same_direction() { let v = Vec4::new(1.0, 2.0, 3.0, 4.0); let angle = v.angle_to(v); - assert!(angle.abs() < 0.0001, "Same direction should have 0 angle, got {}", angle); + assert!( + angle.abs() < 0.0001, + "Same direction should have 0 angle, got {}", + angle + ); } #[test] @@ -527,7 +555,11 @@ mod tests { let v = Vec4::X; let neg_v = -v; let angle = v.angle_to(neg_v); - assert!((angle - PI).abs() < 0.0001, "Opposite vectors should have π angle, got {}", angle); + assert!( + (angle - PI).abs() < 0.0001, + "Opposite vectors should have π angle, got {}", + angle + ); } #[test] @@ -537,7 +569,10 @@ mod tests { // Angle between X and Y is π/2. If we allow more than that, should reach target let result = from.rotate_towards(to, 2.0); let dot = result.dot(to); - assert!(dot > 0.99, "rotate_towards should reach target when max_radians is large enough"); + assert!( + dot > 0.99, + "rotate_towards should reach target when max_radians is large enough" + ); } #[test] @@ -548,8 +583,12 @@ mod tests { // Rotate by π/4, should be halfway between X and Y let result = from.rotate_towards(to, FRAC_PI_4); let angle_from_start = from.angle_to(result); - assert!((angle_from_start - FRAC_PI_4).abs() < 0.01, - "Should rotate by max_radians, rotated {} instead of {}", angle_from_start, FRAC_PI_4); + assert!( + (angle_from_start - FRAC_PI_4).abs() < 0.01, + "Should rotate by max_radians, rotated {} instead of {}", + angle_from_start, + FRAC_PI_4 + ); } #[test] @@ -558,7 +597,11 @@ mod tests { let to = Vec4::new(0.0, 1.0, 1.0, 1.0).normalized(); let result = from.rotate_towards(to, 0.5); let len = result.length(); - assert!((len - 1.0).abs() < 0.01, "rotate_towards should preserve length, got {}", len); + assert!( + (len - 1.0).abs() < 0.01, + "rotate_towards should preserve length, got {}", + len + ); } #[test] @@ -569,12 +612,18 @@ mod tests { let to = -Vec4::X; let result = from.rotate_towards(to, FRAC_PI_4); let len = result.length(); - assert!((len - 1.0).abs() < 0.01, - "rotate_towards anti-parallel should produce unit vector, got length {}", len); + assert!( + (len - 1.0).abs() < 0.01, + "rotate_towards anti-parallel should produce unit vector, got length {}", + len + ); // Should have rotated 45° from X toward -X let angle = from.angle_to(result); - assert!((angle - FRAC_PI_4).abs() < 0.01, - "Should rotate by max_radians, got {}", angle); + assert!( + (angle - FRAC_PI_4).abs() < 0.01, + "Should rotate by max_radians, got {}", + angle + ); } #[test] @@ -585,8 +634,12 @@ mod tests { for axis in &axes { let result = axis.rotate_towards(-*axis, FRAC_PI_4); let len = result.length(); - assert!((len - 1.0).abs() < 0.01, - "Anti-parallel rotate_towards failed for {:?}, length = {}", axis, len); + assert!( + (len - 1.0).abs() < 0.01, + "Anti-parallel rotate_towards failed for {:?}, length = {}", + axis, + len + ); } } } diff --git a/crates/rust4d_math/tests/interpolation_external.rs b/crates/rust4d_math/tests/interpolation_external.rs index ab9610f..23b79bc 100644 --- a/crates/rust4d_math/tests/interpolation_external.rs +++ b/crates/rust4d_math/tests/interpolation_external.rs @@ -4,7 +4,7 @@ //! by code outside the rust4d_math crate, which is the typical use case for the //! tween system in rust4d_game. -use rust4d_math::{Interpolatable, Rotor4, RotationPlane, Vec4}; +use rust4d_math::{Interpolatable, RotationPlane, Rotor4, Vec4}; use std::f32::consts::PI; const EPSILON: f32 = 0.001; diff --git a/crates/rust4d_physics/src/body.rs b/crates/rust4d_physics/src/body.rs index 0287475..9d95423 100644 --- a/crates/rust4d_physics/src/body.rs +++ b/crates/rust4d_physics/src/body.rs @@ -398,8 +398,7 @@ mod tests { #[test] fn test_with_material() { - let body = RigidBody4D::new_sphere(Vec4::ZERO, 1.0) - .with_material(PhysicsMaterial::RUBBER); + let body = RigidBody4D::new_sphere(Vec4::ZERO, 1.0).with_material(PhysicsMaterial::RUBBER); assert_eq!(body.material, PhysicsMaterial::RUBBER); } @@ -448,8 +447,7 @@ mod tests { #[test] fn test_with_filter() { use crate::collision::CollisionLayer; - let body = RigidBody4D::new_sphere(Vec4::ZERO, 1.0) - .with_filter(CollisionFilter::player()); + let body = RigidBody4D::new_sphere(Vec4::ZERO, 1.0).with_filter(CollisionFilter::player()); assert_eq!(body.filter.layer, CollisionLayer::PLAYER); } @@ -457,8 +455,7 @@ mod tests { #[test] fn test_with_layer() { use crate::collision::CollisionLayer; - let body = RigidBody4D::new_sphere(Vec4::ZERO, 1.0) - .with_layer(CollisionLayer::ENEMY); + let body = RigidBody4D::new_sphere(Vec4::ZERO, 1.0).with_layer(CollisionLayer::ENEMY); assert_eq!(body.filter.layer, CollisionLayer::ENEMY); } @@ -495,10 +492,10 @@ mod tests { fn test_floor_bounded_creates_aabb() { use crate::shapes::Collider; let collider = StaticCollider::floor_bounded( - 0.0, // y: floor surface at y=0 - 10.0, // half_size_xz - 5.0, // half_size_w - 1.0, // thickness (clamped to minimum 5.0) + 0.0, // y: floor surface at y=0 + 10.0, // half_size_xz + 5.0, // half_size_w + 1.0, // thickness (clamped to minimum 5.0) PhysicsMaterial::CONCRETE, ); @@ -529,10 +526,10 @@ mod tests { use crate::shapes::Collider; // Thin thickness is clamped to minimum 5.0 let collider = StaticCollider::floor_bounded( - 5.0, // y: floor surface at y=5 - 1.0, // half_size_xz - 1.0, // half_size_w - 0.01, // thickness (clamped to 5.0) + 5.0, // y: floor surface at y=5 + 1.0, // half_size_xz + 1.0, // half_size_w + 0.01, // thickness (clamped to 5.0) PhysicsMaterial::RUBBER, ); @@ -552,10 +549,10 @@ mod tests { use crate::shapes::Collider; // Can specify larger thickness let collider = StaticCollider::floor_bounded( - 0.0, // y: floor surface at y=0 - 10.0, // half_size_xz - 5.0, // half_size_w - 20.0, // thickness (larger than minimum) + 0.0, // y: floor surface at y=0 + 10.0, // half_size_xz + 5.0, // half_size_w + 20.0, // thickness (larger than minimum) PhysicsMaterial::CONCRETE, ); @@ -570,8 +567,8 @@ mod tests { #[test] fn test_floor_bounded_collision_with_sphere() { - use crate::shapes::{Collider, Sphere4D}; use crate::collision::sphere_vs_aabb; + use crate::shapes::{Collider, Sphere4D}; // Values from default.ron scene let collider = StaticCollider::floor_bounded( @@ -589,41 +586,54 @@ mod tests { // Verify floor bounds assert_eq!(aabb.max.y, -2.0, "Floor top should be at y=-2"); - assert_eq!(aabb.min.y, -7.0, "Floor bottom should extend 5 units down (minimum)"); + assert_eq!( + aabb.min.y, -7.0, + "Floor bottom should extend 5 units down (minimum)" + ); // Player spawn at (0, 0, 5, 0) with radius 0.5 let player_radius = 0.5; // Player at spawn position (above floor) - should NOT collide let player_above = Sphere4D::new(Vec4::new(0.0, 0.0, 5.0, 0.0), player_radius); - assert!(sphere_vs_aabb(&player_above, aabb).is_none(), "Player at spawn should not collide"); + assert!( + sphere_vs_aabb(&player_above, aabb).is_none(), + "Player at spawn should not collide" + ); // Player fallen to slightly penetrating floor (center at y = -2 + 0.5 - 0.1 = -1.6) // This is 0.1 units below the tangent point - let player_penetrating_slight = Sphere4D::new(Vec4::new(0.0, -1.6, 5.0, 0.0), player_radius); + let player_penetrating_slight = + Sphere4D::new(Vec4::new(0.0, -1.6, 5.0, 0.0), player_radius); let contact = sphere_vs_aabb(&player_penetrating_slight, aabb); assert!(contact.is_some(), "Player penetrating floor should collide"); // Player outside X/Z bounds - should NOT collide (can fall off edge) let player_off_edge_xz = Sphere4D::new(Vec4::new(15.0, -1.6, 5.0, 0.0), player_radius); - assert!(sphere_vs_aabb(&player_off_edge_xz, aabb).is_none(), "Player off X edge should not collide"); + assert!( + sphere_vs_aabb(&player_off_edge_xz, aabb).is_none(), + "Player off X edge should not collide" + ); // Player outside W bounds - should NOT collide (can fall off W edge) // Floor W extent is -5 to +5, so W=10 is outside let player_off_edge_w = Sphere4D::new(Vec4::new(0.0, -1.6, 5.0, 10.0), player_radius); - assert!(sphere_vs_aabb(&player_off_edge_w, aabb).is_none(), "Player off W edge should not collide"); + assert!( + sphere_vs_aabb(&player_off_edge_w, aabb).is_none(), + "Player off W edge should not collide" + ); } #[test] fn test_floor_bounded_4d_edges() { - use crate::shapes::{Collider, Sphere4D}; use crate::collision::sphere_vs_aabb; + use crate::shapes::{Collider, Sphere4D}; let collider = StaticCollider::floor_bounded( - -2.0, // y: floor surface - 10.0, // half_size_xz (X/Z from -10 to +10) - 5.0, // half_size_w (W from -5 to +5) - 5.0, // thickness + -2.0, // y: floor surface + 10.0, // half_size_xz (X/Z from -10 to +10) + 5.0, // half_size_w (W from -5 to +5) + 5.0, // thickness PhysicsMaterial::CONCRETE, ); @@ -637,23 +647,38 @@ mod tests { // On floor at center - SHOULD collide let on_floor = Sphere4D::new(Vec4::new(0.0, y_on_floor, 0.0, 0.0), radius); - assert!(sphere_vs_aabb(&on_floor, aabb).is_some(), "Center should collide"); + assert!( + sphere_vs_aabb(&on_floor, aabb).is_some(), + "Center should collide" + ); // On floor at W=-4 (inside W bounds) - SHOULD collide let inside_w = Sphere4D::new(Vec4::new(0.0, y_on_floor, 0.0, -4.0), radius); - assert!(sphere_vs_aabb(&inside_w, aabb).is_some(), "Inside W bounds should collide"); + assert!( + sphere_vs_aabb(&inside_w, aabb).is_some(), + "Inside W bounds should collide" + ); // Off floor at W=6 (outside W bounds) - should NOT collide let outside_w_pos = Sphere4D::new(Vec4::new(0.0, y_on_floor, 0.0, 6.0), radius); - assert!(sphere_vs_aabb(&outside_w_pos, aabb).is_none(), "Outside +W should not collide"); + assert!( + sphere_vs_aabb(&outside_w_pos, aabb).is_none(), + "Outside +W should not collide" + ); // Off floor at W=-6 (outside W bounds) - should NOT collide let outside_w_neg = Sphere4D::new(Vec4::new(0.0, y_on_floor, 0.0, -6.0), radius); - assert!(sphere_vs_aabb(&outside_w_neg, aabb).is_none(), "Outside -W should not collide"); + assert!( + sphere_vs_aabb(&outside_w_neg, aabb).is_none(), + "Outside -W should not collide" + ); // Off floor at X=12 - should NOT collide let outside_x = Sphere4D::new(Vec4::new(12.0, y_on_floor, 0.0, 0.0), radius); - assert!(sphere_vs_aabb(&outside_x, aabb).is_none(), "Outside X should not collide"); + assert!( + sphere_vs_aabb(&outside_x, aabb).is_none(), + "Outside X should not collide" + ); } // ===== is_position_over Tests ===== @@ -661,10 +686,10 @@ mod tests { #[test] fn test_is_position_over_aabb_inside() { let floor = StaticCollider::floor_bounded( - 0.0, // y - 10.0, // half_size_xz (X/Z: -10 to +10) - 5.0, // half_size_w (W: -5 to +5) - 5.0, // thickness + 0.0, // y + 10.0, // half_size_xz (X/Z: -10 to +10) + 5.0, // half_size_w (W: -5 to +5) + 5.0, // thickness PhysicsMaterial::CONCRETE, ); @@ -681,10 +706,10 @@ mod tests { #[test] fn test_is_position_over_aabb_outside() { let floor = StaticCollider::floor_bounded( - 0.0, // y - 10.0, // half_size_xz (X/Z: -10 to +10) - 5.0, // half_size_w (W: -5 to +5) - 5.0, // thickness + 0.0, // y + 10.0, // half_size_xz (X/Z: -10 to +10) + 5.0, // half_size_w (W: -5 to +5) + 5.0, // thickness PhysicsMaterial::CONCRETE, ); @@ -717,10 +742,10 @@ mod tests { #[test] fn test_is_position_over_ignores_y() { let floor = StaticCollider::floor_bounded( - 0.0, // y: floor at y=0 - 10.0, // half_size_xz - 5.0, // half_size_w - 5.0, // thickness (floor AABB: y from -5 to 0) + 0.0, // y: floor at y=0 + 10.0, // half_size_xz + 5.0, // half_size_w + 5.0, // thickness (floor AABB: y from -5 to 0) PhysicsMaterial::CONCRETE, ); diff --git a/crates/rust4d_physics/src/collision.rs b/crates/rust4d_physics/src/collision.rs index a786a9d..4a74069 100644 --- a/crates/rust4d_physics/src/collision.rs +++ b/crates/rust4d_physics/src/collision.rs @@ -434,7 +434,10 @@ mod tests { #[test] fn test_aabb_vs_plane_above() { - let aabb = AABB4D::from_center_half_extents(Vec4::new(0.0, 2.0, 0.0, 0.0), Vec4::new(0.5, 0.5, 0.5, 0.5)); + let aabb = AABB4D::from_center_half_extents( + Vec4::new(0.0, 2.0, 0.0, 0.0), + Vec4::new(0.5, 0.5, 0.5, 0.5), + ); let plane = Plane4D::floor(0.0); // AABB is above plane (lowest point at y=1.5) @@ -443,7 +446,10 @@ mod tests { #[test] fn test_aabb_vs_plane_colliding() { - let aabb = AABB4D::from_center_half_extents(Vec4::new(0.0, 0.25, 0.0, 0.0), Vec4::new(0.5, 0.5, 0.5, 0.5)); + let aabb = AABB4D::from_center_half_extents( + Vec4::new(0.0, 0.25, 0.0, 0.0), + Vec4::new(0.5, 0.5, 0.5, 0.5), + ); let plane = Plane4D::floor(0.0); // AABB lowest point at y=-0.25, floor at y=0 @@ -475,7 +481,10 @@ mod tests { #[test] fn test_aabb_vs_aabb_no_collision() { let a = AABB4D::from_center_half_extents(Vec4::ZERO, Vec4::new(0.5, 0.5, 0.5, 0.5)); - let b = AABB4D::from_center_half_extents(Vec4::new(5.0, 0.0, 0.0, 0.0), Vec4::new(0.5, 0.5, 0.5, 0.5)); + let b = AABB4D::from_center_half_extents( + Vec4::new(5.0, 0.0, 0.0, 0.0), + Vec4::new(0.5, 0.5, 0.5, 0.5), + ); assert!(aabb_vs_aabb(&a, &b).is_none()); } @@ -483,7 +492,10 @@ mod tests { #[test] fn test_aabb_vs_aabb_colliding() { let a = AABB4D::from_center_half_extents(Vec4::ZERO, Vec4::new(1.0, 1.0, 1.0, 1.0)); - let b = AABB4D::from_center_half_extents(Vec4::new(1.5, 0.0, 0.0, 0.0), Vec4::new(1.0, 1.0, 1.0, 1.0)); + let b = AABB4D::from_center_half_extents( + Vec4::new(1.5, 0.0, 0.0, 0.0), + Vec4::new(1.0, 1.0, 1.0, 1.0), + ); // Overlap on x-axis: a.max.x=1.0, b.min.x=0.5, overlap=0.5 let contact = aabb_vs_aabb(&a, &b).expect("Should collide"); @@ -625,21 +637,23 @@ mod tests { assert_eq!(floor.min.y, -7.0, "Floor bottom should be at y=-7"); // Tesseract at starting position (y=0), half_extent=1 - let tesseract_start = AABB4D::from_center_half_extents( - Vec4::ZERO, - Vec4::new(1.0, 1.0, 1.0, 1.0), - ); + let tesseract_start = + AABB4D::from_center_half_extents(Vec4::ZERO, Vec4::new(1.0, 1.0, 1.0, 1.0)); // Tesseract bottom at y=-1, floor top at y=-2 → no collision - assert!(aabb_vs_aabb(&tesseract_start, &floor).is_none(), - "Tesseract at y=0 should not collide with floor at y=-2"); + assert!( + aabb_vs_aabb(&tesseract_start, &floor).is_none(), + "Tesseract at y=0 should not collide with floor at y=-2" + ); // Tesseract fallen to y=-0.9 (bottom at y=-1.9, still above floor at y=-2) let tesseract_almost = AABB4D::from_center_half_extents( Vec4::new(0.0, -0.9, 0.0, 0.0), Vec4::new(1.0, 1.0, 1.0, 1.0), ); - assert!(aabb_vs_aabb(&tesseract_almost, &floor).is_none(), - "Tesseract at y=-0.9 should not collide (bottom at -1.9, floor top at -2)"); + assert!( + aabb_vs_aabb(&tesseract_almost, &floor).is_none(), + "Tesseract at y=-0.9 should not collide (bottom at -1.9, floor top at -2)" + ); // Tesseract fallen to y=-1.1 (bottom at y=-2.1, penetrating floor) let tesseract_touching = AABB4D::from_center_half_extents( @@ -647,9 +661,15 @@ mod tests { Vec4::new(1.0, 1.0, 1.0, 1.0), ); let contact = aabb_vs_aabb(&tesseract_touching, &floor); - assert!(contact.is_some(), "Tesseract at y=-1.1 should collide with floor"); + assert!( + contact.is_some(), + "Tesseract at y=-1.1 should collide with floor" + ); let contact = contact.unwrap(); - assert!(contact.penetration > 0.0, "Should have positive penetration"); + assert!( + contact.penetration > 0.0, + "Should have positive penetration" + ); // Tesseract at rest position y=-1 (bottom at y=-2, exactly on floor) // At exact boundary - behavior is undefined (floating point edge case) @@ -662,8 +682,10 @@ mod tests { Vec4::new(0.0, -1.001, 0.0, 0.0), Vec4::new(1.0, 1.0, 1.0, 1.0), ); - assert!(aabb_vs_aabb(&tesseract_slightly_in, &floor).is_some(), - "Tesseract slightly below resting position should collide"); + assert!( + aabb_vs_aabb(&tesseract_slightly_in, &floor).is_some(), + "Tesseract slightly below resting position should collide" + ); } #[test] @@ -674,7 +696,9 @@ mod tests { let b = Sphere4D::new(Vec4::ZERO, 1.0); let contact = sphere_vs_sphere(&a, &b).expect("Coincident spheres should return a contact"); assert_eq!(contact.normal, Vec4::Y, "Fallback normal should be Y-axis"); - assert!((contact.penetration - 2.0).abs() < f32::EPSILON, - "Penetration should equal radius_a + radius_b"); + assert!( + (contact.penetration - 2.0).abs() < f32::EPSILON, + "Penetration should equal radius_a + radius_b" + ); } } diff --git a/crates/rust4d_physics/src/lib.rs b/crates/rust4d_physics/src/lib.rs index d3de7c4..86951f7 100644 --- a/crates/rust4d_physics/src/lib.rs +++ b/crates/rust4d_physics/src/lib.rs @@ -52,9 +52,12 @@ pub mod world; // Re-export commonly used types (see module doc for rationale) pub use body::{BodyKey, BodyType, RigidBody4D, StaticCollider}; -pub use collision::{aabb_vs_aabb, aabb_vs_plane, sphere_vs_aabb, sphere_vs_plane, sphere_vs_sphere, CollisionEvent, CollisionEventKind, CollisionFilter, CollisionLayer, Contact}; +pub use collision::{ + aabb_vs_aabb, aabb_vs_plane, sphere_vs_aabb, sphere_vs_plane, sphere_vs_sphere, CollisionEvent, + CollisionEventKind, CollisionFilter, CollisionLayer, Contact, +}; pub use material::PhysicsMaterial; +pub use raycast::{ray_vs_aabb, ray_vs_collider, ray_vs_plane, ray_vs_sphere, RayHit}; pub use shapes::{Collider, Plane4D, Sphere4D, AABB4D}; -pub use raycast::{RayHit, ray_vs_sphere, ray_vs_aabb, ray_vs_plane, ray_vs_collider}; -pub use spatial::{SpatialQueryResult, AreaEffectHit}; +pub use spatial::{AreaEffectHit, SpatialQueryResult}; pub use world::{PhysicsConfig, PhysicsWorld, RayTarget, WorldRayHit}; diff --git a/crates/rust4d_physics/src/raycast.rs b/crates/rust4d_physics/src/raycast.rs index 2dcce6c..e4e1529 100644 --- a/crates/rust4d_physics/src/raycast.rs +++ b/crates/rust4d_physics/src/raycast.rs @@ -2,8 +2,8 @@ //! //! Provides ray intersection tests against spheres, AABBs, and planes. +use crate::shapes::{Collider, Plane4D, Sphere4D, AABB4D}; use rust4d_math::{Ray4D, Vec4}; -use crate::shapes::{Collider, Sphere4D, AABB4D, Plane4D}; /// Information about a ray hit #[derive(Clone, Copy, Debug)] @@ -71,7 +71,12 @@ pub fn ray_vs_aabb(ray: &Ray4D, aabb: &AABB4D) -> Option { // Check each axis using the slab method let origin = [ray.origin.x, ray.origin.y, ray.origin.z, ray.origin.w]; - let dir = [ray.direction.x, ray.direction.y, ray.direction.z, ray.direction.w]; + let dir = [ + ray.direction.x, + ray.direction.y, + ray.direction.z, + ray.direction.w, + ]; let mins = [aabb.min.x, aabb.min.y, aabb.min.z, aabb.min.w]; let maxs = [aabb.max.x, aabb.max.y, aabb.max.z, aabb.max.w]; @@ -132,10 +137,34 @@ pub fn ray_vs_aabb(ray: &Ray4D, aabb: &AABB4D) -> Option { } } match best_axis { - 0 => normal.x = if exit_origin[0] > (mins[0] + maxs[0]) * 0.5 { 1.0 } else { -1.0 }, - 1 => normal.y = if exit_origin[1] > (mins[1] + maxs[1]) * 0.5 { 1.0 } else { -1.0 }, - 2 => normal.z = if exit_origin[2] > (mins[2] + maxs[2]) * 0.5 { 1.0 } else { -1.0 }, - 3 => normal.w = if exit_origin[3] > (mins[3] + maxs[3]) * 0.5 { 1.0 } else { -1.0 }, + 0 => { + normal.x = if exit_origin[0] > (mins[0] + maxs[0]) * 0.5 { + 1.0 + } else { + -1.0 + } + } + 1 => { + normal.y = if exit_origin[1] > (mins[1] + maxs[1]) * 0.5 { + 1.0 + } else { + -1.0 + } + } + 2 => { + normal.z = if exit_origin[2] > (mins[2] + maxs[2]) * 0.5 { + 1.0 + } else { + -1.0 + } + } + 3 => { + normal.w = if exit_origin[3] > (mins[3] + maxs[3]) * 0.5 { + 1.0 + } else { + -1.0 + } + } _ => unreachable!(), } return Some(RayHit { @@ -181,7 +210,11 @@ pub fn ray_vs_plane(ray: &Ray4D, plane: &Plane4D) -> Option { let point = ray.point_at(t); // Normal always faces toward the ray origin side - let normal = if denom < 0.0 { plane.normal } else { -plane.normal }; + let normal = if denom < 0.0 { + plane.normal + } else { + -plane.normal + }; Some(RayHit { distance: t, @@ -295,8 +328,11 @@ mod tests { let hit = ray_vs_aabb(&ray, &aabb).expect("Should hit exit"); // Should hit exit face at x=0.5 - assert!((hit.point.x - 0.5).abs() < 0.01, - "Exit point x should be 0.5, got {}", hit.point.x); + assert!( + (hit.point.x - 0.5).abs() < 0.01, + "Exit point x should be 0.5, got {}", + hit.point.x + ); assert!(hit.distance > 0.0, "Distance should be positive"); } diff --git a/crates/rust4d_physics/src/shapes.rs b/crates/rust4d_physics/src/shapes.rs index 9731c94..c47d191 100644 --- a/crates/rust4d_physics/src/shapes.rs +++ b/crates/rust4d_physics/src/shapes.rs @@ -214,7 +214,10 @@ mod tests { #[test] fn test_aabb_from_center_half_extents() { - let aabb = AABB4D::from_center_half_extents(Vec4::new(1.0, 2.0, 3.0, 4.0), Vec4::new(0.5, 0.5, 0.5, 0.5)); + let aabb = AABB4D::from_center_half_extents( + Vec4::new(1.0, 2.0, 3.0, 4.0), + Vec4::new(0.5, 0.5, 0.5, 0.5), + ); assert_eq!(aabb.min, Vec4::new(0.5, 1.5, 2.5, 3.5)); assert_eq!(aabb.max, Vec4::new(1.5, 2.5, 3.5, 4.5)); assert_eq!(aabb.center(), Vec4::new(1.0, 2.0, 3.0, 4.0)); diff --git a/crates/rust4d_physics/src/world.rs b/crates/rust4d_physics/src/world.rs index 566018b..5f5e6a2 100644 --- a/crates/rust4d_physics/src/world.rs +++ b/crates/rust4d_physics/src/world.rs @@ -1,14 +1,17 @@ //! Physics world and simulation use crate::body::{BodyKey, RigidBody4D, StaticCollider}; -use crate::collision::{aabb_vs_aabb, aabb_vs_plane, sphere_vs_aabb, sphere_vs_plane, sphere_vs_sphere, CollisionEvent, CollisionEventKind, CollisionLayer, Contact}; +use crate::collision::{ + aabb_vs_aabb, aabb_vs_plane, sphere_vs_aabb, sphere_vs_plane, sphere_vs_sphere, CollisionEvent, + CollisionEventKind, CollisionLayer, Contact, +}; use crate::raycast::{ray_vs_collider, RayHit}; use crate::shapes::Collider; use rust4d_math::{Ray4D, Vec4}; use slotmap::SlotMap; use std::collections::HashSet; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; /// Configuration for the physics simulation #[derive(Clone, Debug, Serialize, Deserialize)] @@ -208,7 +211,12 @@ impl PhysicsWorld { /// /// Returns all hits within max_distance whose collision layer matches /// layer_mask, sorted by distance (nearest first). - pub fn raycast(&self, ray: &Ray4D, max_distance: f32, layer_mask: CollisionLayer) -> Vec { + pub fn raycast( + &self, + ray: &Ray4D, + max_distance: f32, + layer_mask: CollisionLayer, + ) -> Vec { let mut hits = Vec::new(); // Check all bodies @@ -218,7 +226,10 @@ impl PhysicsWorld { } if let Some(hit) = ray_vs_collider(ray, &body.collider) { if hit.distance <= max_distance { - hits.push(WorldRayHit { hit, target: RayTarget::Body(key) }); + hits.push(WorldRayHit { + hit, + target: RayTarget::Body(key), + }); } } } @@ -230,13 +241,21 @@ impl PhysicsWorld { } if let Some(hit) = ray_vs_collider(ray, &static_col.collider) { if hit.distance <= max_distance { - hits.push(WorldRayHit { hit, target: RayTarget::Static(index) }); + hits.push(WorldRayHit { + hit, + target: RayTarget::Static(index), + }); } } } // Sort by distance (nearest first) - hits.sort_by(|a, b| a.hit.distance.partial_cmp(&b.hit.distance).unwrap_or(std::cmp::Ordering::Equal)); + hits.sort_by(|a, b| { + a.hit + .distance + .partial_cmp(&b.hit.distance) + .unwrap_or(std::cmp::Ordering::Equal) + }); hits } @@ -244,7 +263,12 @@ impl PhysicsWorld { /// /// More efficient than `raycast()` for the common case — performs a /// single pass without allocating or sorting. - pub fn raycast_nearest(&self, ray: &Ray4D, max_distance: f32, layer_mask: CollisionLayer) -> Option { + pub fn raycast_nearest( + &self, + ray: &Ray4D, + max_distance: f32, + layer_mask: CollisionLayer, + ) -> Option { let mut nearest: Option = None; let mut nearest_dist = max_distance; @@ -255,7 +279,10 @@ impl PhysicsWorld { if let Some(hit) = ray_vs_collider(ray, &body.collider) { if hit.distance <= nearest_dist { nearest_dist = hit.distance; - nearest = Some(WorldRayHit { hit, target: RayTarget::Body(key) }); + nearest = Some(WorldRayHit { + hit, + target: RayTarget::Body(key), + }); } } } @@ -267,7 +294,10 @@ impl PhysicsWorld { if let Some(hit) = ray_vs_collider(ray, &static_col.collider) { if hit.distance <= nearest_dist { nearest_dist = hit.distance; - nearest = Some(WorldRayHit { hit, target: RayTarget::Static(index) }); + nearest = Some(WorldRayHit { + hit, + target: RayTarget::Static(index), + }); } } } @@ -520,12 +550,7 @@ impl PhysicsWorld { /// // Enemy spots player! /// } /// ``` - pub fn line_of_sight( - &self, - from: Vec4, - to: Vec4, - blocking_layers: CollisionLayer, - ) -> bool { + pub fn line_of_sight(&self, from: Vec4, to: Vec4, blocking_layers: CollisionLayer) -> bool { let delta = to - from; let max_distance = delta.length(); @@ -626,20 +651,17 @@ impl PhysicsWorld { } /// Check for collision between a body collider and a static collider - fn check_static_collision(body_collider: &Collider, static_collider: &Collider) -> Option { + fn check_static_collision( + body_collider: &Collider, + static_collider: &Collider, + ) -> Option { match (body_collider, static_collider) { // Body sphere vs static plane - (Collider::Sphere(sphere), Collider::Plane(plane)) => { - sphere_vs_plane(sphere, plane) - } + (Collider::Sphere(sphere), Collider::Plane(plane)) => sphere_vs_plane(sphere, plane), // Body AABB vs static plane - (Collider::AABB(aabb), Collider::Plane(plane)) => { - aabb_vs_plane(aabb, plane) - } + (Collider::AABB(aabb), Collider::Plane(plane)) => aabb_vs_plane(aabb, plane), // Body sphere vs static AABB - (Collider::Sphere(sphere), Collider::AABB(aabb)) => { - sphere_vs_aabb(sphere, aabb) - } + (Collider::Sphere(sphere), Collider::AABB(aabb)) => sphere_vs_aabb(sphere, aabb), // Body AABB vs static AABB (Collider::AABB(body_aabb), Collider::AABB(static_aabb)) => { aabb_vs_aabb(body_aabb, static_aabb) @@ -694,7 +716,10 @@ impl PhysicsWorld { if contact.is_colliding() { // Record collision event self.collision_events.push(CollisionEvent { - kind: CollisionEventKind::BodyVsStatic { body: key, static_index: static_idx }, + kind: CollisionEventKind::BodyVsStatic { + body: key, + static_index: static_idx, + }, contact: Some(contact), }); @@ -720,20 +745,20 @@ impl PhysicsWorld { body.velocity -= normal_velocity * (1.0 + combined.restitution); // Apply friction to horizontal (tangent) velocity - let tangent_velocity = body.velocity - contact.normal * body.velocity.dot(contact.normal); + let tangent_velocity = + body.velocity - contact.normal * body.velocity.dot(contact.normal); let tangent_speed = tangent_velocity.length(); if tangent_speed > 0.0001 { let friction_factor = 1.0 - combined.friction; body.velocity = contact.normal * body.velocity.dot(contact.normal) - + tangent_velocity * friction_factor; + + tangent_velocity * friction_factor; } } } } } } - } /// Resolve collisions between bodies @@ -752,7 +777,14 @@ impl PhysicsWorld { let (collider_a, collider_b, is_static_a, is_static_b, filter_a, filter_b) = { let body_a = &self.bodies[key_a]; let body_b = &self.bodies[key_b]; - (body_a.collider, body_b.collider, body_a.is_static(), body_b.is_static(), body_a.filter, body_b.filter) + ( + body_a.collider, + body_b.collider, + body_a.is_static(), + body_b.is_static(), + body_a.filter, + body_b.filter, + ) }; // Skip if both bodies are static @@ -768,9 +800,7 @@ impl PhysicsWorld { // Check for collision based on collider types // The contact normal convention: points FROM body A TOWARD body B let contact = match (&collider_a, &collider_b) { - (Collider::Sphere(a), Collider::Sphere(b)) => { - sphere_vs_sphere(a, b) - } + (Collider::Sphere(a), Collider::Sphere(b)) => sphere_vs_sphere(a, b), (Collider::Sphere(sphere), Collider::AABB(aabb)) => { // sphere_vs_aabb returns normal pointing from AABB toward sphere // We want normal from A (sphere) toward B (AABB), so flip it @@ -799,10 +829,19 @@ impl PhysicsWorld { if let Some(contact) = contact { if contact.is_colliding() { self.collision_events.push(CollisionEvent { - kind: CollisionEventKind::BodyVsBody { body_a: key_a, body_b: key_b }, + kind: CollisionEventKind::BodyVsBody { + body_a: key_a, + body_b: key_b, + }, contact: Some(contact), }); - self.resolve_body_pair_collision(key_a, key_b, &contact, is_static_a, is_static_b); + self.resolve_body_pair_collision( + key_a, + key_b, + &contact, + is_static_a, + is_static_b, + ); } } } @@ -864,7 +903,9 @@ impl PhysicsWorld { } // Combine materials from both bodies - let combined = self.bodies[key_a].material.combine(&self.bodies[key_b].material); + let combined = self.bodies[key_a] + .material + .combine(&self.bodies[key_b].material); // Velocity response rules: // - Static bodies: no velocity (implicit) @@ -881,12 +922,14 @@ impl PhysicsWorld { self.bodies[key_a].velocity -= normal_velocity * (1.0 + combined.restitution); // Apply friction to tangent velocity - let tangent_velocity = self.bodies[key_a].velocity - (-contact.normal) * self.bodies[key_a].velocity.dot(-contact.normal); + let tangent_velocity = self.bodies[key_a].velocity + - (-contact.normal) * self.bodies[key_a].velocity.dot(-contact.normal); let tangent_speed = tangent_velocity.length(); if tangent_speed > 0.0001 { let friction_factor = 1.0 - combined.friction; - self.bodies[key_a].velocity = (-contact.normal) * self.bodies[key_a].velocity.dot(-contact.normal) - + tangent_velocity * friction_factor; + self.bodies[key_a].velocity = (-contact.normal) + * self.bodies[key_a].velocity.dot(-contact.normal) + + tangent_velocity * friction_factor; } } } @@ -898,12 +941,14 @@ impl PhysicsWorld { self.bodies[key_b].velocity -= normal_velocity * (1.0 + combined.restitution); // Apply friction to tangent velocity - let tangent_velocity = self.bodies[key_b].velocity - contact.normal * self.bodies[key_b].velocity.dot(contact.normal); + let tangent_velocity = self.bodies[key_b].velocity + - contact.normal * self.bodies[key_b].velocity.dot(contact.normal); let tangent_speed = tangent_velocity.length(); if tangent_speed > 0.0001 { let friction_factor = 1.0 - combined.friction; - self.bodies[key_b].velocity = contact.normal * self.bodies[key_b].velocity.dot(contact.normal) - + tangent_velocity * friction_factor; + self.bodies[key_b].velocity = contact.normal + * self.bodies[key_b].velocity.dot(contact.normal) + + tangent_velocity * friction_factor; } } } @@ -941,12 +986,18 @@ impl PhysicsWorld { if self.active_triggers.contains(&(key, static_idx)) { self.collision_events.push(CollisionEvent { - kind: CollisionEventKind::TriggerStay { body: key, trigger_index: static_idx }, + kind: CollisionEventKind::TriggerStay { + body: key, + trigger_index: static_idx, + }, contact: Some(contact), }); } else { self.collision_events.push(CollisionEvent { - kind: CollisionEventKind::TriggerEnter { body: key, trigger_index: static_idx }, + kind: CollisionEventKind::TriggerEnter { + body: key, + trigger_index: static_idx, + }, contact: Some(contact), }); } @@ -958,12 +1009,16 @@ impl PhysicsWorld { // Emit exit events for pairs that are no longer overlapping for &(body_key, trigger_idx) in &self.active_triggers { if !current_overlaps.contains(&(body_key, trigger_idx)) - && self.bodies.contains_key(body_key) { - self.collision_events.push(CollisionEvent { - kind: CollisionEventKind::TriggerExit { body: body_key, trigger_index: trigger_idx }, - contact: None, - }); - } + && self.bodies.contains_key(body_key) + { + self.collision_events.push(CollisionEvent { + kind: CollisionEventKind::TriggerExit { + body: body_key, + trigger_index: trigger_idx, + }, + contact: None, + }); + } } self.active_triggers = current_overlaps; @@ -994,7 +1049,11 @@ mod tests { } /// Helper to create a world with a floor at the given Y position - fn world_with_floor(gravity: f32, floor_y: f32, floor_material: PhysicsMaterial) -> PhysicsWorld { + fn world_with_floor( + gravity: f32, + floor_y: f32, + floor_material: PhysicsMaterial, + ) -> PhysicsWorld { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(gravity)); world.add_static_collider(StaticCollider::floor(floor_y, floor_material)); world @@ -1109,8 +1168,7 @@ mod tests { fn test_floor_collision() { let mut world = world_with_floor(-20.0, 0.0, PhysicsMaterial::CONCRETE); // Sphere starting below the floor (partially penetrating) - let body = RigidBody4D::new_sphere(Vec4::new(0.0, 0.3, 0.0, 0.0), 0.5) - .with_gravity(false); + let body = RigidBody4D::new_sphere(Vec4::new(0.0, 0.3, 0.0, 0.0), 0.5).with_gravity(false); let handle = world.add_body(body); world.step(0.016); @@ -1225,8 +1283,7 @@ mod tests { #[test] fn test_gravity_disabled_body() { let mut world = PhysicsWorld::new(); - let body = RigidBody4D::new_sphere(Vec4::new(0.0, 10.0, 0.0, 0.0), 0.5) - .with_gravity(false); + let body = RigidBody4D::new_sphere(Vec4::new(0.0, 10.0, 0.0, 0.0), 0.5).with_gravity(false); let handle = world.add_body(body); world.step(1.0); @@ -1253,8 +1310,14 @@ mod tests { let body = world.get_body(handle).unwrap(); // Horizontal velocity should be reduced by friction // Rubber has friction 0.9, so velocity should be significantly reduced - assert!(body.velocity.x < 10.0, "Friction should slow horizontal movement"); - assert!(body.velocity.x < 5.0, "High friction should reduce velocity significantly"); + assert!( + body.velocity.x < 10.0, + "Friction should slow horizontal movement" + ); + assert!( + body.velocity.x < 5.0, + "High friction should reduce velocity significantly" + ); } #[test] @@ -1287,7 +1350,7 @@ mod tests { // Add a wall world.add_static_collider(StaticCollider::plane( - Vec4::new(1.0, 0.0, 0.0, 0.0), // Normal pointing +X + Vec4::new(1.0, 0.0, 0.0, 0.0), // Normal pointing +X 0.0, PhysicsMaterial::METAL, )); @@ -1319,8 +1382,11 @@ mod tests { // Ball should still be between 0 and 10 let ball = world.bodies.values().next().unwrap(); - assert!(ball.position.y >= 0.0 && ball.position.y <= 10.0, - "Ball should be between floor and ceiling, got y={}", ball.position.y); + assert!( + ball.position.y >= 0.0 && ball.position.y <= 10.0, + "Ball should be between floor and ceiling, got y={}", + ball.position.y + ); } // ====== Generic Body Method Tests ====== @@ -1466,7 +1532,10 @@ mod tests { let body = world.get_body(handle).unwrap(); // Body should have moved down (no floor collision) - assert!(body.position.y < 0.5, "Body should fall through trigger zone"); + assert!( + body.position.y < 0.5, + "Body should fall through trigger zone" + ); } #[test] @@ -1672,10 +1741,10 @@ mod tests { // Create world with bounded floor (W bounds: -2 to +2) let mut world = PhysicsWorld::with_config(PhysicsConfig::new(-20.0)); world.add_static_collider(StaticCollider::floor_bounded( - 0.0, // y: floor surface at y=0 - 10.0, // half_size_xz (X/Z from -10 to +10) - 2.0, // half_size_w (W from -2 to +2) - 5.0, // thickness + 0.0, // y: floor surface at y=0 + 10.0, // half_size_xz (X/Z from -10 to +10) + 2.0, // half_size_w (W from -2 to +2) + 5.0, // thickness PhysicsMaterial::CONCRETE, )); @@ -1727,10 +1796,10 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(-20.0)); world.add_static_collider(StaticCollider::floor_bounded( - 0.0, // floor at y=0 - 10.0, // half_size_xz - 2.0, // half_size_w (W: -2 to +2) - 5.0, // thickness + 0.0, // floor at y=0 + 10.0, // half_size_xz + 2.0, // half_size_w (W: -2 to +2) + 5.0, // thickness PhysicsMaterial::CONCRETE, )); @@ -1779,10 +1848,10 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(-20.0)); world.add_static_collider(StaticCollider::floor_bounded( - 0.0, // floor at y=0 - 10.0, // half_size_xz - 2.0, // half_size_w (W: -2 to +2) - 5.0, // thickness (floor bottom at y=-5) + 0.0, // floor at y=0 + 10.0, // half_size_xz + 2.0, // half_size_w (W: -2 to +2) + 5.0, // thickness (floor bottom at y=-5) PhysicsMaterial::CONCRETE, )); @@ -1816,10 +1885,10 @@ mod tests { // Make sure the edge falling fix doesn't break normal jumping let mut world = PhysicsWorld::with_config(PhysicsConfig::new(-20.0)); world.add_static_collider(StaticCollider::floor_bounded( - 0.0, // floor at y=0 - 10.0, // half_size_xz - 10.0, // half_size_w (large, body stays over floor) - 5.0, // thickness + 0.0, // floor at y=0 + 10.0, // half_size_xz + 10.0, // half_size_w (large, body stays over floor) + 5.0, // thickness PhysicsMaterial::CONCRETE, )); @@ -1835,7 +1904,10 @@ mod tests { // Jump world.body_jump(key, 8.0); - assert!(!world.body_is_grounded(key), "Body should be airborne after jump"); + assert!( + !world.body_is_grounded(key), + "Body should be airborne after jump" + ); // Let physics run - body should go up then land back on floor for _ in 0..100 { @@ -1863,10 +1935,10 @@ mod tests { // Kinematic body in center of floor should work normally let mut world = PhysicsWorld::with_config(PhysicsConfig::new(-20.0)); world.add_static_collider(StaticCollider::floor_bounded( - 0.0, // floor at y=0 - 10.0, // half_size_xz - 10.0, // half_size_w - 5.0, // thickness + 0.0, // floor at y=0 + 10.0, // half_size_xz + 10.0, // half_size_w + 5.0, // thickness PhysicsMaterial::CONCRETE, )); @@ -1882,7 +1954,10 @@ mod tests { } // Body should be grounded on floor - assert!(world.body_is_grounded(key), "Body should be grounded on floor center"); + assert!( + world.body_is_grounded(key), + "Body should be grounded on floor center" + ); let final_y = world.body_position(key).unwrap().y; assert!( @@ -1926,7 +2001,9 @@ mod tests { assert!( diff < 0.01, "Fixed timestep should produce consistent results. A: {}, B: {}, diff: {}", - pos_a.y, pos_b.y, diff + pos_a.y, + pos_b.y, + diff ); } @@ -1979,12 +2056,18 @@ mod tests { let body = world.get_body(key).unwrap(); // 1 step at 1/60s with velocity 60 = 1.0 unit moved - assert!((body.position.x - 1.0).abs() < 0.01, - "Should have moved 1 unit after 1 fixed step, got {}", body.position.x); + assert!( + (body.position.x - 1.0).abs() < 0.01, + "Should have moved 1 unit after 1 fixed step, got {}", + body.position.x + ); // Remainder should be ~8.33ms - assert!(world.accumulator > 0.008 && world.accumulator < 0.009, - "Accumulator should carry remainder: {}", world.accumulator); + assert!( + world.accumulator > 0.008 && world.accumulator < 0.009, + "Accumulator should carry remainder: {}", + world.accumulator + ); } #[test] @@ -1998,8 +2081,11 @@ mod tests { // Update with half a fixed step world.update(0.5 / 60.0); let alpha = world.interpolation_alpha(); - assert!((alpha - 0.5).abs() < 0.01, - "Alpha should be ~0.5, got {}", alpha); + assert!( + (alpha - 0.5).abs() < 0.01, + "Alpha should be ~0.5, got {}", + alpha + ); } #[test] @@ -2166,7 +2252,10 @@ mod tests { assert!(!events.is_empty(), "First drain should have events"); let events = world.drain_collision_events(); - assert!(events.is_empty(), "Second drain without step should be empty"); + assert!( + events.is_empty(), + "Second drain without step should be empty" + ); } #[test] @@ -2174,16 +2263,16 @@ mod tests { let mut world = world_with_floor(0.0, 0.0, PhysicsMaterial::CONCRETE); // Sphere penetrating the floor (center at y=0.3, radius=0.5, floor at y=0) - let body = RigidBody4D::new_sphere(Vec4::new(0.0, 0.3, 0.0, 0.0), 0.5) - .with_gravity(false); + let body = RigidBody4D::new_sphere(Vec4::new(0.0, 0.3, 0.0, 0.0), 0.5).with_gravity(false); let key = world.add_body(body); world.step(0.016); let events = world.drain_collision_events(); - let static_events: Vec<_> = events.iter().filter(|e| { - matches!(e.kind, CollisionEventKind::BodyVsStatic { .. }) - }).collect(); + let static_events: Vec<_> = events + .iter() + .filter(|e| matches!(e.kind, CollisionEventKind::BodyVsStatic { .. })) + .collect(); assert_eq!(static_events.len(), 1); if let CollisionEventKind::BodyVsStatic { body, static_index } = static_events[0].kind { @@ -2198,19 +2287,16 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Two overlapping spheres (centers 0.8 apart, combined radii 1.0) - let key_a = world.add_body( - RigidBody4D::new_sphere(Vec4::new(0.0, 0.0, 0.0, 0.0), 0.5) - ); - let key_b = world.add_body( - RigidBody4D::new_sphere(Vec4::new(0.8, 0.0, 0.0, 0.0), 0.5) - ); + let key_a = world.add_body(RigidBody4D::new_sphere(Vec4::new(0.0, 0.0, 0.0, 0.0), 0.5)); + let key_b = world.add_body(RigidBody4D::new_sphere(Vec4::new(0.8, 0.0, 0.0, 0.0), 0.5)); world.step(0.016); let events = world.drain_collision_events(); - let body_events: Vec<_> = events.iter().filter(|e| { - matches!(e.kind, CollisionEventKind::BodyVsBody { .. }) - }).collect(); + let body_events: Vec<_> = events + .iter() + .filter(|e| matches!(e.kind, CollisionEventKind::BodyVsBody { .. })) + .collect(); assert_eq!(body_events.len(), 1); if let CollisionEventKind::BodyVsBody { body_a, body_b } = body_events[0].kind { @@ -2231,25 +2317,35 @@ mod tests { Vec4::ZERO, Vec4::new(2.0, 2.0, 2.0, 2.0), PhysicsMaterial::CONCRETE, - ).with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); + ) + .with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); world.add_static_collider(trigger); // Player body inside the trigger zone let key = world.add_body( RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::player()) - .with_gravity(false) + .with_gravity(false), ); world.step(0.016); let events = world.drain_collision_events(); - let enter_events: Vec<_> = events.iter().filter(|e| { - matches!(e.kind, CollisionEventKind::TriggerEnter { .. }) - }).collect(); + let enter_events: Vec<_> = events + .iter() + .filter(|e| matches!(e.kind, CollisionEventKind::TriggerEnter { .. })) + .collect(); - assert_eq!(enter_events.len(), 1, "Should have exactly one TriggerEnter"); - if let CollisionEventKind::TriggerEnter { body, trigger_index } = enter_events[0].kind { + assert_eq!( + enter_events.len(), + 1, + "Should have exactly one TriggerEnter" + ); + if let CollisionEventKind::TriggerEnter { + body, + trigger_index, + } = enter_events[0].kind + { assert_eq!(body, key); assert_eq!(trigger_index, 0); } @@ -2266,29 +2362,37 @@ mod tests { Vec4::ZERO, Vec4::new(2.0, 2.0, 2.0, 2.0), PhysicsMaterial::CONCRETE, - ).with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); + ) + .with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); world.add_static_collider(trigger); world.add_body( RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::player()) - .with_gravity(false) + .with_gravity(false), ); // First step: Enter world.step(0.016); let events = world.drain_collision_events(); - assert!(events.iter().any(|e| matches!(e.kind, CollisionEventKind::TriggerEnter { .. }))); + assert!(events + .iter() + .any(|e| matches!(e.kind, CollisionEventKind::TriggerEnter { .. }))); // Second step: Stay (body hasn't moved) world.step(0.016); let events = world.drain_collision_events(); - let stay_events: Vec<_> = events.iter().filter(|e| { - matches!(e.kind, CollisionEventKind::TriggerStay { .. }) - }).collect(); + let stay_events: Vec<_> = events + .iter() + .filter(|e| matches!(e.kind, CollisionEventKind::TriggerStay { .. })) + .collect(); - assert_eq!(stay_events.len(), 1, "Should have TriggerStay on second step"); + assert_eq!( + stay_events.len(), + 1, + "Should have TriggerStay on second step" + ); } #[test] @@ -2302,38 +2406,54 @@ mod tests { Vec4::ZERO, Vec4::new(1.0, 1.0, 1.0, 1.0), PhysicsMaterial::CONCRETE, - ).with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); + ) + .with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); world.add_static_collider(trigger); // Body starts inside trigger let key = world.add_body( RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::player()) - .with_gravity(false) + .with_gravity(false), ); // First step: Enter world.step(0.016); let events = world.drain_collision_events(); - assert!(events.iter().any(|e| matches!(e.kind, CollisionEventKind::TriggerEnter { .. })), - "Should have TriggerEnter on first step"); + assert!( + events + .iter() + .any(|e| matches!(e.kind, CollisionEventKind::TriggerEnter { .. })), + "Should have TriggerEnter on first step" + ); // Teleport body far outside trigger - world.get_body_mut(key).unwrap().set_position(Vec4::new(10.0, 0.0, 0.0, 0.0)); + world + .get_body_mut(key) + .unwrap() + .set_position(Vec4::new(10.0, 0.0, 0.0, 0.0)); // Second step: should detect exit world.step(0.016); let events = world.drain_collision_events(); - let exit_events: Vec<_> = events.iter().filter(|e| { - matches!(e.kind, CollisionEventKind::TriggerExit { .. }) - }).collect(); + let exit_events: Vec<_> = events + .iter() + .filter(|e| matches!(e.kind, CollisionEventKind::TriggerExit { .. })) + .collect(); - assert_eq!(exit_events.len(), 1, "Should have TriggerExit after body leaves"); + assert_eq!( + exit_events.len(), + 1, + "Should have TriggerExit after body leaves" + ); if let CollisionEventKind::TriggerExit { body, .. } = exit_events[0].kind { assert_eq!(body, key); } - assert!(exit_events[0].contact.is_none(), "TriggerExit should have no contact"); + assert!( + exit_events[0].contact.is_none(), + "TriggerExit should have no contact" + ); } #[test] @@ -2347,28 +2467,36 @@ mod tests { Vec4::ZERO, Vec4::new(2.0, 2.0, 2.0, 2.0), PhysicsMaterial::CONCRETE, - ).with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); + ) + .with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); world.add_static_collider(trigger); // Enemy body inside trigger (ENEMY layer, not PLAYER) world.add_body( RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::enemy()) - .with_gravity(false) + .with_gravity(false), ); world.step(0.016); let events = world.drain_collision_events(); - let trigger_events: Vec<_> = events.iter().filter(|e| { - matches!(e.kind, - CollisionEventKind::TriggerEnter { .. } | - CollisionEventKind::TriggerStay { .. } | - CollisionEventKind::TriggerExit { .. } - ) - }).collect(); + let trigger_events: Vec<_> = events + .iter() + .filter(|e| { + matches!( + e.kind, + CollisionEventKind::TriggerEnter { .. } + | CollisionEventKind::TriggerStay { .. } + | CollisionEventKind::TriggerExit { .. } + ) + }) + .collect(); - assert!(trigger_events.is_empty(), "Trigger should not detect enemy bodies"); + assert!( + trigger_events.is_empty(), + "Trigger should not detect enemy bodies" + ); } #[test] @@ -2382,7 +2510,8 @@ mod tests { Vec4::ZERO, Vec4::new(2.0, 2.0, 2.0, 2.0), PhysicsMaterial::CONCRETE, - ).with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); + ) + .with_filter(CollisionFilter::trigger(CollisionLayer::PLAYER)); world.add_static_collider(trigger); // Player moving through the trigger should not be stopped @@ -2390,15 +2519,18 @@ mod tests { RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::player()) .with_velocity(Vec4::new(10.0, 0.0, 0.0, 0.0)) - .with_gravity(false) + .with_gravity(false), ); world.step(0.1); let body = world.get_body(key).unwrap(); // Body should have moved freely through trigger (velocity * dt = 1.0) - assert!((body.position.x - 1.0).abs() < 0.01, - "Body should pass through trigger without physical response. Got x={}", body.position.x); + assert!( + (body.position.x - 1.0).abs() < 0.01, + "Body should pass through trigger without physical response. Got x={}", + body.position.x + ); } #[test] @@ -2407,9 +2539,7 @@ mod tests { // generating a BodyVsStatic event every step let mut world = world_with_floor(-20.0, 0.0, PhysicsMaterial::CONCRETE); - world.add_body( - RigidBody4D::new_sphere(Vec4::new(0.0, 0.4, 0.0, 0.0), 0.5) - ); + world.add_body(RigidBody4D::new_sphere(Vec4::new(0.0, 0.4, 0.0, 0.0), 0.5)); // Run multiple steps without draining world.step(0.016); @@ -2418,7 +2548,11 @@ mod tests { let events = world.drain_collision_events(); // Should have accumulated events from all 3 steps (gravity recollides every step) - assert!(events.len() >= 3, "Events should accumulate across steps, got {}", events.len()); + assert!( + events.len() >= 3, + "Events should accumulate across steps, got {}", + events.len() + ); } // ===== Spatial Query Tests ===== @@ -2428,24 +2562,20 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Add body at origin - let key = world.add_body( - RigidBody4D::new_sphere(Vec4::ZERO, 0.5) - .with_gravity(false) - ); + let key = world.add_body(RigidBody4D::new_sphere(Vec4::ZERO, 0.5).with_gravity(false)); // Query with center at (5,0,0,0), radius 10 - should find the body - let results = world.query_sphere( - Vec4::new(5.0, 0.0, 0.0, 0.0), - 10.0, - CollisionLayer::ALL, - ); + let results = world.query_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 10.0, CollisionLayer::ALL); assert_eq!(results.len(), 1, "Should find one body in range"); match results[0].target { RayTarget::Body(found_key) => assert_eq!(found_key, key), _ => panic!("Expected Body target"), } - assert!((results[0].distance - 5.0).abs() < 0.001, "Distance should be 5.0"); + assert!( + (results[0].distance - 5.0).abs() < 0.001, + "Distance should be 5.0" + ); } #[test] @@ -2453,17 +2583,10 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Add body at origin - world.add_body( - RigidBody4D::new_sphere(Vec4::ZERO, 0.5) - .with_gravity(false) - ); + world.add_body(RigidBody4D::new_sphere(Vec4::ZERO, 0.5).with_gravity(false)); // Query with center at (20,0,0,0), radius 5 - too far away - let results = world.query_sphere( - Vec4::new(20.0, 0.0, 0.0, 0.0), - 5.0, - CollisionLayer::ALL, - ); + let results = world.query_sphere(Vec4::new(20.0, 0.0, 0.0, 0.0), 5.0, CollisionLayer::ALL); assert!(results.is_empty(), "Should not find body out of range"); } @@ -2478,17 +2601,16 @@ mod tests { world.add_body( RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::enemy()) - .with_gravity(false) + .with_gravity(false), ); // Query with PLAYER layer mask - should not find the ENEMY body - let results = world.query_sphere( - Vec4::ZERO, - 10.0, - CollisionLayer::PLAYER, - ); + let results = world.query_sphere(Vec4::ZERO, 10.0, CollisionLayer::PLAYER); - assert!(results.is_empty(), "Should not find body with non-matching layer"); + assert!( + results.is_empty(), + "Should not find body with non-matching layer" + ); } #[test] @@ -2501,15 +2623,11 @@ mod tests { world.add_body( RigidBody4D::new_sphere(Vec4::ZERO, 0.5) .with_filter(CollisionFilter::enemy()) - .with_gravity(false) + .with_gravity(false), ); // Query with ENEMY layer mask - should find the body - let results = world.query_sphere( - Vec4::ZERO, - 10.0, - CollisionLayer::ENEMY, - ); + let results = world.query_sphere(Vec4::ZERO, 10.0, CollisionLayer::ENEMY); assert_eq!(results.len(), 1, "Should find body with matching layer"); } @@ -2520,28 +2638,30 @@ mod tests { // Add bodies at different distances world.add_body( - RigidBody4D::new_sphere(Vec4::new(10.0, 0.0, 0.0, 0.0), 0.5) - .with_gravity(false) + RigidBody4D::new_sphere(Vec4::new(10.0, 0.0, 0.0, 0.0), 0.5).with_gravity(false), ); world.add_body( - RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 0.5) - .with_gravity(false) + RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 0.5).with_gravity(false), ); world.add_body( - RigidBody4D::new_sphere(Vec4::new(15.0, 0.0, 0.0, 0.0), 0.5) - .with_gravity(false) + RigidBody4D::new_sphere(Vec4::new(15.0, 0.0, 0.0, 0.0), 0.5).with_gravity(false), ); - let results = world.query_sphere( - Vec4::ZERO, - 20.0, - CollisionLayer::ALL, - ); + let results = world.query_sphere(Vec4::ZERO, 20.0, CollisionLayer::ALL); assert_eq!(results.len(), 3); - assert!((results[0].distance - 5.0).abs() < 0.001, "Nearest should be at distance 5"); - assert!((results[1].distance - 10.0).abs() < 0.001, "Second should be at distance 10"); - assert!((results[2].distance - 15.0).abs() < 0.001, "Farthest should be at distance 15"); + assert!( + (results[0].distance - 5.0).abs() < 0.001, + "Nearest should be at distance 5" + ); + assert!( + (results[1].distance - 10.0).abs() < 0.001, + "Second should be at distance 10" + ); + assert!( + (results[2].distance - 15.0).abs() < 0.001, + "Farthest should be at distance 15" + ); } #[test] @@ -2550,23 +2670,24 @@ mod tests { // Add body at distance 5 from origin world.add_body( - RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 0.5) - .with_gravity(false) + RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 0.5).with_gravity(false), ); // Query with radius 10, with_falloff = true - let results = world.query_area_effect( - Vec4::ZERO, - 10.0, - CollisionLayer::ALL, - true, - ); + let results = world.query_area_effect(Vec4::ZERO, 10.0, CollisionLayer::ALL, true); assert_eq!(results.len(), 1); // Falloff = 1.0 - (5.0 / 10.0) = 0.5 - assert!((results[0].falloff - 0.5).abs() < 0.001, "Falloff should be 0.5, got {}", results[0].falloff); + assert!( + (results[0].falloff - 0.5).abs() < 0.001, + "Falloff should be 0.5, got {}", + results[0].falloff + ); // Direction should point from origin toward body (positive X) - assert!(results[0].direction.x > 0.9, "Direction should be positive X"); + assert!( + results[0].direction.x > 0.9, + "Direction should be positive X" + ); } #[test] @@ -2575,21 +2696,18 @@ mod tests { // Add body at distance 5 from origin world.add_body( - RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 0.5) - .with_gravity(false) + RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 0.5).with_gravity(false), ); // Query with radius 10, with_falloff = false - let results = world.query_area_effect( - Vec4::ZERO, - 10.0, - CollisionLayer::ALL, - false, - ); + let results = world.query_area_effect(Vec4::ZERO, 10.0, CollisionLayer::ALL, false); assert_eq!(results.len(), 1); // Falloff should be 1.0 when with_falloff is false - assert!((results[0].falloff - 1.0).abs() < 0.001, "Falloff should be 1.0 without falloff"); + assert!( + (results[0].falloff - 1.0).abs() < 0.001, + "Falloff should be 1.0 without falloff" + ); } #[test] @@ -2597,23 +2715,22 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Add body at exact center - world.add_body( - RigidBody4D::new_sphere(Vec4::ZERO, 0.5) - .with_gravity(false) - ); + world.add_body(RigidBody4D::new_sphere(Vec4::ZERO, 0.5).with_gravity(false)); - let results = world.query_area_effect( - Vec4::ZERO, - 10.0, - CollisionLayer::ALL, - true, - ); + let results = world.query_area_effect(Vec4::ZERO, 10.0, CollisionLayer::ALL, true); assert_eq!(results.len(), 1); // Falloff should be 1.0 at center - assert!((results[0].falloff - 1.0).abs() < 0.001, "Falloff at center should be 1.0"); + assert!( + (results[0].falloff - 1.0).abs() < 0.001, + "Falloff at center should be 1.0" + ); // Direction should be Y (fallback for zero distance) - assert_eq!(results[0].direction, Vec4::Y, "Direction should be Y at zero distance"); + assert_eq!( + results[0].direction, + Vec4::Y, + "Direction should be Y at zero distance" + ); } #[test] @@ -2635,13 +2752,11 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Add a wall between the two points - world.add_static_collider( - StaticCollider::aabb( - Vec4::new(5.0, 0.0, 0.0, 0.0), - Vec4::new(1.0, 10.0, 10.0, 10.0), - PhysicsMaterial::CONCRETE, - ) - ); + world.add_static_collider(StaticCollider::aabb( + Vec4::new(5.0, 0.0, 0.0, 0.0), + Vec4::new(1.0, 10.0, 10.0, 10.0), + PhysicsMaterial::CONCRETE, + )); let blocked = world.line_of_sight( Vec4::ZERO, @@ -2649,7 +2764,10 @@ mod tests { CollisionLayer::STATIC, ); - assert!(!blocked, "Line of sight should be blocked by static collider"); + assert!( + !blocked, + "Line of sight should be blocked by static collider" + ); } #[test] @@ -2662,7 +2780,7 @@ mod tests { world.add_body( RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 1.0) .with_filter(CollisionFilter::enemy()) - .with_gravity(false) + .with_gravity(false), ); // Check line of sight blocking only STATIC layer @@ -2672,7 +2790,10 @@ mod tests { CollisionLayer::STATIC, ); - assert!(clear, "Line of sight should be clear when obstacle is on different layer"); + assert!( + clear, + "Line of sight should be clear when obstacle is on different layer" + ); } #[test] @@ -2685,7 +2806,7 @@ mod tests { world.add_body( RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 1.0) .with_filter(CollisionFilter::enemy()) - .with_gravity(false) + .with_gravity(false), ); // Check line of sight blocking ENEMY layer @@ -2703,11 +2824,7 @@ mod tests { let world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Same start and end point - let clear = world.line_of_sight( - Vec4::ZERO, - Vec4::ZERO, - CollisionLayer::ALL, - ); + let clear = world.line_of_sight(Vec4::ZERO, Vec4::ZERO, CollisionLayer::ALL); assert!(clear, "Line of sight to same point should be clear"); } @@ -2717,25 +2834,22 @@ mod tests { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Add a static collider - world.add_static_collider( - StaticCollider::aabb( - Vec4::new(3.0, 0.0, 0.0, 0.0), - Vec4::new(1.0, 1.0, 1.0, 1.0), - PhysicsMaterial::CONCRETE, - ) - ); + world.add_static_collider(StaticCollider::aabb( + Vec4::new(3.0, 0.0, 0.0, 0.0), + Vec4::new(1.0, 1.0, 1.0, 1.0), + PhysicsMaterial::CONCRETE, + )); - let results = world.query_sphere( - Vec4::ZERO, - 10.0, - CollisionLayer::STATIC, - ); + let results = world.query_sphere(Vec4::ZERO, 10.0, CollisionLayer::STATIC); assert_eq!(results.len(), 1, "Should find static collider"); match results[0].target { RayTarget::Static(idx) => assert_eq!(idx, 0), _ => panic!("Expected Static target"), } - assert!((results[0].distance - 3.0).abs() < 0.001, "Distance should be 3.0"); + assert!( + (results[0].distance - 3.0).abs() < 0.001, + "Distance should be 3.0" + ); } } diff --git a/crates/rust4d_physics/tests/edge_cases.rs b/crates/rust4d_physics/tests/edge_cases.rs index 81bc692..4661b3e 100644 --- a/crates/rust4d_physics/tests/edge_cases.rs +++ b/crates/rust4d_physics/tests/edge_cases.rs @@ -94,7 +94,10 @@ fn stale_key_does_not_alias_new_body() { let key2 = world.add_body(RigidBody4D::new_sphere(Vec4::new(2.0, 0.0, 0.0, 0.0), 0.5)); // key1 is stale; key2 is valid - assert!(world.get_body(key1).is_none(), "Stale key must not resolve to new body"); + assert!( + world.get_body(key1).is_none(), + "Stale key must not resolve to new body" + ); assert!(world.get_body(key2).is_some()); assert_ne!(key1, key2); } @@ -234,10 +237,7 @@ fn zero_movement_then_step_no_drift() { #[test] fn raycast_very_large_max_distance() { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); - let key = world.add_body(RigidBody4D::new_sphere( - Vec4::new(5.0, 0.0, 0.0, 0.0), - 1.0, - )); + let key = world.add_body(RigidBody4D::new_sphere(Vec4::new(5.0, 0.0, 0.0, 0.0), 1.0)); let ray = Ray4D::new(Vec4::ZERO, Vec4::X); let hits = world.raycast(&ray, 1e12, CollisionLayer::ALL); @@ -257,20 +257,14 @@ fn raycast_very_large_max_distance() { #[test] fn raycast_zero_max_distance() { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); - world.add_body(RigidBody4D::new_sphere( - Vec4::new(0.5, 0.0, 0.0, 0.0), - 1.0, - )); + world.add_body(RigidBody4D::new_sphere(Vec4::new(0.5, 0.0, 0.0, 0.0), 1.0)); let ray = Ray4D::new(Vec4::ZERO, Vec4::X); // The ray origin is inside the sphere, so the exit point distance is > 0. // With max_distance = 0, it should miss because hit.distance > 0. let hits = world.raycast(&ray, 0.0, CollisionLayer::ALL); // Even if origin is inside, exit point distance > 0, so with max_distance=0 nothing should match - assert!( - hits.is_empty(), - "Zero max_distance should yield no hits" - ); + assert!(hits.is_empty(), "Zero max_distance should yield no hits"); } /// Ray originating inside a sphere should hit the exit point. @@ -355,10 +349,7 @@ fn raycast_very_small_sphere_off_axis_misses() { fn raycast_nearest_large_max_distance() { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); - let near = world.add_body(RigidBody4D::new_sphere( - Vec4::new(3.0, 0.0, 0.0, 0.0), - 0.5, - )); + let near = world.add_body(RigidBody4D::new_sphere(Vec4::new(3.0, 0.0, 0.0, 0.0), 0.5)); world.add_body(RigidBody4D::new_sphere( Vec4::new(100.0, 0.0, 0.0, 0.0), 0.5, @@ -384,10 +375,7 @@ fn raycast_hits_body_and_static_sorted() { world.add_static_collider(StaticCollider::floor(0.0, PhysicsMaterial::CONCRETE)); // Body sphere at (0, 5, 0, 0) -- above floor - world.add_body(RigidBody4D::new_sphere( - Vec4::new(0.0, 5.0, 0.0, 0.0), - 1.0, - )); + world.add_body(RigidBody4D::new_sphere(Vec4::new(0.0, 5.0, 0.0, 0.0), 1.0)); // Ray shooting downward from high above let ray = Ray4D::new(Vec4::new(0.0, 20.0, 0.0, 0.0), -Vec4::Y); @@ -410,10 +398,8 @@ fn raycast_hits_body_and_static_sorted() { fn zero_mass_body_collision_no_panic() { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); - let body_a = RigidBody4D::new_sphere(Vec4::new(0.0, 0.0, 0.0, 0.0), 0.5) - .with_mass(0.0); - let body_b = RigidBody4D::new_sphere(Vec4::new(0.8, 0.0, 0.0, 0.0), 0.5) - .with_mass(0.0); + let body_a = RigidBody4D::new_sphere(Vec4::new(0.0, 0.0, 0.0, 0.0), 0.5).with_mass(0.0); + let body_b = RigidBody4D::new_sphere(Vec4::new(0.8, 0.0, 0.0, 0.0), 0.5).with_mass(0.0); world.add_body(body_a); world.add_body(body_b); @@ -443,7 +429,10 @@ fn zero_mass_vs_normal_mass_no_panic() { // Neither body should have NaN position let pos_zero = world.body_position(key_zero).unwrap(); let pos_normal = world.body_position(key_normal).unwrap(); - assert!(!pos_zero.x.is_nan(), "Zero-mass body should not produce NaN"); + assert!( + !pos_zero.x.is_nan(), + "Zero-mass body should not produce NaN" + ); assert!( !pos_normal.x.is_nan(), "Normal body should not produce NaN from zero-mass collision" @@ -498,10 +487,7 @@ fn multiple_triggers_on_same_body() { let indices: Vec = enter_events .iter() .map(|e| { - if let CollisionEventKind::TriggerEnter { - trigger_index, .. - } = e.kind - { + if let CollisionEventKind::TriggerEnter { trigger_index, .. } = e.kind { trigger_index } else { unreachable!() @@ -642,18 +628,9 @@ fn collision_events_report_correct_keys() { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); // Three non-overlapping bodies -- only first two overlap - let key_a = world.add_body(RigidBody4D::new_sphere( - Vec4::new(0.0, 0.0, 0.0, 0.0), - 0.5, - )); - let key_b = world.add_body(RigidBody4D::new_sphere( - Vec4::new(0.8, 0.0, 0.0, 0.0), - 0.5, - )); - let key_c = world.add_body(RigidBody4D::new_sphere( - Vec4::new(10.0, 0.0, 0.0, 0.0), - 0.5, - )); + let key_a = world.add_body(RigidBody4D::new_sphere(Vec4::new(0.0, 0.0, 0.0, 0.0), 0.5)); + let key_b = world.add_body(RigidBody4D::new_sphere(Vec4::new(0.8, 0.0, 0.0, 0.0), 0.5)); + let key_c = world.add_body(RigidBody4D::new_sphere(Vec4::new(10.0, 0.0, 0.0, 0.0), 0.5)); world.step(0.016); let events = world.drain_collision_events(); @@ -684,10 +661,7 @@ fn removed_body_during_simulation_no_panic() { let mut world = PhysicsWorld::with_config(PhysicsConfig::new(0.0)); let key_a = world.add_body(RigidBody4D::new_sphere(Vec4::ZERO, 0.5)); - let key_b = world.add_body(RigidBody4D::new_sphere( - Vec4::new(0.8, 0.0, 0.0, 0.0), - 0.5, - )); + let key_b = world.add_body(RigidBody4D::new_sphere(Vec4::new(0.8, 0.0, 0.0, 0.0), 0.5)); // Step once to establish collision world.step(0.016); @@ -748,7 +722,9 @@ fn raycast_empty_world() { let ray = Ray4D::new(Vec4::ZERO, Vec4::X); let hits = world.raycast(&ray, 100.0, CollisionLayer::ALL); assert!(hits.is_empty()); - assert!(world.raycast_nearest(&ray, 100.0, CollisionLayer::ALL).is_none()); + assert!(world + .raycast_nearest(&ray, 100.0, CollisionLayer::ALL) + .is_none()); } /// A dynamic body with gravity disabled colliding with another should still produce events. diff --git a/crates/rust4d_render/src/camera4d.rs b/crates/rust4d_render/src/camera4d.rs index df9913c..e87bbe9 100644 --- a/crates/rust4d_render/src/camera4d.rs +++ b/crates/rust4d_render/src/camera4d.rs @@ -9,8 +9,8 @@ //! This design ensures intuitive movement behavior: walking forward stays //! horizontal regardless of 4D rotation state. -use rust4d_math::{Vec4, Rotor4, RotationPlane, mat4}; use rust4d_input::CameraControl; +use rust4d_math::{mat4, RotationPlane, Rotor4, Vec4}; /// 4D Camera using Engine4D-style architecture /// @@ -157,7 +157,8 @@ impl Camera4D { /// /// This matches Engine4D's `accel = camMatrix * accel` fn move_camera(&mut self, forward: f32, right: f32, up: f32, ana: f32) { - if forward.abs() < 0.0001 && right.abs() < 0.0001 && up.abs() < 0.0001 && ana.abs() < 0.0001 { + if forward.abs() < 0.0001 && right.abs() < 0.0001 && up.abs() < 0.0001 && ana.abs() < 0.0001 + { return; } @@ -318,7 +319,11 @@ mod tests { // Up should still be purely +Y (or close to it) let up = cam.up(); - assert!(up.y > 0.99, "Up should still be +Y after 4D rotation, got {:?}", up); + assert!( + up.y > 0.99, + "Up should still be +Y after 4D rotation, got {:?}", + up + ); assert!(up.x.abs() < EPSILON, "Up.x should be ~0, got {}", up.x); assert!(up.z.abs() < EPSILON, "Up.z should be ~0, got {}", up.z); assert!(up.w.abs() < EPSILON, "Up.w should be ~0, got {}", up.w); @@ -335,10 +340,18 @@ mod tests { let fwd = cam.forward(); // Up should be tilted (Y component < 1) - assert!(up.y < 0.95, "Up should be tilted after pitch, got up.y={}", up.y); + assert!( + up.y < 0.95, + "Up should be tilted after pitch, got up.y={}", + up.y + ); // Forward should point up (positive Y) - assert!(fwd.y > 0.5, "Forward should point up after pitch, got fwd.y={}", fwd.y); + assert!( + fwd.y > 0.5, + "Forward should point up after pitch, got fwd.y={}", + fwd.y + ); } #[test] @@ -355,7 +368,11 @@ mod tests { println!("Forward after 90° yaw: {:?}", fwd); // Y should still be 0 (yaw doesn't affect pitch) - assert!(fwd.y.abs() < EPSILON, "Forward.y should be ~0 after pure yaw, got {}", fwd.y); + assert!( + fwd.y.abs() < EPSILON, + "Forward.y should be ~0 after pure yaw, got {}", + fwd.y + ); } #[test] @@ -372,8 +389,11 @@ mod tests { cam.move_local_xz(1.0, 0.0); // Y should be unchanged (movement stays horizontal!) - assert!(cam.position.y.abs() < EPSILON, - "Forward movement should not affect Y after 4D rotation, got Y={}", cam.position.y); + assert!( + cam.position.y.abs() < EPSILON, + "Forward movement should not affect Y after 4D rotation, got Y={}", + cam.position.y + ); } #[test] @@ -388,8 +408,11 @@ mod tests { cam.move_local_xz(1.0, 0.0); // Y should be positive (moving up because we're pitched up) - assert!(cam.position.y > 0.5, - "Forward movement should affect Y when pitched, got Y={}", cam.position.y); + assert!( + cam.position.y > 0.5, + "Forward movement should affect Y when pitched, got Y={}", + cam.position.y + ); } #[test] @@ -407,8 +430,16 @@ mod tests { let fwd = cam.forward(); let up = cam.up(); - assert!(approx_eq(fwd.z, -1.0), "Forward should be -Z after reset, got {:?}", fwd); - assert!(approx_eq(up.y, 1.0), "Up should be +Y after reset, got {:?}", up); + assert!( + approx_eq(fwd.z, -1.0), + "Forward should be -Z after reset, got {:?}", + fwd + ); + assert!( + approx_eq(up.y, 1.0), + "Up should be +Y after reset, got {:?}", + up + ); } #[test] @@ -419,8 +450,11 @@ mod tests { cam.rotate_3d(0.0, 10.0); // Pitch should be clamped to ~89° (default pitch limit) - assert!(cam.pitch.abs() <= Camera4D::DEFAULT_PITCH_LIMIT + 0.001, - "Pitch should be clamped, got {}", cam.pitch); + assert!( + cam.pitch.abs() <= Camera4D::DEFAULT_PITCH_LIMIT + 0.001, + "Pitch should be clamped, got {}", + cam.pitch + ); } #[test] @@ -438,17 +472,39 @@ mod tests { let ana = cam.ana(); // Check vectors are unit length - assert!((fwd.length() - 1.0).abs() < EPSILON, "Forward not unit: {}", fwd.length()); - assert!((right.length() - 1.0).abs() < EPSILON, "Right not unit: {}", right.length()); - assert!((up.length() - 1.0).abs() < EPSILON, "Up not unit: {}", up.length()); - assert!((ana.length() - 1.0).abs() < EPSILON, "Ana not unit: {}", ana.length()); + assert!( + (fwd.length() - 1.0).abs() < EPSILON, + "Forward not unit: {}", + fwd.length() + ); + assert!( + (right.length() - 1.0).abs() < EPSILON, + "Right not unit: {}", + right.length() + ); + assert!( + (up.length() - 1.0).abs() < EPSILON, + "Up not unit: {}", + up.length() + ); + assert!( + (ana.length() - 1.0).abs() < EPSILON, + "Ana not unit: {}", + ana.length() + ); // Check orthogonality - assert!(fwd.dot(right).abs() < EPSILON, "Fwd not orthogonal to Right"); + assert!( + fwd.dot(right).abs() < EPSILON, + "Fwd not orthogonal to Right" + ); assert!(fwd.dot(up).abs() < EPSILON, "Fwd not orthogonal to Up"); assert!(fwd.dot(ana).abs() < EPSILON, "Fwd not orthogonal to Ana"); assert!(right.dot(up).abs() < EPSILON, "Right not orthogonal to Up"); - assert!(right.dot(ana).abs() < EPSILON, "Right not orthogonal to Ana"); + assert!( + right.dot(ana).abs() < EPSILON, + "Right not orthogonal to Ana" + ); assert!(up.dot(ana).abs() < EPSILON, "Up not orthogonal to Ana"); } @@ -465,7 +521,11 @@ mod tests { // Up should still be +Y (4D rotation + yaw both preserve Y) let up = cam.up(); - assert!(up.y > 0.99, "Up should be +Y after 4D rotation + yaw, got {:?}", up); + assert!( + up.y > 0.99, + "Up should be +Y after 4D rotation + yaw, got {:?}", + up + ); } #[test] @@ -473,20 +533,26 @@ mod tests { let mut cam = Camera4D::new(); // Apply multiple 4D rotations - cam.rotate_w(FRAC_PI_2); // Look into W + cam.rotate_w(FRAC_PI_2); // Look into W cam.rotate_xw(FRAC_PI_4); // Tilt in XW // Y axis should still be preserved let up = cam.up(); - assert!(up.y > 0.99, "Up should be +Y after combined 4D rotations, got {:?}", up); + assert!( + up.y > 0.99, + "Up should be +Y after combined 4D rotations, got {:?}", + up + ); // But forward should be in a different direction let fwd = cam.forward(); println!("Forward after combined 4D rotations: {:?}", fwd); // Forward should have W component (looking into 4D) - assert!(fwd.w.abs() > 0.1 || fwd.z.abs() > 0.1, - "Forward should be affected by 4D rotation"); + assert!( + fwd.w.abs() > 0.1 || fwd.z.abs() > 0.1, + "Forward should be affected by 4D rotation" + ); } #[test] @@ -496,7 +562,10 @@ mod tests { // Without any rotation, W movement should go in +W cam.move_w(1.0); - assert!(cam.position.w > 0.9, "W movement should go in +W by default"); + assert!( + cam.position.w > 0.9, + "W movement should go in +W by default" + ); // Reset cam.reset(); @@ -508,8 +577,11 @@ mod tests { // W axis is now rotated, so movement goes in a different direction // But Y should still be unchanged - assert!(cam.position.y.abs() < EPSILON, - "W movement should not affect Y, got Y={}", cam.position.y); + assert!( + cam.position.y.abs() < EPSILON, + "W movement should not affect Y, got Y={}", + cam.position.y + ); } #[test] @@ -518,34 +590,56 @@ mod tests { // Initial ana() should point in +W direction let ana_before = cam.ana(); - eprintln!("ana_before: ({:.4}, {:.4}, {:.4}, {:.4})", - ana_before.x, ana_before.y, ana_before.z, ana_before.w); - assert!(ana_before.w > 0.9, - "Initial ana should be ~(0,0,0,1), got W={}", ana_before.w); - assert!(ana_before.x.abs() < 0.1, - "Initial ana X should be ~0, got {}", ana_before.x); + eprintln!( + "ana_before: ({:.4}, {:.4}, {:.4}, {:.4})", + ana_before.x, ana_before.y, ana_before.z, ana_before.w + ); + assert!( + ana_before.w > 0.9, + "Initial ana should be ~(0,0,0,1), got W={}", + ana_before.w + ); + assert!( + ana_before.x.abs() < 0.1, + "Initial ana X should be ~0, got {}", + ana_before.x + ); // After 90° rotation in the ZW plane (via rotate_w), ana should change cam.rotate_w(FRAC_PI_2); let ana_after = cam.ana(); - eprintln!("ana_after: ({:.4}, {:.4}, {:.4}, {:.4})", - ana_after.x, ana_after.y, ana_after.z, ana_after.w); + eprintln!( + "ana_after: ({:.4}, {:.4}, {:.4}, {:.4})", + ana_after.x, ana_after.y, ana_after.z, ana_after.w + ); // After 90° ZW rotation, the W axis should point along Z - assert!(ana_after.w.abs() < 0.1, - "After 90° rotation, W component should be ~0, got {}", ana_after.w); - assert!(ana_after.z.abs() > 0.9, - "After 90° rotation, Z component should be ~±1, got {}", ana_after.z); + assert!( + ana_after.w.abs() < 0.1, + "After 90° rotation, W component should be ~0, got {}", + ana_after.w + ); + assert!( + ana_after.z.abs() > 0.9, + "After 90° rotation, Z component should be ~±1, got {}", + ana_after.z + ); // And forward (-Z) should now look into the 4th dimension let fwd = cam.forward(); - assert!(fwd.w.abs() > 0.9, - "After 90° ZW rotation, forward should point along W, got {:?}", fwd); + assert!( + fwd.w.abs() > 0.9, + "After 90° ZW rotation, forward should point along W, got {:?}", + fwd + ); // Y should never be affected by rotate_w (that's the point of SkipY) - assert!(ana_after.y.abs() < 0.1, - "Y should never be affected by rotate_w, got {}", ana_after.y); + assert!( + ana_after.y.abs() < 0.1, + "Y should never be affected by rotate_w, got {}", + ana_after.y + ); } #[test] @@ -561,33 +655,54 @@ mod tests { // === Before any rotation === let ana = cam.ana(); let ana_xzw = project_ana(ana); - eprintln!("Before rotation: ana=({:.2},{:.2},{:.2},{:.2}) projected=({:.2},{:.2},{:.2},{:.2})", - ana.x, ana.y, ana.z, ana.w, ana_xzw.x, ana_xzw.y, ana_xzw.z, ana_xzw.w); + eprintln!( + "Before rotation: ana=({:.2},{:.2},{:.2},{:.2}) projected=({:.2},{:.2},{:.2},{:.2})", + ana.x, ana.y, ana.z, ana.w, ana_xzw.x, ana_xzw.y, ana_xzw.z, ana_xzw.w + ); // W movement should go in +W direction assert!(ana_xzw.w > 0.9, "Before rotation, W movement should be +W"); - assert!(ana_xzw.x.abs() < 0.1, "Before rotation, X component should be ~0"); + assert!( + ana_xzw.x.abs() < 0.1, + "Before rotation, X component should be ~0" + ); // === After 90° rotation === cam.rotate_w(FRAC_PI_2); let ana = cam.ana(); let ana_xzw = project_ana(ana); - eprintln!("After 90° rotation: ana=({:.2},{:.2},{:.2},{:.2}) projected=({:.2},{:.2},{:.2},{:.2})", - ana.x, ana.y, ana.z, ana.w, ana_xzw.x, ana_xzw.y, ana_xzw.z, ana_xzw.w); + eprintln!( + "After 90° rotation: ana=({:.2},{:.2},{:.2},{:.2}) projected=({:.2},{:.2},{:.2},{:.2})", + ana.x, ana.y, ana.z, ana.w, ana_xzw.x, ana_xzw.y, ana_xzw.z, ana_xzw.w + ); // W movement should now go in +Z or -Z direction (ZW rotation) - assert!(ana_xzw.w.abs() < 0.1, "After 90° rotation, W movement should NOT go in W direction"); - assert!(ana_xzw.z.abs() > 0.9, "After 90° rotation, W movement should go in Z direction"); + assert!( + ana_xzw.w.abs() < 0.1, + "After 90° rotation, W movement should NOT go in W direction" + ); + assert!( + ana_xzw.z.abs() > 0.9, + "After 90° rotation, W movement should go in Z direction" + ); // Verify: pressing Q after rotation affects Z, not W let w_input = 1.0; let move_from_w = ana_xzw * w_input; - eprintln!("Movement from Q key: ({:.2},{:.2},{:.2},{:.2})", - move_from_w.x, move_from_w.y, move_from_w.z, move_from_w.w); + eprintln!( + "Movement from Q key: ({:.2},{:.2},{:.2},{:.2})", + move_from_w.x, move_from_w.y, move_from_w.z, move_from_w.w + ); - assert!(move_from_w.z.abs() > 0.9, "Q key should affect Z position after rotation"); - assert!(move_from_w.w.abs() < 0.1, "Q key should NOT affect W position after rotation"); + assert!( + move_from_w.z.abs() > 0.9, + "Q key should affect Z position after rotation" + ); + assert!( + move_from_w.w.abs() < 0.1, + "Q key should NOT affect W position after rotation" + ); } #[test] @@ -615,8 +730,8 @@ mod tests { assert!( (slice_w_after - slice_w_before).abs() < EPSILON, "slice_w changed from {} to {} during movement! This would cause morphing.", - slice_w_before, slice_w_after + slice_w_before, + slice_w_after ); } - } diff --git a/crates/rust4d_render/src/egui_overlay/context.rs b/crates/rust4d_render/src/egui_overlay/context.rs index b199175..0923313 100644 --- a/crates/rust4d_render/src/egui_overlay/context.rs +++ b/crates/rust4d_render/src/egui_overlay/context.rs @@ -56,12 +56,21 @@ impl<'a> HudContext<'a> { let pos = Pos2::new(pos[0], pos[1]); let color = rgba_to_color32(color); - Area::new(egui::Id::new(("hud_text", pos.x as i32, pos.y as i32, text))) - .order(Order::Foreground) - .fixed_pos(pos) - .show(self.ctx, |ui| { - ui.label(RichText::new(text).font(FontId::proportional(size)).color(color)); - }); + Area::new(egui::Id::new(( + "hud_text", + pos.x as i32, + pos.y as i32, + text, + ))) + .order(Order::Foreground) + .fixed_pos(pos) + .show(self.ctx, |ui| { + ui.label( + RichText::new(text) + .font(FontId::proportional(size)) + .color(color), + ); + }); } /// Draw text centered at screen position @@ -76,13 +85,22 @@ impl<'a> HudContext<'a> { let pos = Pos2::new(pos[0], pos[1]); let color = rgba_to_color32(color); - Area::new(egui::Id::new(("hud_text_centered", pos.x as i32, pos.y as i32, text))) - .order(Order::Foreground) - .anchor(Align2::CENTER_CENTER, Vec2::ZERO) - .fixed_pos(pos) - .show(self.ctx, |ui| { - ui.label(RichText::new(text).font(FontId::proportional(size)).color(color)); - }); + Area::new(egui::Id::new(( + "hud_text_centered", + pos.x as i32, + pos.y as i32, + text, + ))) + .order(Order::Foreground) + .anchor(Align2::CENTER_CENTER, Vec2::ZERO) + .fixed_pos(pos) + .show(self.ctx, |ui| { + ui.label( + RichText::new(text) + .font(FontId::proportional(size)) + .color(color), + ); + }); } /// Draw a filled rectangle @@ -116,14 +134,23 @@ impl<'a> HudContext<'a> { pub fn rect_outline(&self, pos: [f32; 2], size: [f32; 2], color: [f32; 4], stroke_width: f32) { let color = rgba_to_color32(color); - Area::new(egui::Id::new(("hud_rect_outline", pos[0] as i32, pos[1] as i32))) - .order(Order::Foreground) - .fixed_pos(Pos2::new(pos[0], pos[1])) - .show(self.ctx, |ui| { - let (response, painter) = - ui.allocate_painter(Vec2::new(size[0], size[1]), Sense::hover()); - painter.rect_stroke(response.rect, CornerRadius::ZERO, Stroke::new(stroke_width, color), StrokeKind::Outside); - }); + Area::new(egui::Id::new(( + "hud_rect_outline", + pos[0] as i32, + pos[1] as i32, + ))) + .order(Order::Foreground) + .fixed_pos(Pos2::new(pos[0], pos[1])) + .show(self.ctx, |ui| { + let (response, painter) = + ui.allocate_painter(Vec2::new(size[0], size[1]), Sense::hover()); + painter.rect_stroke( + response.rect, + CornerRadius::ZERO, + Stroke::new(stroke_width, color), + StrokeKind::Outside, + ); + }); } /// Draw a progress bar @@ -147,22 +174,26 @@ impl<'a> HudContext<'a> { let bg_color = rgba_to_color32(bg_color); let fill_color = rgba_to_color32(fill_color); - Area::new(egui::Id::new(("hud_progress", pos[0] as i32, pos[1] as i32))) - .order(Order::Foreground) - .fixed_pos(Pos2::new(pos[0], pos[1])) - .show(self.ctx, |ui| { - let (response, painter) = - ui.allocate_painter(Vec2::new(size[0], size[1]), Sense::hover()); - let rect = response.rect; + Area::new(egui::Id::new(( + "hud_progress", + pos[0] as i32, + pos[1] as i32, + ))) + .order(Order::Foreground) + .fixed_pos(Pos2::new(pos[0], pos[1])) + .show(self.ctx, |ui| { + let (response, painter) = + ui.allocate_painter(Vec2::new(size[0], size[1]), Sense::hover()); + let rect = response.rect; - // Draw background - painter.rect_filled(rect, CornerRadius::ZERO, bg_color); + // Draw background + painter.rect_filled(rect, CornerRadius::ZERO, bg_color); - // Draw fill - let fill_width = rect.width() * progress; - let fill_rect = Rect::from_min_size(rect.min, Vec2::new(fill_width, rect.height())); - painter.rect_filled(fill_rect, CornerRadius::ZERO, fill_color); - }); + // Draw fill + let fill_width = rect.width() * progress; + let fill_rect = Rect::from_min_size(rect.min, Vec2::new(fill_width, rect.height())); + painter.rect_filled(fill_rect, CornerRadius::ZERO, fill_color); + }); } /// Flash the screen (for damage, pickups, etc) @@ -180,8 +211,7 @@ impl<'a> HudContext<'a> { .order(Order::Background) // Behind other HUD elements but over scene .fixed_pos(Pos2::ZERO) .show(self.ctx, |ui| { - let (_response, painter) = - ui.allocate_painter(screen.size(), Sense::hover()); + let (_response, painter) = ui.allocate_painter(screen.size(), Sense::hover()); painter.rect_filled(screen, CornerRadius::ZERO, color); }); } @@ -276,7 +306,9 @@ mod tests { assert!( (original[i] - back[i]).abs() < 0.02, "Channel {}: original={}, back={}", - i, original[i], back[i] + i, + original[i], + back[i] ); } } diff --git a/crates/rust4d_render/src/egui_overlay/mod.rs b/crates/rust4d_render/src/egui_overlay/mod.rs index fb757fd..0a95d3c 100644 --- a/crates/rust4d_render/src/egui_overlay/mod.rs +++ b/crates/rust4d_render/src/egui_overlay/mod.rs @@ -90,7 +90,7 @@ mod context; mod renderer; -pub use context::{HudContext, color32_to_rgba, rgba_to_color32}; +pub use context::{color32_to_rgba, rgba_to_color32, HudContext}; pub use renderer::EguiRenderer; // Re-export key egui types for convenience diff --git a/crates/rust4d_render/src/egui_overlay/renderer.rs b/crates/rust4d_render/src/egui_overlay/renderer.rs index 27c6867..34329fd 100644 --- a/crates/rust4d_render/src/egui_overlay/renderer.rs +++ b/crates/rust4d_render/src/egui_overlay/renderer.rs @@ -91,7 +91,9 @@ impl EguiRenderer { } // Tessellate shapes into primitives - let clipped_primitives = self.ctx.tessellate(full_output.shapes, full_output.pixels_per_point); + let clipped_primitives = self + .ctx + .tessellate(full_output.shapes, full_output.pixels_per_point); // Update buffers self.renderer.update_buffers( diff --git a/crates/rust4d_render/src/lib.rs b/crates/rust4d_render/src/lib.rs index f2eb4f8..417acd7 100644 --- a/crates/rust4d_render/src/lib.rs +++ b/crates/rust4d_render/src/lib.rs @@ -19,27 +19,29 @@ //! Shape geometry is defined in `rust4d_math`. This crate re-exports the shapes //! for convenience, but you can also import them directly from `rust4d_math`. -pub mod context; pub mod camera4d; +pub mod context; +pub mod egui_overlay; +pub mod particle; pub mod pipeline; pub mod renderable; pub mod sprite; -pub mod particle; -pub mod egui_overlay; // Re-export core types for convenience -pub use rust4d_core::{World, Transform4D, Material, ShapeRef, DirtyFlags, Tags}; -pub use rust4d_core::{ConvexShape4D, Tetrahedron, Tesseract4D, Hyperplane4D}; -pub use rust4d_core::{Vec4, Rotor4, RotationPlane}; +pub use rust4d_core::{ConvexShape4D, Hyperplane4D, Tesseract4D, Tetrahedron}; +pub use rust4d_core::{DirtyFlags, Material, ShapeRef, Tags, Transform4D, World}; +pub use rust4d_core::{RotationPlane, Rotor4, Vec4}; // Re-export renderable for easy access -pub use renderable::{RenderableGeometry, CheckerboardGeometry, position_gradient_color}; +pub use renderable::{position_gradient_color, CheckerboardGeometry, RenderableGeometry}; // Re-export sprite types for easy access -pub use sprite::{Sprite, SpriteSheet, SpriteBatch, WFadeConfig}; +pub use sprite::{Sprite, SpriteBatch, SpriteSheet, WFadeConfig}; // Re-export particle types for easy access -pub use particle::{Particle, BlendMode, BurstConfig, EmitterConfig, ParticleEmitter, ParticleSystem}; +pub use particle::{ + BlendMode, BurstConfig, EmitterConfig, Particle, ParticleEmitter, ParticleSystem, +}; // Re-export egui overlay types for easy access -pub use egui_overlay::{EguiRenderer, HudContext, ScreenDescriptor, RawInput}; +pub use egui_overlay::{EguiRenderer, HudContext, RawInput, ScreenDescriptor}; diff --git a/crates/rust4d_render/src/particle/emitter.rs b/crates/rust4d_render/src/particle/emitter.rs index e0cf486..ecdf6ad 100644 --- a/crates/rust4d_render/src/particle/emitter.rs +++ b/crates/rust4d_render/src/particle/emitter.rs @@ -1,9 +1,9 @@ //! Particle emitter for continuous particle emission -use rand::{Rng, SeedableRng}; +use super::types::{BurstConfig, EmitterConfig, Particle}; use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; use rust4d_math::Vec4; -use super::types::{Particle, EmitterConfig, BurstConfig}; /// A continuous particle emitter #[derive(Clone, Debug)] @@ -74,7 +74,8 @@ impl ParticleEmitter { self.accumulator -= particles_to_spawn as f32; // Spawn the particles - let mut particles = Vec::with_capacity((particles_to_spawn * self.config.burst.count) as usize); + let mut particles = + Vec::with_capacity((particles_to_spawn * self.config.burst.count) as usize); for _ in 0..particles_to_spawn { particles.extend(self.spawn_burst_internal(&self.config.burst.clone())); } @@ -214,7 +215,7 @@ pub fn spawn_burst(position: Vec4, config: &BurstConfig, seed: u64) -> Vec assert_eq!(tri_count, 0), @@ -257,7 +266,9 @@ mod tests { assert_eq!( TETRA_EDGE_TABLE[i].count_ones(), TETRA_EDGE_TABLE[15 - i].count_ones(), - "Cases {} and {} should have same edge count", i, 15 - i + "Cases {} and {} should have same edge count", + i, + 15 - i ); } } @@ -272,9 +283,13 @@ mod tests { for i in 0..6 { let idx = TETRA_TRI_TABLE[case_idx][i]; if idx >= 0 { - assert!(idx < num_edges, + assert!( + idx < num_edges, "Case {}: triangle index {} out of range (only {} edges)", - case_idx, idx, num_edges); + case_idx, + idx, + num_edges + ); } } } diff --git a/crates/rust4d_render/src/pipeline/mod.rs b/crates/rust4d_render/src/pipeline/mod.rs index ba17d10..8c0816d 100644 --- a/crates/rust4d_render/src/pipeline/mod.rs +++ b/crates/rust4d_render/src/pipeline/mod.rs @@ -4,22 +4,24 @@ //! 4D cross-section rendering. pub mod lookup_tables; -pub mod types; -pub mod slice_pipeline; pub mod render_pipeline; +pub mod slice_pipeline; +pub mod types; // Re-export lookup tables (tetrahedra tables only) pub use lookup_tables::{ - TETRA_EDGES, TETRA_EDGE_TABLE, TETRA_TRI_TABLE, TETRA_TRI_COUNT, - tetra_edge_count, tetra_crossed_edges, + tetra_crossed_edges, tetra_edge_count, TETRA_EDGES, TETRA_EDGE_TABLE, TETRA_TRI_COUNT, + TETRA_TRI_TABLE, }; // Re-export types pub use types::{ - Vertex4D, Vertex3D, SliceParams, RenderUniforms, - AtomicCounter, GpuTetrahedron, MAX_OUTPUT_TRIANGLES, TRIANGLE_VERTEX_COUNT, + AtomicCounter, GpuTetrahedron, RenderUniforms, SliceParams, Vertex3D, Vertex4D, + MAX_OUTPUT_TRIANGLES, TRIANGLE_VERTEX_COUNT, }; // Re-export pipelines +pub use render_pipeline::{ + look_at_matrix, mat4_mul, perspective_matrix, DrawIndirectArgs, RenderPipeline, +}; pub use slice_pipeline::SlicePipeline; -pub use render_pipeline::{RenderPipeline, DrawIndirectArgs, perspective_matrix, look_at_matrix, mat4_mul}; diff --git a/crates/rust4d_render/src/pipeline/render_pipeline.rs b/crates/rust4d_render/src/pipeline/render_pipeline.rs index c44b69e..5127afa 100644 --- a/crates/rust4d_render/src/pipeline/render_pipeline.rs +++ b/crates/rust4d_render/src/pipeline/render_pipeline.rs @@ -129,12 +129,10 @@ impl RenderPipeline { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("Render Bind Group"), layout: &bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.as_entire_binding(), - }, - ], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], }); // Create indirect draw buffer @@ -238,11 +236,13 @@ impl RenderPipeline { sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); - self.depth_texture = Some(depth_texture.create_view(&wgpu::TextureViewDescriptor::default())); + self.depth_texture = + Some(depth_texture.create_view(&wgpu::TextureViewDescriptor::default())); self.depth_size = (width, height); } } @@ -257,7 +257,10 @@ impl RenderPipeline { vertex_buffer: &wgpu::Buffer, clear_color: wgpu::Color, ) { - let depth_view = self.depth_texture.as_ref().expect("Depth texture not created. Call ensure_depth_texture first."); + let depth_view = self + .depth_texture + .as_ref() + .expect("Depth texture not created. Call ensure_depth_texture first."); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), @@ -313,11 +316,7 @@ pub fn perspective_matrix(fov_y: f32, aspect: f32, near: f32, far: f32) -> [[f32 /// Helper to create a look-at view matrix pub fn look_at_matrix(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> [[f32; 4]; 4] { - let f = normalize([ - target[0] - eye[0], - target[1] - eye[1], - target[2] - eye[2], - ]); + let f = normalize([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]); let s = normalize(cross(f, up)); let u = cross(s, f); @@ -334,7 +333,8 @@ pub fn mat4_mul(a: [[f32; 4]; 4], b: [[f32; 4]; 4]) -> [[f32; 4]; 4] { let mut result = [[0.0f32; 4]; 4]; for i in 0..4 { for j in 0..4 { - result[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j] + a[i][3] * b[3][j]; + result[i][j] = + a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j] + a[i][3] * b[3][j]; } } result @@ -399,8 +399,14 @@ mod tests { // Near plane (z = -near) must map to depth 0, far plane to depth 1. let d_near = project(proj, [0.0, 0.0, -near])[2]; let d_far = project(proj, [0.0, 0.0, -far])[2]; - assert!(d_near.abs() < 1e-5, "near plane depth should be 0, got {d_near}"); - assert!((d_far - 1.0).abs() < 1e-5, "far plane depth should be 1, got {d_far}"); + assert!( + d_near.abs() < 1e-5, + "near plane depth should be 0, got {d_near}" + ); + assert!( + (d_far - 1.0).abs() < 1e-5, + "far plane depth should be 1, got {d_far}" + ); // Every depth between near and far must stay inside [0, 1] — the // OpenGL convention put anything closer than √(near·far) below 0, diff --git a/crates/rust4d_render/src/pipeline/slice_pipeline.rs b/crates/rust4d_render/src/pipeline/slice_pipeline.rs index 4cffc9d..2074ebb 100644 --- a/crates/rust4d_render/src/pipeline/slice_pipeline.rs +++ b/crates/rust4d_render/src/pipeline/slice_pipeline.rs @@ -6,8 +6,7 @@ use wgpu::util::DeviceExt; use super::types::{ - SliceParams, Vertex3D, Vertex4D, GpuTetrahedron, AtomicCounter, - TRIANGLE_VERTEX_COUNT, + AtomicCounter, GpuTetrahedron, SliceParams, Vertex3D, Vertex4D, TRIANGLE_VERTEX_COUNT, }; /// Compute pipeline for slicing 4D geometry @@ -143,11 +142,14 @@ impl SlicePipeline { }); // Create output buffer sized by max_triangles parameter - let output_size = (max_triangles * TRIANGLE_VERTEX_COUNT * std::mem::size_of::()) as u64; + let output_size = + (max_triangles * TRIANGLE_VERTEX_COUNT * std::mem::size_of::()) as u64; let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Slice Output Buffer"), size: output_size, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_SRC, + usage: wgpu::BufferUsages::STORAGE + | wgpu::BufferUsages::VERTEX + | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false, }); @@ -155,7 +157,10 @@ impl SlicePipeline { let counter_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Slice Counter Buffer"), size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::INDIRECT, + usage: wgpu::BufferUsages::STORAGE + | wgpu::BufferUsages::COPY_DST + | wgpu::BufferUsages::COPY_SRC + | wgpu::BufferUsages::INDIRECT, mapped_at_creation: false, }); @@ -181,22 +186,31 @@ impl SlicePipeline { } /// Upload tetrahedra and vertices to the GPU - pub fn upload_tetrahedra(&mut self, device: &wgpu::Device, vertices: &[Vertex4D], tetrahedra: &[GpuTetrahedron]) { + pub fn upload_tetrahedra( + &mut self, + device: &wgpu::Device, + vertices: &[Vertex4D], + tetrahedra: &[GpuTetrahedron], + ) { self.tetra_count = tetrahedra.len() as u32; // Create vertex buffer - self.vertex_buffer = Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Vertex Buffer"), - contents: bytemuck::cast_slice(vertices), - usage: wgpu::BufferUsages::STORAGE, - })); + self.vertex_buffer = Some( + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(vertices), + usage: wgpu::BufferUsages::STORAGE, + }), + ); // Create tetrahedra buffer - self.tetra_buffer = Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Tetrahedra Buffer"), - contents: bytemuck::cast_slice(tetrahedra), - usage: wgpu::BufferUsages::STORAGE, - })); + self.tetra_buffer = Some( + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Tetrahedra Buffer"), + contents: bytemuck::cast_slice(tetrahedra), + usage: wgpu::BufferUsages::STORAGE, + }), + ); // Recreate bind group self.bind_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor { diff --git a/crates/rust4d_render/src/renderable.rs b/crates/rust4d_render/src/renderable.rs index f966122..fd59671 100644 --- a/crates/rust4d_render/src/renderable.rs +++ b/crates/rust4d_render/src/renderable.rs @@ -3,9 +3,9 @@ //! This module converts the abstract shape data from rust4d_core into //! GPU-compatible vertex and tetrahedra buffers. -use rust4d_core::{World, Material, Transform4D, ShapeRef}; +use crate::pipeline::{GpuTetrahedron, Vertex4D}; +use rust4d_core::{Material, ShapeRef, Transform4D, World}; use rust4d_math::Vec4; -use crate::pipeline::{Vertex4D, GpuTetrahedron}; /// GPU-ready geometry collected from entities /// @@ -46,9 +46,16 @@ impl RenderableGeometry { /// /// Iterates all entities with Transform4D, ShapeRef, and Material components /// using ECS queries. - pub fn from_world_with_color(world: &World, color_fn: &dyn Fn(&Vec4, &Material) -> [f32; 4]) -> Self { + pub fn from_world_with_color( + world: &World, + color_fn: &dyn Fn(&Vec4, &Material) -> [f32; 4], + ) -> Self { let mut result = Self::new(); - for (_entity, (transform, shape, material)) in world.ecs().query::<(&Transform4D, &ShapeRef, &Material)>().iter() { + for (_entity, (transform, shape, material)) in world + .ecs() + .query::<(&Transform4D, &ShapeRef, &Material)>() + .iter() + { result.add_components_with_color(transform, shape.as_shape(), material, color_fn); } result @@ -142,7 +149,11 @@ pub struct CheckerboardGeometry { impl CheckerboardGeometry { /// Create a new checkerboard with the given colors and cell size pub fn new(color_a: [f32; 4], color_b: [f32; 4], cell_size: f32) -> Self { - Self { color_a, color_b, cell_size } + Self { + color_a, + color_b, + cell_size, + } } /// Get the color for a vertex based on its XZ position @@ -159,16 +170,14 @@ impl CheckerboardGeometry { /// Create a color function that applies checkerboard pattern pub fn color_fn(&self) -> impl Fn(&Vec4, &Material) -> [f32; 4] + '_ { - move |vertex, _material| { - self.color_for_position(vertex.x, vertex.z) - } + move |vertex, _material| self.color_for_position(vertex.x, vertex.z) } } #[cfg(test)] mod tests { use super::*; - use rust4d_core::{ShapeRef, Tesseract4D, Transform4D, DirtyFlags}; + use rust4d_core::{DirtyFlags, ShapeRef, Tesseract4D, Transform4D}; fn spawn_test_entity(world: &mut World) -> rust4d_core::hecs::Entity { let tesseract = Tesseract4D::new(2.0); @@ -195,7 +204,12 @@ mod tests { let transform = Transform4D::identity(); let mut geom = RenderableGeometry::new(); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); assert_eq!(geom.vertex_count(), 16); // Tesseract has 16 vertices assert!(geom.tetrahedron_count() > 0); @@ -225,10 +239,20 @@ mod tests { let material = Material::from_rgb(1.0, 0.5, 0.25); let transform = Transform4D::identity(); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); assert_eq!(geom.vertex_count(), 16); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); assert_eq!(geom.vertex_count(), 32); } @@ -240,7 +264,12 @@ mod tests { let transform = Transform4D::identity(); let mut geom = RenderableGeometry::new(); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); assert!(geom.vertex_count() > 0); geom.clear(); @@ -286,12 +315,20 @@ mod tests { let transform = Transform4D::from_position(Vec4::new(10.0, 0.0, 0.0, 0.0)); let mut geom = RenderableGeometry::new(); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); // All vertices should be offset by 10 in x for v in &geom.vertices { - assert!(v.position[0] >= 9.0 && v.position[0] <= 11.0, - "Vertex x should be around 10, got {}", v.position[0]); + assert!( + v.position[0] >= 9.0 && v.position[0] <= 11.0, + "Vertex x should be around 10, got {}", + v.position[0] + ); } } @@ -303,14 +340,26 @@ mod tests { let material = Material::from_rgb(1.0, 0.5, 0.25); let transform = Transform4D::identity(); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); let first_entity_verts = geom.vertex_count(); - geom.add_components_with_color(&transform, shape_ref.as_shape(), &material, &default_color_fn); + geom.add_components_with_color( + &transform, + shape_ref.as_shape(), + &material, + &default_color_fn, + ); // Second entity's tetrahedra should have indices >= first_entity_verts let second_tet = geom.tetrahedra.last().unwrap(); - assert!(second_tet.v0 >= first_entity_verts as u32, - "Second entity's tetrahedra should have offset indices"); + assert!( + second_tet.v0 >= first_entity_verts as u32, + "Second entity's tetrahedra should have offset indices" + ); } } diff --git a/crates/rust4d_render/src/sprite/batch.rs b/crates/rust4d_render/src/sprite/batch.rs index d571571..1ea1c58 100644 --- a/crates/rust4d_render/src/sprite/batch.rs +++ b/crates/rust4d_render/src/sprite/batch.rs @@ -3,9 +3,9 @@ //! Collects sprites during a frame and provides sorted output for rendering //! with proper transparency handling. -use std::collections::HashMap; -use rust4d_math::Vec4; use super::types::{Sprite, SpriteSheet}; +use rust4d_math::Vec4; +use std::collections::HashMap; /// Batch renderer for sprites /// @@ -93,7 +93,9 @@ impl SpriteBatch { let dist_a = a.distance_3d(camera_pos); let dist_b = b.distance_3d(camera_pos); // Sort by distance descending (far sprites first) - dist_b.partial_cmp(&dist_a).unwrap_or(std::cmp::Ordering::Equal) + dist_b + .partial_cmp(&dist_a) + .unwrap_or(std::cmp::Ordering::Equal) }); sorted } @@ -103,10 +105,12 @@ impl SpriteBatch { /// Returns sprites that have non-zero alpha after W-fade, /// sorted back to front. pub fn get_visible_sorted(&self, camera_pos: Vec4, current_w: f32) -> Vec<(&Sprite, f32)> { - let mut visible: Vec<(&Sprite, f32)> = self.sprites + let mut visible: Vec<(&Sprite, f32)> = self + .sprites .iter() .filter_map(|sprite| { - let w_fade = Self::calculate_w_fade(sprite.position.w, current_w, sprite.w_fade_range); + let w_fade = + Self::calculate_w_fade(sprite.position.w, current_w, sprite.w_fade_range); let final_alpha = sprite.color_tint[3] * w_fade; if final_alpha > 0.001 { Some((sprite, final_alpha)) @@ -119,7 +123,9 @@ impl SpriteBatch { visible.sort_by(|a, b| { let dist_a = a.0.distance_3d(camera_pos); let dist_b = b.0.distance_3d(camera_pos); - dist_b.partial_cmp(&dist_a).unwrap_or(std::cmp::Ordering::Equal) + dist_b + .partial_cmp(&dist_a) + .unwrap_or(std::cmp::Ordering::Equal) }); visible diff --git a/crates/rust4d_render/src/sprite/mod.rs b/crates/rust4d_render/src/sprite/mod.rs index f50a279..9359c16 100644 --- a/crates/rust4d_render/src/sprite/mod.rs +++ b/crates/rust4d_render/src/sprite/mod.rs @@ -68,9 +68,9 @@ //! distance from the camera (W dimension is ignored for depth sorting since //! billboards are rendered at their XYZ projection). -pub mod types; pub mod batch; +pub mod types; // Re-export main types for convenience -pub use types::{Sprite, SpriteSheet, WFadeConfig}; pub use batch::SpriteBatch; +pub use types::{Sprite, SpriteSheet, WFadeConfig}; diff --git a/crates/rust4d_render/src/sprite/types.rs b/crates/rust4d_render/src/sprite/types.rs index 49d71af..c98ef7a 100644 --- a/crates/rust4d_render/src/sprite/types.rs +++ b/crates/rust4d_render/src/sprite/types.rs @@ -28,7 +28,13 @@ pub struct SpriteSheet { impl SpriteSheet { /// Create a new sprite sheet with the given parameters - pub fn new(name: impl Into, frame_width: u32, frame_height: u32, columns: u32, rows: u32) -> Self { + pub fn new( + name: impl Into, + frame_width: u32, + frame_height: u32, + columns: u32, + rows: u32, + ) -> Self { Self { name: name.into(), frame_width, @@ -114,7 +120,7 @@ impl Sprite { sheet_name: sheet_name.into(), frame: 0, size: [1.0, 1.0], - w_fade_range: 2.0, // Default fade range of 2 world units + w_fade_range: 2.0, // Default fade range of 2 world units color_tint: [1.0, 1.0, 1.0, 1.0], // White (no tint) } } @@ -179,7 +185,8 @@ impl WFadeConfig { debug_assert!( fade_end > fade_start, "WFadeConfig: fade_end ({}) must be greater than fade_start ({})", - fade_end, fade_start + fade_end, + fade_start ); Self { current_w, @@ -241,8 +248,8 @@ mod tests { // First frame (top-left) let uvs = sheet.frame_uvs(0); - assert_eq!(uvs[0], 0.0); // u_min - assert_eq!(uvs[1], 0.0); // v_min + assert_eq!(uvs[0], 0.0); // u_min + assert_eq!(uvs[1], 0.0); // v_min assert_eq!(uvs[2], 0.25); // u_max assert_eq!(uvs[3], 0.25); // v_max diff --git a/crates/rust4d_scripting/src/bindings/audio.rs b/crates/rust4d_scripting/src/bindings/audio.rs index f8078b9..b7d19a4 100644 --- a/crates/rust4d_scripting/src/bindings/audio.rs +++ b/crates/rust4d_scripting/src/bindings/audio.rs @@ -186,7 +186,11 @@ pub fn register(lua: &Lua) -> LuaResult<()> { let bus_name = validate_bus_name(&bus)?; // STUB: Log warning // Real implementation would call engine.play(&handle, bus) - log::trace!("[audio] play(handle={}, bus='{}') - stub", handle.id, bus_name); + log::trace!( + "[audio] play(handle={}, bus='{}') - stub", + handle.id, + bus_name + ); Ok(()) })?, )?; @@ -203,7 +207,11 @@ pub fn register(lua: &Lua) -> LuaResult<()> { "play_oneshot", lua.create_function(|_, (handle, bus): (LuaSoundHandle, String)| { let bus_name = validate_bus_name(&bus)?; - log::trace!("[audio] play_oneshot(handle={}, bus='{}') - stub", handle.id, bus_name); + log::trace!( + "[audio] play_oneshot(handle={}, bus='{}') - stub", + handle.id, + bus_name + ); Ok(()) })?, )?; @@ -289,7 +297,11 @@ pub fn register(lua: &Lua) -> LuaResult<()> { ); } - log::trace!("[audio] set_volume(bus='{}', volume={:.2}) - stub", bus_name, clamped); + log::trace!( + "[audio] set_volume(bus='{}', volume={:.2}) - stub", + bus_name, + clamped + ); Ok(()) })?, )?; @@ -332,7 +344,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { lua.create_function(|_, pos: LuaVec4| { log::trace!( "[audio] update_listener(pos=({:.2}, {:.2}, {:.2}, {:.2})) - stub", - pos.0.x, pos.0.y, pos.0.z, pos.0.w + pos.0.x, + pos.0.y, + pos.0.z, + pos.0.w ); Ok(()) })?, diff --git a/crates/rust4d_scripting/src/bindings/ecs.rs b/crates/rust4d_scripting/src/bindings/ecs.rs index 681551f..f81f805 100644 --- a/crates/rust4d_scripting/src/bindings/ecs.rs +++ b/crates/rust4d_scripting/src/bindings/ecs.rs @@ -163,18 +163,15 @@ pub fn register(lua: &Lua) -> LuaResult<()> { "query", lua.create_function(|lua, component: String| { if !ECS_WARNED.swap(true, Ordering::Relaxed) { - log::warn!( - "[ecs] hecs::World not connected - entity operations are stubs." - ); + log::warn!("[ecs] hecs::World not connected - entity operations are stubs."); } log::trace!("[ecs] query called for component: {}", component); // Return an empty iterator function // Real implementation would iterate over hecs::World query results - let empty_iter = lua.create_function(|_, ()| -> LuaResult> { - Ok(None) - })?; + let empty_iter = + lua.create_function(|_, ()| -> LuaResult> { Ok(None) })?; Ok(empty_iter) })?, )?; @@ -186,9 +183,7 @@ pub fn register(lua: &Lua) -> LuaResult<()> { "find_by_name", lua.create_function(|_, name: String| { if !ECS_WARNED.swap(true, Ordering::Relaxed) { - log::warn!( - "[ecs] hecs::World not connected - entity operations are stubs." - ); + log::warn!("[ecs] hecs::World not connected - entity operations are stubs."); } log::trace!("[ecs] find_by_name called: {}", name); // Real implementation would query World for entity with matching Name component @@ -240,7 +235,10 @@ mod tests { #[test] fn test_world_table_exists() { let lua = create_lua_with_ecs(); - let world: LuaTable = lua.globals().get("world").expect("world table should exist"); + let world: LuaTable = lua + .globals() + .get("world") + .expect("world table should exist"); assert!(world.contains_key("spawn").unwrap()); assert!(world.contains_key("query").unwrap()); assert!(world.contains_key("find_by_name").unwrap()); @@ -423,7 +421,8 @@ mod tests { let generation: u64 = 1; let id: u64 = 42; let bits = (generation << 32) | id; - let entity = LuaEntity(Entity::from_bits(bits).expect("should create entity from valid bits")); + let entity = + LuaEntity(Entity::from_bits(bits).expect("should create entity from valid bits")); // Entity created with id 42 should have id 42 assert_eq!(entity.0.id(), 42); // to_bits returns a non-zero value diff --git a/crates/rust4d_scripting/src/bindings/hud.rs b/crates/rust4d_scripting/src/bindings/hud.rs index 796c2ef..3643860 100644 --- a/crates/rust4d_scripting/src/bindings/hud.rs +++ b/crates/rust4d_scripting/src/bindings/hud.rs @@ -124,22 +124,24 @@ pub fn register(lua: &Lua) -> LuaResult<()> { // - color: RGBA color table {r, g, b, a} or {1, 2, 3, 4} hud_table.set( "text", - lua.create_function(|_, (x, y, text, size, color): (f32, f32, String, f32, LuaTable)| { - let color = table_to_color(&color)?; - // STUB: Log at trace level (LOW-6 - called every frame) - // Real implementation would: - // 1. Get HudContext from lua.app_data() - // 2. Call hud.text([x, y], &text, size, color) - log::trace!( - "[hud] text at ({}, {}): '{}' size={} color={:?}", - x, - y, - text, - size, - color - ); - Ok(()) - })?, + lua.create_function( + |_, (x, y, text, size, color): (f32, f32, String, f32, LuaTable)| { + let color = table_to_color(&color)?; + // STUB: Log at trace level (LOW-6 - called every frame) + // Real implementation would: + // 1. Get HudContext from lua.app_data() + // 2. Call hud.text([x, y], &text, size, color) + log::trace!( + "[hud] text at ({}, {}): '{}' size={} color={:?}", + x, + y, + text, + size, + color + ); + Ok(()) + }, + )?, )?; // hud.text_centered(x, y, text, size, color) @@ -154,18 +156,20 @@ pub fn register(lua: &Lua) -> LuaResult<()> { // - color: RGBA color table {r, g, b, a} or {1, 2, 3, 4} hud_table.set( "text_centered", - lua.create_function(|_, (x, y, text, size, color): (f32, f32, String, f32, LuaTable)| { - let color = table_to_color(&color)?; - log::trace!( - "[hud] text_centered at ({}, {}): '{}' size={} color={:?}", - x, - y, - text, - size, - color - ); - Ok(()) - })?, + lua.create_function( + |_, (x, y, text, size, color): (f32, f32, String, f32, LuaTable)| { + let color = table_to_color(&color)?; + log::trace!( + "[hud] text_centered at ({}, {}): '{}' size={} color={:?}", + x, + y, + text, + size, + color + ); + Ok(()) + }, + )?, )?; // hud.rect(x, y, width, height, color) @@ -365,7 +369,11 @@ fn table_to_color(table: &LuaTable) -> LuaResult<[f32; 4]> { log::warn!( "[hud] Color table using named format but missing component(s): {:?}. \ Typo? Got r={}, g={}, b={}, a={}. Missing components default to 0.0.", - missing, r, g, b, a + missing, + r, + g, + b, + a ); } @@ -393,10 +401,7 @@ mod tests { #[test] fn test_hud_table_exists() { let lua = create_lua_with_hud(); - let hud: LuaTable = lua - .globals() - .get("hud") - .expect("hud table should exist"); + let hud: LuaTable = lua.globals().get("hud").expect("hud table should exist"); assert!(hud.contains_key("text").unwrap()); assert!(hud.contains_key("text_centered").unwrap()); assert!(hud.contains_key("rect").unwrap()); @@ -538,7 +543,10 @@ mod tests { // No alpha specified let color = table_to_color(&table).unwrap(); - assert!((color[3] - 1.0).abs() < 0.001, "Default alpha should be 1.0"); + assert!( + (color[3] - 1.0).abs() < 0.001, + "Default alpha should be 1.0" + ); } #[test] @@ -550,9 +558,18 @@ mod tests { let color = table_to_color(&table).unwrap(); assert!((color[0] - 1.0).abs() < 0.001); - assert!((color[1] - 0.0).abs() < 0.001, "Missing g should default to 0.0"); - assert!((color[2] - 0.0).abs() < 0.001, "Missing b should default to 0.0"); - assert!((color[3] - 1.0).abs() < 0.001, "Missing a should default to 1.0"); + assert!( + (color[1] - 0.0).abs() < 0.001, + "Missing g should default to 0.0" + ); + assert!( + (color[2] - 0.0).abs() < 0.001, + "Missing b should default to 0.0" + ); + assert!( + (color[3] - 1.0).abs() < 0.001, + "Missing a should default to 1.0" + ); } #[test] diff --git a/crates/rust4d_scripting/src/bindings/input.rs b/crates/rust4d_scripting/src/bindings/input.rs index 7c6d667..6a62f89 100644 --- a/crates/rust4d_scripting/src/bindings/input.rs +++ b/crates/rust4d_scripting/src/bindings/input.rs @@ -55,36 +55,134 @@ static INPUT_WARNED: AtomicBool = AtomicBool::new(false); /// These correspond to common keyboard keys that would be handled by winit/gilrs. const VALID_KEY_NAMES: &[&str] = &[ // Letters - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", - "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", // Numbers - "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", // Function keys - "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", // Modifiers - "Shift", "LShift", "RShift", - "Control", "LControl", "RControl", "Ctrl", "LCtrl", "RCtrl", - "Alt", "LAlt", "RAlt", - "Super", "LSuper", "RSuper", "Win", "LWin", "RWin", + "Shift", + "LShift", + "RShift", + "Control", + "LControl", + "RControl", + "Ctrl", + "LCtrl", + "RCtrl", + "Alt", + "LAlt", + "RAlt", + "Super", + "LSuper", + "RSuper", + "Win", + "LWin", + "RWin", // Special keys - "Space", "Enter", "Return", "Escape", "Esc", - "Tab", "Backspace", "Delete", "Insert", - "Home", "End", "PageUp", "PageDown", - "Up", "Down", "Left", "Right", + "Space", + "Enter", + "Return", + "Escape", + "Esc", + "Tab", + "Backspace", + "Delete", + "Insert", + "Home", + "End", + "PageUp", + "PageDown", + "Up", + "Down", + "Left", + "Right", // Punctuation and symbols - "Minus", "Plus", "Equals", - "LeftBracket", "RightBracket", "LBracket", "RBracket", - "Backslash", "Semicolon", "Quote", "Apostrophe", - "Comma", "Period", "Slash", - "Grave", "Backtick", "Tilde", + "Minus", + "Plus", + "Equals", + "LeftBracket", + "RightBracket", + "LBracket", + "RBracket", + "Backslash", + "Semicolon", + "Quote", + "Apostrophe", + "Comma", + "Period", + "Slash", + "Grave", + "Backtick", + "Tilde", // Numpad - "Numpad0", "Numpad1", "Numpad2", "Numpad3", "Numpad4", - "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", - "NumpadAdd", "NumpadSubtract", "NumpadMultiply", "NumpadDivide", - "NumpadEnter", "NumpadDecimal", + "Numpad0", + "Numpad1", + "Numpad2", + "Numpad3", + "Numpad4", + "Numpad5", + "Numpad6", + "Numpad7", + "Numpad8", + "Numpad9", + "NumpadAdd", + "NumpadSubtract", + "NumpadMultiply", + "NumpadDivide", + "NumpadEnter", + "NumpadDecimal", // Other - "CapsLock", "NumLock", "ScrollLock", - "PrintScreen", "Pause", + "CapsLock", + "NumLock", + "ScrollLock", + "PrintScreen", + "Pause", ]; /// Validate a key name and return it normalized (uppercase for letters). @@ -194,7 +292,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { } // STUB: Return false - log::trace!("[input] is_key_just_pressed('{}') - stub returning false", key); + log::trace!( + "[input] is_key_just_pressed('{}') - stub returning false", + key + ); Ok(false) })?, )?; @@ -220,7 +321,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { // STUB: Return false // Action names are user-defined, so we don't validate them - log::trace!("[input] is_action_pressed('{}') - stub returning false", action); + log::trace!( + "[input] is_action_pressed('{}') - stub returning false", + action + ); Ok(false) })?, )?; @@ -244,7 +348,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { } // STUB: Return false - log::trace!("[input] is_action_just_pressed('{}') - stub returning false", action); + log::trace!( + "[input] is_action_just_pressed('{}') - stub returning false", + action + ); Ok(false) })?, )?; @@ -274,7 +381,11 @@ pub fn register(lua: &Lua) -> LuaResult<()> { } // STUB: Return 0.0 - log::trace!("[input] get_axis('{}', '{}') - stub returning 0.0", positive, negative); + log::trace!( + "[input] get_axis('{}', '{}') - stub returning 0.0", + positive, + negative + ); Ok(0.0f32) })?, )?; @@ -343,7 +454,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { ); } - log::trace!("[input] is_mouse_button_pressed('{}') - stub returning false", button); + log::trace!( + "[input] is_mouse_button_pressed('{}') - stub returning false", + button + ); Ok(false) })?, )?; @@ -368,7 +482,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { ); } - log::trace!("[input] is_mouse_button_just_pressed('{}') - stub returning false", button); + log::trace!( + "[input] is_mouse_button_just_pressed('{}') - stub returning false", + button + ); Ok(false) })?, )?; diff --git a/crates/rust4d_scripting/src/bindings/math.rs b/crates/rust4d_scripting/src/bindings/math.rs index 73fa0d5..a3299aa 100644 --- a/crates/rust4d_scripting/src/bindings/math.rs +++ b/crates/rust4d_scripting/src/bindings/math.rs @@ -28,7 +28,7 @@ //! This module is owned by Agent D2 (Math/Physics Bindings). use mlua::prelude::*; -use rust4d_math::{Rotor4, RotationPlane, Vec4}; +use rust4d_math::{RotationPlane, Rotor4, Vec4}; // Re-export Transform4D binding if rust4d_core is available // For now, we'll implement Transform4D bindings directly using the math crate @@ -78,9 +78,7 @@ impl LuaUserData for LuaVec4 { methods.add_method("length_squared", |_, this, ()| Ok(this.0.length_squared())); // Normalize to unit length - methods.add_method("normalized", |_, this, ()| { - Ok(LuaVec4(this.0.normalized())) - }); + methods.add_method("normalized", |_, this, ()| Ok(LuaVec4(this.0.normalized()))); // Distance between two vectors methods.add_method("distance", |_, this, other: LuaVec4| { @@ -218,9 +216,7 @@ impl LuaUserData for LuaRotor4 { methods.add_method("reverse", |_, this, ()| Ok(LuaRotor4(this.0.reverse()))); // Normalize the rotor - methods.add_method("normalize", |_, this, ()| { - Ok(LuaRotor4(this.0.normalize())) - }); + methods.add_method("normalize", |_, this, ()| Ok(LuaRotor4(this.0.normalize()))); // Get magnitude methods.add_method("magnitude", |_, this, ()| Ok(this.0.magnitude())); diff --git a/crates/rust4d_scripting/src/bindings/physics.rs b/crates/rust4d_scripting/src/bindings/physics.rs index 1cb5bbc..0a0bacd 100644 --- a/crates/rust4d_scripting/src/bindings/physics.rs +++ b/crates/rust4d_scripting/src/bindings/physics.rs @@ -262,34 +262,36 @@ pub fn register(lua: &Lua) -> LuaResult<()> { // When physics is not connected, returns a table with `_stub = true` field. physics_table.set( "query_sphere", - lua.create_function( - |lua, (center, radius, layers): (LuaVec4, f32, LuaValue)| { - let layer_names = parse_layers(layers)?; + lua.create_function(|lua, (center, radius, layers): (LuaVec4, f32, LuaValue)| { + let layer_names = parse_layers(layers)?; - if !PHYSICS_WARNED.swap(true, Ordering::Relaxed) { - log::warn!( - "[physics] PhysicsWorld not connected - all physics queries will return \ + if !PHYSICS_WARNED.swap(true, Ordering::Relaxed) { + log::warn!( + "[physics] PhysicsWorld not connected - all physics queries will return \ empty/stub results." - ); - } - - log::trace!( - "[physics] query_sphere at ({:.2}, {:.2}, {:.2}, {:.2}) radius={:.2} layers={:?}", - center.0.x, center.0.y, center.0.z, center.0.w, - radius, layer_names ); + } - // STUB: Return empty array with _stub marker (MEDIUM-7) - // Real implementation would: - // 1. Get PhysicsWorld from lua.app_data() - // 2. Iterate all bodies and check distance from center - // 3. Filter by layer - // 4. Return matches with position and distance - let results = lua.create_table()?; - results.set("_stub", true)?; - Ok(results) - }, - )?, + log::trace!( + "[physics] query_sphere at ({:.2}, {:.2}, {:.2}, {:.2}) radius={:.2} layers={:?}", + center.0.x, + center.0.y, + center.0.z, + center.0.w, + radius, + layer_names + ); + + // STUB: Return empty array with _stub marker (MEDIUM-7) + // Real implementation would: + // 1. Get PhysicsWorld from lua.app_data() + // 2. Iterate all bodies and check distance from center + // 3. Filter by layer + // 4. Return matches with position and distance + let results = lua.create_table()?; + results.set("_stub", true)?; + Ok(results) + })?, )?; // physics.query_area_effect(center, radius, layers, with_falloff) -> array of results @@ -389,7 +391,10 @@ pub fn register(lua: &Lua) -> LuaResult<()> { lua.create_function(|lua, ()| { // Try to get PhysicsConfig from app_data let gravity_y = if let Some(config) = lua.app_data_ref::() { - log::trace!("[physics] gravity() - returning configured value: {}", config.gravity); + log::trace!( + "[physics] gravity() - returning configured value: {}", + config.gravity + ); config.gravity } else { // No config set - use default and warn once diff --git a/crates/rust4d_scripting/src/error.rs b/crates/rust4d_scripting/src/error.rs index 869cad7..9847909 100644 --- a/crates/rust4d_scripting/src/error.rs +++ b/crates/rust4d_scripting/src/error.rs @@ -22,10 +22,7 @@ pub enum ScriptError { /// File watcher error (hot-reload) WatcherError(String), /// Module reload failed (old version continues running) - ModuleReloadError { - path: String, - error: mlua::Error, - }, + ModuleReloadError { path: String, error: mlua::Error }, } impl ScriptError { @@ -72,7 +69,9 @@ impl std::error::Error for ScriptError { Self::IoError(_, e) => Some(e), Self::LuaError(e) => Some(e), Self::RuntimeError { error, .. } => Some(error), - Self::ReloadError { source: Some(e), .. } => Some(e.as_ref()), + Self::ReloadError { + source: Some(e), .. + } => Some(e.as_ref()), Self::ModuleReloadError { error, .. } => Some(error), _ => None, } diff --git a/crates/rust4d_scripting/src/lib.rs b/crates/rust4d_scripting/src/lib.rs index e75194f..222738c 100644 --- a/crates/rust4d_scripting/src/lib.rs +++ b/crates/rust4d_scripting/src/lib.rs @@ -317,25 +317,13 @@ mod tests { engine.call_shutdown().unwrap(); // Verify the lifecycle log - let count: i64 = engine - .lua() - .load("return #lifecycle_log") - .eval() - .unwrap(); + let count: i64 = engine.lua().load("return #lifecycle_log").eval().unwrap(); assert_eq!(count, 4); - let first: String = engine - .lua() - .load("return lifecycle_log[1]") - .eval() - .unwrap(); + let first: String = engine.lua().load("return lifecycle_log[1]").eval().unwrap(); assert_eq!(first, "init"); - let last: String = engine - .lua() - .load("return lifecycle_log[4]") - .eval() - .unwrap(); + let last: String = engine.lua().load("return lifecycle_log[4]").eval().unwrap(); assert_eq!(last, "shutdown"); } @@ -381,8 +369,7 @@ mod tests { let (dir, config) = create_game_dir(); // Create a module - let mut module_file = - std::fs::File::create(dir.path().join("utils.lua")).unwrap(); + let mut module_file = std::fs::File::create(dir.path().join("utils.lua")).unwrap(); writeln!(module_file, "local M = {{}}").unwrap(); writeln!(module_file, "function M.double(x) return x * 2 end").unwrap(); writeln!(module_file, "return M").unwrap(); diff --git a/crates/rust4d_scripting/src/lifecycle.rs b/crates/rust4d_scripting/src/lifecycle.rs index 605c118..43d9df1 100644 --- a/crates/rust4d_scripting/src/lifecycle.rs +++ b/crates/rust4d_scripting/src/lifecycle.rs @@ -1,7 +1,7 @@ //! Game loop lifecycle callback dispatch -use mlua::prelude::*; use crate::error::ScriptError; +use mlua::prelude::*; /// Check if a callback function exists without calling it pub fn has_callback(lua: &Lua, callback_name: &str) -> bool { @@ -21,10 +21,11 @@ pub fn call_lifecycle(lua: &Lua, name: &str, args: impl IntoLuaMulti) -> Result< match globals.get::(name) { Ok(LuaValue::Function(func)) => { - func.call::<()>(args).map_err(|e| ScriptError::RuntimeError { - callback: name.to_string(), - error: e, - })?; + func.call::<()>(args) + .map_err(|e| ScriptError::RuntimeError { + callback: name.to_string(), + error: e, + })?; Ok(()) } Ok(LuaValue::Nil) => { diff --git a/crates/rust4d_scripting/src/loader.rs b/crates/rust4d_scripting/src/loader.rs index a648402..4319697 100644 --- a/crates/rust4d_scripting/src/loader.rs +++ b/crates/rust4d_scripting/src/loader.rs @@ -1,7 +1,7 @@ //! Script loading and require() resolution -use mlua::prelude::*; use crate::error::ScriptError; +use mlua::prelude::*; /// Load the game's main.lua entry point and execute it. /// diff --git a/crates/rust4d_scripting/src/vm.rs b/crates/rust4d_scripting/src/vm.rs index 67b85a9..ac60d29 100644 --- a/crates/rust4d_scripting/src/vm.rs +++ b/crates/rust4d_scripting/src/vm.rs @@ -1,7 +1,7 @@ //! Lua VM initialization and configuration -use mlua::prelude::*; use crate::error::ScriptError; +use mlua::prelude::*; /// Configuration for the scripting engine #[derive(Debug, Clone)] @@ -59,14 +59,18 @@ pub fn create_lua_vm(config: &ScriptConfig) -> Result { // Clear cpath to prevent loading native C modules from system paths package.set("cpath", "").map_err(ScriptError::LuaError)?; // Remove loadlib which can load arbitrary shared libraries - package.set("loadlib", LuaNil).map_err(ScriptError::LuaError)?; + package + .set("loadlib", LuaNil) + .map_err(ScriptError::LuaError)?; } // Remove dangerous standard library modules for sandboxing let globals = lua.globals(); globals.set("os", LuaNil).map_err(ScriptError::LuaError)?; globals.set("io", LuaNil).map_err(ScriptError::LuaError)?; - globals.set("debug", LuaNil).map_err(ScriptError::LuaError)?; + globals + .set("debug", LuaNil) + .map_err(ScriptError::LuaError)?; globals .set("loadfile", LuaNil) .map_err(ScriptError::LuaError)?; @@ -84,12 +88,10 @@ pub fn create_lua_vm(config: &ScriptConfig) -> Result { LuaValue::Boolean(b) => b.to_string(), LuaValue::Integer(n) => n.to_string(), LuaValue::Number(n) => n.to_string(), - LuaValue::String(s) => { - match s.to_str() { - Ok(s) => s.to_string(), - Err(_) => "".to_string(), - } - } + LuaValue::String(s) => match s.to_str() { + Ok(s) => s.to_string(), + Err(_) => "".to_string(), + }, other => format!("{:?}", other), }) .collect(); @@ -117,9 +119,10 @@ pub fn create_lua_vm(config: &ScriptConfig) -> Result { move |_lua, _debug| { let prev = count_clone.fetch_add(1000, std::sync::atomic::Ordering::Relaxed); if prev + 1000 > limit { - return Err(mlua::Error::RuntimeError( - format!("instruction limit exceeded ({})", limit), - )); + return Err(mlua::Error::RuntimeError(format!( + "instruction limit exceeded ({})", + limit + ))); } Ok(mlua::VmState::Continue) }, @@ -190,9 +193,7 @@ mod tests { let config = ScriptConfig::default(); let lua = create_lua_vm(&config).unwrap(); // Should not panic even though log isn't fully initialized - lua.load(r#"print("hello", 42, true, nil)"#) - .exec() - .unwrap(); + lua.load(r#"print("hello", 42, true, nil)"#).exec().unwrap(); } #[test] @@ -202,10 +203,7 @@ mod tests { ..Default::default() }; let lua = create_lua_vm(&config).unwrap(); - let path: String = lua - .load("return package.path") - .eval() - .unwrap(); + let path: String = lua.load("return package.path").eval().unwrap(); assert!(path.contains("/tmp/test_scripts/?.lua")); } diff --git a/crates/rust4d_scripting/tests/sandbox_escape.rs b/crates/rust4d_scripting/tests/sandbox_escape.rs index a30e299..1c63cb8 100644 --- a/crates/rust4d_scripting/tests/sandbox_escape.rs +++ b/crates/rust4d_scripting/tests/sandbox_escape.rs @@ -4,8 +4,8 @@ //! a forbidden action. The test passes if the script errors (or the //! forbidden global is nil). -use rust4d_scripting::ScriptConfig; use rust4d_scripting::vm::create_lua_vm; +use rust4d_scripting::ScriptConfig; /// Helper: create a sandboxed Lua VM with default config. fn sandboxed_vm() -> mlua::Lua { @@ -182,10 +182,7 @@ fn sandbox_blocks_require_c_module() { // Attempting to require a C module should fail because cpath is empty // and loadlib is removed let result = lua.load(r#"require("socket")"#).exec(); - assert!( - result.is_err(), - "require('socket') should fail in sandbox" - ); + assert!(result.is_err(), "require('socket') should fail in sandbox"); } // --------------------------------------------------------------------------- @@ -229,10 +226,7 @@ fn sandbox_blocks_rawget_global_restore() { .load(r#"return rawget(_G, "os")"#) .eval() .unwrap_or(mlua::Value::Nil); - assert!( - result == mlua::Value::Nil, - "rawget(_G, 'os') should be nil" - ); + assert!(result == mlua::Value::Nil, "rawget(_G, 'os') should be nil"); } #[test] @@ -242,10 +236,7 @@ fn sandbox_blocks_rawget_io() { .load(r#"return rawget(_G, "io")"#) .eval() .unwrap_or(mlua::Value::Nil); - assert!( - result == mlua::Value::Nil, - "rawget(_G, 'io') should be nil" - ); + assert!(result == mlua::Value::Nil, "rawget(_G, 'io') should be nil"); } #[test] diff --git a/examples/01_hello_tesseract.rs b/examples/01_hello_tesseract.rs index 89528d4..b81aeb6 100644 --- a/examples/01_hello_tesseract.rs +++ b/examples/01_hello_tesseract.rs @@ -19,14 +19,17 @@ use winit::{ window::{Window, WindowId}, }; -use rust4d_core::{Material, ShapeRef, Tesseract4D, Transform4D, DirtyFlags, World}; +use rust4d_core::{DirtyFlags, Material, ShapeRef, Tesseract4D, Transform4D, World}; +use rust4d_math::Vec4; use rust4d_render::{ camera4d::Camera4D, context::RenderContext, - pipeline::{perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, MAX_OUTPUT_TRIANGLES}, + pipeline::{ + perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, + MAX_OUTPUT_TRIANGLES, + }, RenderableGeometry, }; -use rust4d_math::Vec4; /// Application state struct App { @@ -88,7 +91,8 @@ impl ApplicationHandler for App { // Initialize rendering let render_context = pollster::block_on(RenderContext::new(window.clone())); - let mut slice_pipeline = SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); + let mut slice_pipeline = + SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); let mut render_pipeline = RenderPipeline::new(&render_context.device, render_context.config.format); @@ -140,8 +144,18 @@ impl ApplicationHandler for App { sp.update_params(&ctx.queue, &slice_params); let render_uniforms = RenderUniforms { - view_matrix: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]], - projection_matrix: perspective_matrix(std::f32::consts::FRAC_PI_4, ctx.aspect_ratio(), 0.1, 100.0), + view_matrix: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], + projection_matrix: perspective_matrix( + std::f32::consts::FRAC_PI_4, + ctx.aspect_ratio(), + 0.1, + 100.0, + ), light_dir: [0.5, 1.0, 0.3], _padding: 0.0, ambient_strength: 0.3, @@ -156,13 +170,27 @@ impl ApplicationHandler for App { Ok(o) => o, Err(_) => return, }; - let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); sp.reset_counter(&ctx.queue); sp.run_slice_pass(&mut encoder); rp.prepare_indirect_draw(&mut encoder, sp.counter_buffer()); - rp.render(&mut encoder, &view, sp.output_buffer(), wgpu::Color { r: 0.02, g: 0.02, b: 0.08, a: 1.0 }); + rp.render( + &mut encoder, + &view, + sp.output_buffer(), + wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.08, + a: 1.0, + }, + ); ctx.queue.submit(std::iter::once(encoder.finish())); output.present(); diff --git a/examples/02_multiple_shapes.rs b/examples/02_multiple_shapes.rs index 0af37fb..95b9e34 100644 --- a/examples/02_multiple_shapes.rs +++ b/examples/02_multiple_shapes.rs @@ -19,14 +19,17 @@ use winit::{ window::{Window, WindowId}, }; -use rust4d_core::{Material, ShapeRef, Tesseract4D, Transform4D, DirtyFlags, World}; +use rust4d_core::{DirtyFlags, Material, ShapeRef, Tesseract4D, Transform4D, World}; +use rust4d_math::Vec4; use rust4d_render::{ camera4d::Camera4D, context::RenderContext, - pipeline::{perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, MAX_OUTPUT_TRIANGLES}, + pipeline::{ + perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, + MAX_OUTPUT_TRIANGLES, + }, RenderableGeometry, }; -use rust4d_math::Vec4; /// Movement state struct Movement { @@ -36,15 +39,21 @@ struct Movement { right: bool, up: bool, down: bool, - ana: bool, // Q - move toward +W - kata: bool, // E - move toward -W + ana: bool, // Q - move toward +W + kata: bool, // E - move toward -W } impl Movement { fn new() -> Self { Self { - forward: false, backward: false, left: false, right: false, - up: false, down: false, ana: false, kata: false, + forward: false, + backward: false, + left: false, + right: false, + up: false, + down: false, + ana: false, + kata: false, } } } @@ -69,11 +78,26 @@ impl App { // Create multiple tesseracts with different colors and positions let positions_and_colors = [ - (Vec4::new(0.0, 0.0, 0.0, 0.0), Material::from_rgb(0.9, 0.3, 0.2)), // Red at origin - (Vec4::new(4.0, 0.0, 0.0, 0.0), Material::from_rgb(0.2, 0.9, 0.3)), // Green to the right - (Vec4::new(-4.0, 0.0, 0.0, 0.0), Material::from_rgb(0.2, 0.3, 0.9)), // Blue to the left - (Vec4::new(0.0, 4.0, 0.0, 0.0), Material::from_rgb(0.9, 0.9, 0.2)), // Yellow above - (Vec4::new(0.0, 0.0, 0.0, 4.0), Material::from_rgb(0.9, 0.2, 0.9)), // Magenta in +W + ( + Vec4::new(0.0, 0.0, 0.0, 0.0), + Material::from_rgb(0.9, 0.3, 0.2), + ), // Red at origin + ( + Vec4::new(4.0, 0.0, 0.0, 0.0), + Material::from_rgb(0.2, 0.9, 0.3), + ), // Green to the right + ( + Vec4::new(-4.0, 0.0, 0.0, 0.0), + Material::from_rgb(0.2, 0.3, 0.9), + ), // Blue to the left + ( + Vec4::new(0.0, 4.0, 0.0, 0.0), + Material::from_rgb(0.9, 0.9, 0.2), + ), // Yellow above + ( + Vec4::new(0.0, 0.0, 0.0, 4.0), + Material::from_rgb(0.9, 0.2, 0.9), + ), // Magenta in +W ]; for (position, material) in positions_and_colors { @@ -120,7 +144,8 @@ impl ApplicationHandler for App { ); let render_context = pollster::block_on(RenderContext::new(window.clone())); - let mut slice_pipeline = SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); + let mut slice_pipeline = + SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); let mut render_pipeline = RenderPipeline::new(&render_context.device, render_context.config.format); @@ -187,7 +212,8 @@ impl ApplicationHandler for App { let up = (self.movement.up as i32 - self.movement.down as i32) as f32; let w = (self.movement.ana as i32 - self.movement.kata as i32) as f32; - self.camera.move_local_xz(forward * speed * dt, right * speed * dt); + self.camera + .move_local_xz(forward * speed * dt, right * speed * dt); self.camera.move_y(up * speed * dt); self.camera.move_w(w * speed * dt); @@ -208,10 +234,17 @@ impl ApplicationHandler for App { sp.update_params(&ctx.queue, &slice_params); let render_uniforms = RenderUniforms { - view_matrix: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]], + view_matrix: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], projection_matrix: perspective_matrix( - std::f32::consts::FRAC_PI_4, ctx.aspect_ratio(), 0.1, 100.0, + std::f32::consts::FRAC_PI_4, + ctx.aspect_ratio(), + 0.1, + 100.0, ), light_dir: [0.5, 1.0, 0.3], _padding: 0.0, @@ -227,16 +260,27 @@ impl ApplicationHandler for App { Ok(o) => o, Err(_) => return, }; - let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); sp.reset_counter(&ctx.queue); sp.run_slice_pass(&mut encoder); rp.prepare_indirect_draw(&mut encoder, sp.counter_buffer()); - rp.render(&mut encoder, &view, sp.output_buffer(), - wgpu::Color { r: 0.02, g: 0.02, b: 0.08, a: 1.0 }); + rp.render( + &mut encoder, + &view, + sp.output_buffer(), + wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.08, + a: 1.0, + }, + ); ctx.queue.submit(std::iter::once(encoder.finish())); output.present(); diff --git a/examples/03_physics_demo.rs b/examples/03_physics_demo.rs index e71a290..7c95e65 100644 --- a/examples/03_physics_demo.rs +++ b/examples/03_physics_demo.rs @@ -20,19 +20,22 @@ use winit::{ window::{Window, WindowId}, }; +use rust4d_core::hecs; use rust4d_core::{ - Material, ShapeRef, Tesseract4D, Transform4D, DirtyFlags, World, Tags, Name, - PhysicsConfig, PhysicsBody, RigidBody4D, StaticCollider, Hyperplane4D, + DirtyFlags, Hyperplane4D, Material, Name, PhysicsBody, PhysicsConfig, RigidBody4D, ShapeRef, + StaticCollider, Tags, Tesseract4D, Transform4D, World, }; +use rust4d_math::Vec4; +use rust4d_physics::{BodyType, PhysicsMaterial}; use rust4d_render::{ camera4d::Camera4D, context::RenderContext, - pipeline::{perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, MAX_OUTPUT_TRIANGLES}, - RenderableGeometry, CheckerboardGeometry, position_gradient_color, + pipeline::{ + perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, + MAX_OUTPUT_TRIANGLES, + }, + position_gradient_color, CheckerboardGeometry, RenderableGeometry, }; -use rust4d_math::Vec4; -use rust4d_physics::{BodyType, PhysicsMaterial}; -use rust4d_core::hecs; /// Application state struct App { @@ -72,11 +75,26 @@ impl App { // Add falling tesseracts at different heights let spawn_positions = [ - (Vec4::new(0.0, 5.0, 0.0, 0.0), Material::from_rgb(0.9, 0.3, 0.2)), - (Vec4::new(3.0, 8.0, 0.0, 0.0), Material::from_rgb(0.2, 0.9, 0.3)), - (Vec4::new(-3.0, 11.0, 0.0, 0.0), Material::from_rgb(0.2, 0.3, 0.9)), - (Vec4::new(0.0, 14.0, 2.0, 0.0), Material::from_rgb(0.9, 0.9, 0.2)), - (Vec4::new(1.5, 17.0, -1.5, 0.0), Material::from_rgb(0.9, 0.2, 0.9)), + ( + Vec4::new(0.0, 5.0, 0.0, 0.0), + Material::from_rgb(0.9, 0.3, 0.2), + ), + ( + Vec4::new(3.0, 8.0, 0.0, 0.0), + Material::from_rgb(0.2, 0.9, 0.3), + ), + ( + Vec4::new(-3.0, 11.0, 0.0, 0.0), + Material::from_rgb(0.2, 0.3, 0.9), + ), + ( + Vec4::new(0.0, 14.0, 2.0, 0.0), + Material::from_rgb(0.9, 0.9, 0.2), + ), + ( + Vec4::new(1.5, 17.0, -1.5, 0.0), + Material::from_rgb(0.9, 0.2, 0.9), + ), ]; for (i, (position, material)) in spawn_positions.iter().enumerate() { @@ -134,22 +152,29 @@ impl App { fn build_geometry(world: &World) -> RenderableGeometry { let mut geometry = RenderableGeometry::new(); - let checkerboard = CheckerboardGeometry::new( - [0.3, 0.3, 0.35, 1.0], - [0.6, 0.6, 0.65, 1.0], - 2.0, - ); + let checkerboard = + CheckerboardGeometry::new([0.3, 0.3, 0.35, 1.0], [0.6, 0.6, 0.65, 1.0], 2.0); - for (_entity, (transform, shape, material, tags)) in - world.ecs().query::<(&Transform4D, &ShapeRef, &Material, Option<&Tags>)>().iter() + for (_entity, (transform, shape, material, tags)) in world + .ecs() + .query::<(&Transform4D, &ShapeRef, &Material, Option<&Tags>)>() + .iter() { let is_dynamic = tags.map(|t| t.has("dynamic")).unwrap_or(false); if is_dynamic { - geometry.add_components_with_color(transform, shape.as_shape(), material, &position_gradient_color); + geometry.add_components_with_color( + transform, + shape.as_shape(), + material, + &position_gradient_color, + ); } else { - geometry.add_components_with_color(transform, shape.as_shape(), material, &|v, _m| { - checkerboard.color_for_position(v.x, v.z) - }); + geometry.add_components_with_color( + transform, + shape.as_shape(), + material, + &|v, _m| checkerboard.color_for_position(v.x, v.z), + ); } } @@ -171,7 +196,8 @@ impl ApplicationHandler for App { ); let render_context = pollster::block_on(RenderContext::new(window.clone())); - let mut slice_pipeline = SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); + let mut slice_pipeline = + SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); let mut render_pipeline = RenderPipeline::new(&render_context.device, render_context.config.format); @@ -198,12 +224,11 @@ impl ApplicationHandler for App { match event { WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { event, .. } - if event.state == ElementState::Pressed => { - if let PhysicalKey::Code(KeyCode::Escape) = event.physical_key { - event_loop.exit(); - } + WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => { + if let PhysicalKey::Code(KeyCode::Escape) = event.physical_key { + event_loop.exit(); } + } WindowEvent::Resized(size) => { if let Some(ctx) = &mut self.render_context { @@ -226,7 +251,8 @@ impl ApplicationHandler for App { if self.world.has_dirty_entities() { self.geometry = Self::build_geometry(&self.world); - if let (Some(sp), Some(ctx)) = (&mut self.slice_pipeline, &self.render_context) { + if let (Some(sp), Some(ctx)) = (&mut self.slice_pipeline, &self.render_context) + { sp.upload_tetrahedra( &ctx.device, &self.geometry.vertices, @@ -254,10 +280,17 @@ impl ApplicationHandler for App { sp.update_params(&ctx.queue, &slice_params); let render_uniforms = RenderUniforms { - view_matrix: [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]], + view_matrix: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ], projection_matrix: perspective_matrix( - std::f32::consts::FRAC_PI_4, ctx.aspect_ratio(), 0.1, 100.0, + std::f32::consts::FRAC_PI_4, + ctx.aspect_ratio(), + 0.1, + 100.0, ), light_dir: [0.5, 1.0, 0.3], _padding: 0.0, @@ -273,16 +306,27 @@ impl ApplicationHandler for App { Ok(o) => o, Err(_) => return, }; - let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = ctx.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); sp.reset_counter(&ctx.queue); sp.run_slice_pass(&mut encoder); rp.prepare_indirect_draw(&mut encoder, sp.counter_buffer()); - rp.render(&mut encoder, &view, sp.output_buffer(), - wgpu::Color { r: 0.02, g: 0.02, b: 0.08, a: 1.0 }); + rp.render( + &mut encoder, + &view, + sp.output_buffer(), + wgpu::Color { + r: 0.02, + g: 0.02, + b: 0.08, + a: 1.0, + }, + ); ctx.queue.submit(std::iter::once(encoder.finish())); output.present(); diff --git a/examples/04_camera_exploration.rs b/examples/04_camera_exploration.rs index 5605a1c..afa14da 100644 --- a/examples/04_camera_exploration.rs +++ b/examples/04_camera_exploration.rs @@ -34,17 +34,19 @@ use winit::{ }; use rust4d_core::{ - Material, ShapeRef, Tesseract4D, Transform4D, DirtyFlags, World, Tags, Name, - Hyperplane4D, + DirtyFlags, Hyperplane4D, Material, Name, ShapeRef, Tags, Tesseract4D, Transform4D, World, }; +use rust4d_input::CameraController; +use rust4d_math::Vec4; use rust4d_render::{ camera4d::Camera4D, context::RenderContext, - pipeline::{perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, MAX_OUTPUT_TRIANGLES}, - RenderableGeometry, CheckerboardGeometry, position_gradient_color, + pipeline::{ + perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, + MAX_OUTPUT_TRIANGLES, + }, + position_gradient_color, CheckerboardGeometry, RenderableGeometry, }; -use rust4d_math::Vec4; -use rust4d_input::CameraController; /// Application state with full camera controller integration struct App { @@ -81,15 +83,47 @@ impl App { // Each one at a different location in 4D space let tesseracts = [ // XYZ positions (W=0) - visible immediately - (Vec4::new(0.0, 0.0, 0.0, 0.0), Material::from_rgb(0.9, 0.4, 0.2), "origin"), - (Vec4::new(5.0, 0.0, 0.0, 0.0), Material::from_rgb(0.2, 0.9, 0.3), "right"), - (Vec4::new(-5.0, 0.0, 0.0, 0.0), Material::from_rgb(0.2, 0.3, 0.9), "left"), - (Vec4::new(0.0, 3.0, 0.0, 0.0), Material::from_rgb(0.9, 0.9, 0.2), "above"), - (Vec4::new(0.0, 0.0, -5.0, 0.0), Material::from_rgb(0.2, 0.9, 0.9), "forward"), + ( + Vec4::new(0.0, 0.0, 0.0, 0.0), + Material::from_rgb(0.9, 0.4, 0.2), + "origin", + ), + ( + Vec4::new(5.0, 0.0, 0.0, 0.0), + Material::from_rgb(0.2, 0.9, 0.3), + "right", + ), + ( + Vec4::new(-5.0, 0.0, 0.0, 0.0), + Material::from_rgb(0.2, 0.3, 0.9), + "left", + ), + ( + Vec4::new(0.0, 3.0, 0.0, 0.0), + Material::from_rgb(0.9, 0.9, 0.2), + "above", + ), + ( + Vec4::new(0.0, 0.0, -5.0, 0.0), + Material::from_rgb(0.2, 0.9, 0.9), + "forward", + ), // Tesseracts at different W positions (use Q/E to find them!) - (Vec4::new(3.0, 0.0, 3.0, 2.0), Material::from_rgb(0.9, 0.2, 0.9), "w+2"), - (Vec4::new(-3.0, 0.0, 3.0, -2.0), Material::from_rgb(0.6, 0.3, 0.9), "w-2"), - (Vec4::new(0.0, 1.0, -3.0, 4.0), Material::from_rgb(0.3, 0.6, 0.9), "w+4"), + ( + Vec4::new(3.0, 0.0, 3.0, 2.0), + Material::from_rgb(0.9, 0.2, 0.9), + "w+2", + ), + ( + Vec4::new(-3.0, 0.0, 3.0, -2.0), + Material::from_rgb(0.6, 0.3, 0.9), + "w-2", + ), + ( + Vec4::new(0.0, 1.0, -3.0, 4.0), + Material::from_rgb(0.3, 0.6, 0.9), + "w+4", + ), ]; for (position, material, name) in tesseracts { @@ -136,22 +170,29 @@ impl App { fn build_geometry(world: &World) -> RenderableGeometry { let mut geometry = RenderableGeometry::new(); - let checkerboard = CheckerboardGeometry::new( - [0.25, 0.25, 0.30, 1.0], - [0.55, 0.55, 0.60, 1.0], - 2.0, - ); + let checkerboard = + CheckerboardGeometry::new([0.25, 0.25, 0.30, 1.0], [0.55, 0.55, 0.60, 1.0], 2.0); - for (_entity, (transform, shape, material, tags)) in - world.ecs().query::<(&Transform4D, &ShapeRef, &Material, Option<&Tags>)>().iter() + for (_entity, (transform, shape, material, tags)) in world + .ecs() + .query::<(&Transform4D, &ShapeRef, &Material, Option<&Tags>)>() + .iter() { let is_dynamic = tags.map(|t| t.has("dynamic")).unwrap_or(false); if is_dynamic { - geometry.add_components_with_color(transform, shape.as_shape(), material, &position_gradient_color); + geometry.add_components_with_color( + transform, + shape.as_shape(), + material, + &position_gradient_color, + ); } else { - geometry.add_components_with_color(transform, shape.as_shape(), material, &|v, _m| { - checkerboard.color_for_position(v.x, v.z) - }); + geometry.add_components_with_color( + transform, + shape.as_shape(), + material, + &|v, _m| checkerboard.color_for_position(v.x, v.z), + ); } } @@ -161,7 +202,8 @@ impl App { /// Capture cursor for FPS-style controls fn capture_cursor(&mut self) { if let Some(window) = &self.window { - let grab_result = window.set_cursor_grab(CursorGrabMode::Locked) + let grab_result = window + .set_cursor_grab(CursorGrabMode::Locked) .or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined)); if grab_result.is_ok() { @@ -195,7 +237,8 @@ impl ApplicationHandler for App { ); let render_context = pollster::block_on(RenderContext::new(window.clone())); - let mut slice_pipeline = SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); + let mut slice_pipeline = + SlicePipeline::new(&render_context.device, MAX_OUTPUT_TRIANGLES); let mut render_pipeline = RenderPipeline::new(&render_context.device, render_context.config.format); @@ -275,7 +318,10 @@ impl ApplicationHandler for App { WindowEvent::MouseInput { state, button, .. } => { // Click to capture cursor - if state == ElementState::Pressed && button == MouseButton::Left && !self.cursor_captured { + if state == ElementState::Pressed + && button == MouseButton::Left + && !self.cursor_captured + { self.capture_cursor(); } self.controller.process_mouse_button(button, state); @@ -297,7 +343,8 @@ impl ApplicationHandler for App { self.last_frame = now; // Update camera via controller - self.controller.update(&mut self.camera, dt, self.cursor_captured); + self.controller + .update(&mut self.camera, dt, self.cursor_captured); // Update window title with position info if let Some(window) = &self.window { diff --git a/examples/05_audio_demo.rs b/examples/05_audio_demo.rs index a1e814b..dd2a387 100644 --- a/examples/05_audio_demo.rs +++ b/examples/05_audio_demo.rs @@ -179,6 +179,9 @@ fn main() { println!(" - with_min_distance(f32) -> Full volume within this range"); println!(" - with_max_distance(f32) -> Silent beyond this range"); - println!("\nCurrent listener position: {:?}", engine.listener_position()); + println!( + "\nCurrent listener position: {:?}", + engine.listener_position() + ); println!("\nAudio demo complete!"); } diff --git a/examples/headless_protocol.rs b/examples/headless_protocol.rs index 3921637..4b73009 100644 --- a/examples/headless_protocol.rs +++ b/examples/headless_protocol.rs @@ -70,7 +70,10 @@ impl HeadlessGpu { .expect("no GPU adapter available"); let info = adapter.get_info(); - println!("[GPU] adapter: {} ({:?}, {:?})", info.name, info.device_type, info.backend); + println!( + "[GPU] adapter: {} ({:?}, {:?})", + info.name, info.device_type, info.backend + ); let (device, queue) = pollster::block_on(adapter.request_device( &wgpu::DeviceDescriptor { @@ -140,12 +143,7 @@ impl HeadlessGpu { } /// Render one frame exactly like `RenderSystem::render_frame` and save it. - fn capture( - &mut self, - camera: &Camera4D, - tetrahedron_count: u32, - path: &Path, - ) -> u32 { + fn capture(&mut self, camera: &Camera4D, tetrahedron_count: u32, path: &Path) -> u32 { let pos = camera.position; let camera_matrix = camera.rotation_matrix(); let slice_params = SliceParams { @@ -155,7 +153,8 @@ impl HeadlessGpu { camera_matrix, camera_position: [pos.x, pos.y, pos.z, pos.w], }; - self.slice_pipeline.update_params(&self.queue, &slice_params); + self.slice_pipeline + .update_params(&self.queue, &slice_params); let proj = perspective_matrix( 45.0_f32.to_radians(), diff --git a/examples/ron_preview.rs b/examples/ron_preview.rs index 7f31735..3c43ff5 100644 --- a/examples/ron_preview.rs +++ b/examples/ron_preview.rs @@ -35,7 +35,9 @@ use winit::{ window::{CursorGrabMode, Window, WindowId}, }; -use rust4d_core::{Material, ShapeRef, Tesseract4D, Transform4D, DirtyFlags, World}; +use rust4d_core::{DirtyFlags, Material, ShapeRef, Tesseract4D, Transform4D, World}; +use rust4d_input::CameraController; +use rust4d_math::Vec4; use rust4d_render::{ camera4d::Camera4D, context::RenderContext, @@ -43,10 +45,8 @@ use rust4d_render::{ perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline, MAX_OUTPUT_TRIANGLES, }, - RenderableGeometry, position_gradient_color, + position_gradient_color, RenderableGeometry, }; -use rust4d_math::Vec4; -use rust4d_input::CameraController; // ============================================================================ // RON Asset Types @@ -229,7 +229,9 @@ impl PreviewApp { PreviewMesh::Hypersphere { segments: _ } => { // TODO: Implement proper hypersphere when available in rust4d_math // For now, use a tesseract as placeholder - println!("Note: Hypersphere not yet implemented, using tesseract as placeholder"); + println!( + "Note: Hypersphere not yet implemented, using tesseract as placeholder" + ); Box::new(Tesseract4D::new(1.0)) } }; @@ -304,7 +306,11 @@ impl PreviewApp { /// Upload geometry to GPU fn upload_geometry(&mut self) { if let (Some(ctx), Some(sp)) = (&self.render_context, &mut self.slice_pipeline) { - sp.upload_tetrahedra(&ctx.device, &self.geometry.vertices, &self.geometry.tetrahedra); + sp.upload_tetrahedra( + &ctx.device, + &self.geometry.vertices, + &self.geometry.tetrahedra, + ); } } diff --git a/examples/shape_showcase.rs b/examples/shape_showcase.rs index 06342e2..3c2121e 100644 --- a/examples/shape_showcase.rs +++ b/examples/shape_showcase.rs @@ -42,7 +42,9 @@ impl ShowcaseShape { fn geometry(&self) -> RenderableGeometry { let shape = self.template.create_shape(); let mut geom = RenderableGeometry::new(); - let material = Material { base_color: self.color }; + let material = Material { + base_color: self.color, + }; geom.add_components_with_color( &Transform4D::identity(), shape.as_ref(), @@ -78,7 +80,10 @@ impl HeadlessGpu { .expect("no GPU adapter available"); let info = adapter.get_info(); - println!("[GPU] adapter: {} ({:?}, {:?})", info.name, info.device_type, info.backend); + println!( + "[GPU] adapter: {} ({:?}, {:?})", + info.name, info.device_type, info.backend + ); let (device, queue) = pollster::block_on(adapter.request_device( &wgpu::DeviceDescriptor { @@ -134,8 +139,11 @@ impl HeadlessGpu { } fn upload_geometry(&mut self, geometry: &RenderableGeometry) { - self.slice_pipeline - .upload_tetrahedra(&self.device, &geometry.vertices, &geometry.tetrahedra); + self.slice_pipeline.upload_tetrahedra( + &self.device, + &geometry.vertices, + &geometry.tetrahedra, + ); println!( "[GPU] uploaded {} vertices, {} tetrahedra", geometry.vertex_count(), @@ -152,7 +160,8 @@ impl HeadlessGpu { camera_matrix: camera.rotation_matrix(), camera_position: [pos.x, pos.y, pos.z, pos.w], }; - self.slice_pipeline.update_params(&self.queue, &slice_params); + self.slice_pipeline + .update_params(&self.queue, &slice_params); let projection_matrix = perspective_matrix( 40.0_f32.to_radians(), @@ -302,7 +311,10 @@ fn shapes() -> Vec { }, ShowcaseShape { name: "hypersphere", - template: ShapeTemplate::Hypersphere { radius: 1.25, subdivisions: 2 }, + template: ShapeTemplate::Hypersphere { + radius: 1.25, + subdivisions: 2, + }, color: [0.35, 0.70, 1.0, 1.0], radius: 1.25, }, @@ -332,19 +344,31 @@ fn shapes() -> Vec { }, ShowcaseShape { name: "spherinder", - template: ShapeTemplate::Spherinder { radius: 1.05, half_height: 1.25, subdivisions: 2 }, + template: ShapeTemplate::Spherinder { + radius: 1.05, + half_height: 1.25, + subdivisions: 2, + }, color: [0.28, 0.95, 0.88, 1.0], radius: 1.65, }, ShowcaseShape { name: "cubinder", - template: ShapeTemplate::Cubinder { radius: 1.05, half_size: 0.9, segments: 32 }, + template: ShapeTemplate::Cubinder { + radius: 1.05, + half_size: 0.9, + segments: 32, + }, color: [0.95, 0.72, 0.24, 1.0], radius: 1.65, }, ShowcaseShape { name: "duocylinder", - template: ShapeTemplate::Duocylinder { radius_xy: 1.0, radius_zw: 1.0, segments: 32 }, + template: ShapeTemplate::Duocylinder { + radius_xy: 1.0, + radius_zw: 1.0, + segments: 32, + }, color: [0.40, 0.65, 1.0, 1.0], radius: 1.45, }, @@ -423,7 +447,11 @@ mod tests { for shape in shapes() { let geom = shape.geometry(); assert!(geom.vertex_count() > 0, "{} has no vertices", shape.name); - assert!(geom.tetrahedron_count() > 0, "{} has no tetrahedra", shape.name); + assert!( + geom.tetrahedron_count() > 0, + "{} has no tetrahedra", + shape.name + ); } } @@ -431,6 +459,9 @@ mod tests { fn test_showcase_gpu_types_still_match() { // Keeps this example honest if the pipeline types change. assert_eq!(std::mem::size_of::(), 32); - assert_eq!(std::mem::size_of::(), 16); + assert_eq!( + std::mem::size_of::(), + 16 + ); } } diff --git a/src/config.rs b/src/config.rs index ef8cf46..eab4264 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,8 +5,11 @@ //! 2. `config/user.toml` (gitignored, user overrides) //! 3. Environment variables (`R4D_SECTION__KEY`) -use figment::{Figment, providers::{Format, Toml, Env}}; -use serde::{Serialize, Deserialize}; +use figment::{ + providers::{Env, Format, Toml}, + Figment, +}; +use serde::{Deserialize, Serialize}; use std::path::Path; // Re-export PhysicsConfig from the physics crate for convenience diff --git a/src/input/input_mapper.rs b/src/input/input_mapper.rs index 23c7989..cb43eab 100644 --- a/src/input/input_mapper.rs +++ b/src/input/input_mapper.rs @@ -107,8 +107,7 @@ mod tests { #[test] fn test_key_release_ignored() { - let action = - InputMapper::map_keyboard(KeyCode::Escape, ElementState::Released, true); + let action = InputMapper::map_keyboard(KeyCode::Escape, ElementState::Released, true); assert_eq!(action, None); } diff --git a/src/input/mod.rs b/src/input/mod.rs index 8411da8..e866883 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -4,4 +4,4 @@ mod input_mapper; -pub use input_mapper::{InputMapper, InputAction}; +pub use input_mapper::{InputAction, InputMapper}; diff --git a/src/main.rs b/src/main.rs index 29b6625..2f6c40b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,6 @@ //! //! A 4D rendering engine that displays 3D cross-sections of 4D geometry. - - use winit::{ application::ApplicationHandler, event::{DeviceEvent, DeviceId, WindowEvent}, @@ -12,14 +10,14 @@ use winit::{ window::WindowId, }; -use rust4d::input::{InputMapper, InputAction}; +use rust4d::input::{InputAction, InputMapper}; use rust4d::systems::{build_geometry, RenderError, RenderSystem, SimulationSystem, WindowSystem}; use rust4d_core::SceneManager; -use rust4d_game::{CharacterController4D, CharacterConfig, scene_helpers}; -use rust4d_render::{camera4d::Camera4D, RenderableGeometry}; +use rust4d_game::{scene_helpers, CharacterConfig, CharacterController4D}; use rust4d_input::CameraController; use rust4d_math::Vec4; +use rust4d_render::{camera4d::Camera4D, RenderableGeometry}; use rust4d::config::AppConfig; @@ -53,19 +51,22 @@ impl App { // Create scene manager and load scene from file // Pass physics config from TOML to the physics engine - let mut scene_manager = SceneManager::new() - .with_physics(config.physics.to_physics_config()); + let mut scene_manager = + SceneManager::new().with_physics(config.physics.to_physics_config()); // Load scene from configured path - let scene_name = scene_manager.load_scene(&config.scene.path) + let scene_name = scene_manager + .load_scene(&config.scene.path) .unwrap_or_else(|e| { panic!("Failed to load scene '{}': {}", config.scene.path, e); }); // Instantiate and activate the scene - scene_manager.instantiate(&scene_name) + scene_manager + .instantiate(&scene_name) .unwrap_or_else(|e| panic!("Failed to instantiate scene: {}", e)); - scene_manager.push_scene(&scene_name) + scene_manager + .push_scene(&scene_name) .unwrap_or_else(|e| panic!("Failed to push scene: {}", e)); // Create player body from scene spawn point using scene_helpers @@ -85,24 +86,35 @@ impl App { } // Get player start from scene's player_spawn - let player_start = scene_manager.active_scene() + let player_start = scene_manager + .active_scene() .and_then(|s| s.player_spawn) .map(|spawn| Vec4::new(spawn[0], spawn[1], spawn[2], spawn[3])) - .unwrap_or_else(|| Vec4::new( - config.camera.start_position[0], - config.camera.start_position[1], - config.camera.start_position[2], - config.camera.start_position[3], - )); + .unwrap_or_else(|| { + Vec4::new( + config.camera.start_position[0], + config.camera.start_position[1], + config.camera.start_position[2], + config.camera.start_position[3], + ) + }); // Build GPU geometry from the world let geometry = build_geometry(scene_manager.active_world().unwrap()); - log::info!("Loaded scene '{}' with {} entities", + log::info!( + "Loaded scene '{}' with {} entities", scene_name, - scene_manager.active_world().map(|w| w.entity_count()).unwrap_or(0)); - log::info!("Total geometry: {} vertices, {} tetrahedra", - geometry.vertex_count(), geometry.tetrahedron_count()); + scene_manager + .active_world() + .map(|w| w.entity_count()) + .unwrap_or(0) + ); + log::info!( + "Total geometry: {} vertices, {} tetrahedra", + geometry.vertex_count(), + geometry.tetrahedron_count() + ); // Set camera with configured pitch limit and player start position let mut camera = Camera4D::with_pitch_limit(config.camera.pitch_limit.to_radians()); @@ -122,11 +134,14 @@ impl App { .active_scene() .and_then(|s| s.player_body_key) .map(|key| { - CharacterController4D::new(key, CharacterConfig { - move_speed: config.input.move_speed, - w_move_speed: config.input.w_move_speed, - jump_velocity: config.physics.jump_velocity, - }) + CharacterController4D::new( + key, + CharacterConfig { + move_speed: config.input.move_speed, + w_move_speed: config.input.w_move_speed, + jump_velocity: config.physics.jump_velocity, + }, + ) }); Self { @@ -141,7 +156,6 @@ impl App { simulation: SimulationSystem::new(), } } - } impl ApplicationHandler for App { @@ -182,11 +196,15 @@ impl ApplicationHandler for App { WindowEvent::KeyboardInput { event, .. } => { if let PhysicalKey::Code(key) = event.physical_key { // Map to action via InputMapper - let cursor_captured = self.window_system.as_ref() + let cursor_captured = self + .window_system + .as_ref() .map(|ws| ws.is_cursor_captured()) .unwrap_or(false); - if let Some(action) = InputMapper::map_keyboard(key, event.state, cursor_captured) { + if let Some(action) = + InputMapper::map_keyboard(key, event.state, cursor_captured) + { match action { InputAction::ToggleCursor => { if let Some(ws) = &mut self.window_system { @@ -207,7 +225,10 @@ impl ApplicationHandler for App { } InputAction::ToggleSmoothing => { let enabled = self.controller.toggle_smoothing(); - log::info!("Input smoothing: {}", if enabled { "ON" } else { "OFF" }); + log::info!( + "Input smoothing: {}", + if enabled { "ON" } else { "OFF" } + ); } } return; @@ -220,11 +241,14 @@ impl ApplicationHandler for App { WindowEvent::MouseInput { state, button, .. } => { // Map to action via InputMapper - let cursor_captured = self.window_system.as_ref() + let cursor_captured = self + .window_system + .as_ref() .map(|ws| ws.is_cursor_captured()) .unwrap_or(false); - if let Some(action) = InputMapper::map_mouse_button(button, state, cursor_captured) { + if let Some(action) = InputMapper::map_mouse_button(button, state, cursor_captured) + { if action == InputAction::ToggleCursor { if let Some(ws) = &mut self.window_system { ws.capture_cursor(); @@ -245,7 +269,9 @@ impl ApplicationHandler for App { WindowEvent::RedrawRequested => { // Run simulation - let cursor_captured = self.window_system.as_ref() + let cursor_captured = self + .window_system + .as_ref() .map(|ws| ws.is_cursor_captured()) .unwrap_or(false); diff --git a/src/systems/render.rs b/src/systems/render.rs index 66f23e8..655d2dc 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -5,15 +5,15 @@ //! - Slice and render pipelines //! - Frame rendering -use std::sync::Arc; -use winit::window::Window; +use crate::config::{CameraConfig, RenderingConfig}; use rust4d_render::{ - context::RenderContext, camera4d::Camera4D, + context::RenderContext, pipeline::{perspective_matrix, RenderPipeline, RenderUniforms, SliceParams, SlicePipeline}, RenderableGeometry, }; -use crate::config::{CameraConfig, RenderingConfig}; +use std::sync::Arc; +use winit::window::Window; /// Render error types #[derive(Debug)] @@ -57,10 +57,8 @@ impl RenderSystem { ) -> Self { let context = pollster::block_on(RenderContext::with_vsync(window, vsync)); - let slice_pipeline = SlicePipeline::new( - &context.device, - render_config.max_triangles as usize, - ); + let slice_pipeline = + SlicePipeline::new(&context.device, render_config.max_triangles as usize); let mut render_pipeline = RenderPipeline::new(&context.device, context.config.format); @@ -84,7 +82,8 @@ impl RenderSystem { pub fn resize(&mut self, width: u32, height: u32) { self.context .resize(winit::dpi::PhysicalSize::new(width, height)); - self.render_pipeline.ensure_depth_texture(&self.context.device, width, height); + self.render_pipeline + .ensure_depth_texture(&self.context.device, width, height); } /// Upload geometry to GPU @@ -167,12 +166,12 @@ impl RenderSystem { .create_view(&wgpu::TextureViewDescriptor::default()); // Create command encoder - let mut encoder = self - .context - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Render Encoder"), - }); + let mut encoder = + self.context + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); // Reset counter and run compute pass self.slice_pipeline.reset_counter(&self.context.queue); diff --git a/src/systems/simulation.rs b/src/systems/simulation.rs index 0f2e19a..70a8c14 100644 --- a/src/systems/simulation.rs +++ b/src/systems/simulation.rs @@ -6,12 +6,12 @@ //! - Physics stepping //! - Camera synchronization -use std::time::Instant; use rust4d_core::SceneManager; use rust4d_game::CharacterController4D; use rust4d_input::CameraController; use rust4d_math::Vec4; use rust4d_render::camera4d::Camera4D; +use std::time::Instant; /// Result of a simulation update pub struct SimulationResult { @@ -65,7 +65,14 @@ impl SimulationSystem { let dt = raw_dt.min(0.25); self.last_frame = now; - self.update_with_dt(dt, scene_manager, camera, controller, character, cursor_captured) + self.update_with_dt( + dt, + scene_manager, + camera, + controller, + character, + cursor_captured, + ) } /// Run one simulation frame with an explicit delta time. @@ -86,8 +93,16 @@ impl SimulationSystem { let w_input = controller.get_w_input(); // Guard against NaN/infinity from broken input state - let forward_input = if forward_input.is_finite() { forward_input } else { 0.0 }; - let right_input = if right_input.is_finite() { right_input } else { 0.0 }; + let forward_input = if forward_input.is_finite() { + forward_input + } else { + 0.0 + }; + let right_input = if right_input.is_finite() { + right_input + } else { + 0.0 + }; let w_input = if w_input.is_finite() { w_input } else { 0.0 }; // 3. Calculate movement direction in world space using camera orientation @@ -113,8 +128,7 @@ impl SimulationSystem { // Project to XZW hyperplane (zero out Y for horizontal movement) let forward_xzw = Vec4::new(camera_forward.x, 0.0, camera_forward.z, camera_forward.w).normalized(); - let right_xzw = - Vec4::new(camera_right.x, 0.0, camera_right.z, camera_right.w).normalized(); + let right_xzw = Vec4::new(camera_right.x, 0.0, camera_right.z, camera_right.w).normalized(); let ana_xzw = Vec4::new(camera_ana.x, 0.0, camera_ana.z, camera_ana.w).normalized(); // Combine WASD slice movement; clamp to unit length to prevent faster @@ -132,19 +146,23 @@ impl SimulationSystem { // 4. Apply movement to player via character controller // The controller owns the speeds and scales each component uniformly - if let (Some(character), Some(physics)) = (character, scene_manager - .active_world_mut() - .and_then(|w| w.physics_mut())) - { + if let (Some(character), Some(physics)) = ( + character, + scene_manager + .active_world_mut() + .and_then(|w| w.physics_mut()), + ) { character.apply_movement(physics, slice_dir, ana_dir); } // 5. Handle jump via character controller if controller.consume_jump() { - if let (Some(character), Some(physics)) = (character, scene_manager - .active_world_mut() - .and_then(|w| w.physics_mut())) - { + if let (Some(character), Some(physics)) = ( + character, + scene_manager + .active_world_mut() + .and_then(|w| w.physics_mut()), + ) { character.jump(physics); } } @@ -162,10 +180,10 @@ impl SimulationSystem { // This sets the camera to the physics-authoritative position BEFORE the // controller runs, so controller.update() in step 9 computes rotation // deltas from the correct starting position. - if let (Some(character), Some(physics)) = (character, scene_manager - .active_world() - .and_then(|w| w.physics())) - { + if let (Some(character), Some(physics)) = ( + character, + scene_manager.active_world().and_then(|w| w.physics()), + ) { if let Some(pos) = character.position(physics) { camera.position = pos; } @@ -178,10 +196,10 @@ impl SimulationSystem { // controller.update() in step 9 applies both rotation AND movement. We want // the rotation (mouse look) but not the movement (physics owns position). // Re-syncing here overwrites any position drift the controller introduced. - if let (Some(character), Some(physics)) = (character, scene_manager - .active_world() - .and_then(|w| w.physics())) - { + if let (Some(character), Some(physics)) = ( + character, + scene_manager.active_world().and_then(|w| w.physics()), + ) { if let Some(pos) = character.position(physics) { camera.position = pos; } diff --git a/src/systems/window.rs b/src/systems/window.rs index 948e885..610a8b4 100644 --- a/src/systems/window.rs +++ b/src/systems/window.rs @@ -2,12 +2,12 @@ //! //! Handles window creation, cursor capture/release, fullscreen toggle, and title updates. +use crate::config::WindowConfig; use std::sync::Arc; use winit::{ event_loop::ActiveEventLoop, window::{CursorGrabMode, Fullscreen, Window}, }; -use crate::config::WindowConfig; /// Manages the application window and cursor state pub struct WindowSystem { @@ -24,10 +24,7 @@ impl WindowSystem { ) -> Result { let mut attrs = Window::default_attributes() .with_title(&config.title) - .with_inner_size(winit::dpi::LogicalSize::new( - config.width, - config.height, - )); + .with_inner_size(winit::dpi::LogicalSize::new(config.width, config.height)); if config.fullscreen { attrs = attrs.with_fullscreen(Some(Fullscreen::Borderless(None))); @@ -58,7 +55,8 @@ impl WindowSystem { /// Capture cursor for FPS-style controls pub fn capture_cursor(&mut self) -> bool { - let grab_result = self.window + let grab_result = self + .window .set_cursor_grab(CursorGrabMode::Locked) .or_else(|_| self.window.set_cursor_grab(CursorGrabMode::Confined)); diff --git a/tests/gallery_scene.rs b/tests/gallery_scene.rs index 6f2b7b2..39ef45b 100644 --- a/tests/gallery_scene.rs +++ b/tests/gallery_scene.rs @@ -53,14 +53,24 @@ fn gallery_scene_loads_instantiates_and_builds_geometry() { assert!(variants.contains(&expected), "gallery missing {expected}"); } - manager.instantiate(&name).expect("gallery should instantiate"); - manager.push_scene(&name).expect("gallery should become active"); + manager + .instantiate(&name) + .expect("gallery should instantiate"); + manager + .push_scene(&name) + .expect("gallery should become active"); let world = manager.active_world().expect("active gallery world"); let geometry = build_geometry(world); - assert!(geometry.vertex_count() > 2_000, "gallery should contain substantial geometry"); - assert!(geometry.tetrahedron_count() > 8_000, "gallery should upload all primitive meshes"); + assert!( + geometry.vertex_count() > 2_000, + "gallery should contain substantial geometry" + ); + assert!( + geometry.tetrahedron_count() > 8_000, + "gallery should upload all primitive meshes" + ); let active = manager.active_scene().unwrap(); assert_eq!(active.world.entity_count(), 10); diff --git a/tests/game_integration.rs b/tests/game_integration.rs index 82735fa..81db1c2 100644 --- a/tests/game_integration.rs +++ b/tests/game_integration.rs @@ -3,10 +3,8 @@ //! These tests verify the integration between rust4d_game and rust4d_core, //! specifically the CharacterController4D working with ActiveScene and scene_helpers. -use rust4d_core::{ - ActiveScene, Scene, EntityTemplate, Transform4D, Material, ShapeTemplate, -}; -use rust4d_game::{CharacterController4D, CharacterConfig, scene_helpers}; +use rust4d_core::{ActiveScene, EntityTemplate, Material, Scene, ShapeTemplate, Transform4D}; +use rust4d_game::{scene_helpers, CharacterConfig, CharacterController4D}; use rust4d_math::Vec4; /// T1: Integration test between CharacterController4D and ActiveScene @@ -27,7 +25,7 @@ fn test_character_controller_with_active_scene() { Material::GRAY, ) .with_name("floor") - .with_tag("static") + .with_tag("static"), ); // Instantiate scene (player body creation is at the app layer) @@ -36,22 +34,30 @@ fn test_character_controller_with_active_scene() { // Create player body using scene_helpers (the single source of truth) let spawn_pos = Vec4::new(0.0, 1.0, 5.0, 0.0); let player_key = { - let physics = active.world.physics_mut().expect("Scene should have physics"); + let physics = active + .world + .physics_mut() + .expect("Scene should have physics"); scene_helpers::create_player_body(physics, spawn_pos, 0.5) }; active.player_body_key = Some(player_key); // Create CharacterController4D from the player body key - let controller = CharacterController4D::new(player_key, CharacterConfig { - move_speed: 5.0, - w_move_speed: 5.0, - jump_velocity: 10.0, - }); + let controller = CharacterController4D::new( + player_key, + CharacterConfig { + move_speed: 5.0, + w_move_speed: 5.0, + jump_velocity: 10.0, + }, + ); // Verify initial position { let physics = active.world.physics().unwrap(); - let pos = controller.position(physics).expect("Player body should exist"); + let pos = controller + .position(physics) + .expect("Player body should exist"); assert_eq!(pos, spawn_pos, "Initial position should match spawn"); } diff --git a/tests/slice_invariant.rs b/tests/slice_invariant.rs index 0cf4c2b..038c35a 100644 --- a/tests/slice_invariant.rs +++ b/tests/slice_invariant.rs @@ -89,7 +89,10 @@ impl TestRig { }, ) }); - assert!(character.is_some(), "default scene should produce a player body"); + assert!( + character.is_some(), + "default scene should produce a player body" + ); Self { scene_manager, @@ -131,7 +134,8 @@ impl TestRig { } fn release(&mut self, key: KeyCode) { - self.controller.process_keyboard(key, ElementState::Released); + self.controller + .process_keyboard(key, ElementState::Released); } fn debug_state(&self, label: &str) {