diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md
new file mode 100644
index 0000000..d1973ab
--- /dev/null
+++ b/.github/workflows/pull_request_template.md
@@ -0,0 +1,41 @@
+## Summary
+
+-
+
+
+Why this change?
+
+
+
+
+
+---
+
+## What changed
+
+-
+
+---
+
+## Impact
+
+- [ ] Bug fix
+- [ ] Behavior change
+- [ ] Performance improvement
+- [ ] Refactor / cleanup
+- [ ] Documentation
+
+---
+
+## Testing
+
+- [ ] Unit tests
+- [ ] Manual testing
+- [ ] Not tested (explain why)
+
+---
+
+## Notes for reviewers
+
+-
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 37f2ca2..8844d8d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,3 +1,4 @@
+---
name: Release
on:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index c45445b..b92a273 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -125,7 +125,9 @@ jobs:
- run: zip simulation-engine-${CARGO_RELEASE_TAG}-linux-x64.zip target/release/simulation_engine
- run: |
if ! gh release view $CARGO_RELEASE_TAG > /dev/null 2>&1; then
- gh release create $CARGO_RELEASE_TAG simulation-engine-${CARGO_RELEASE_TAG}-linux-x64.zip --generate-notes --verify-tag
+ gh release create $CARGO_RELEASE_TAG \
+ simulation-engine-${CARGO_RELEASE_TAG}-linux-x64.zip \
+ --generate-notes --verify-tag
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 396a9b1..a73f92f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
diff --git a/engine/constraints/mod.rs b/engine/constraints/mod.rs
new file mode 100644
index 0000000..9dc04df
--- /dev/null
+++ b/engine/constraints/mod.rs
@@ -0,0 +1,2 @@
+/// Constraints module for physics engine constraints and limitations
+pub mod physics_constraints;
diff --git a/engine/constraints/physics_constraints.rs b/engine/constraints/physics_constraints.rs
new file mode 100644
index 0000000..07471f7
--- /dev/null
+++ b/engine/constraints/physics_constraints.rs
@@ -0,0 +1,88 @@
+/// Physics constraints and boundary conditions
+/// Handles various types of constraints that can be applied to physics objects
+use nalgebra::Vector2;
+
+/// Represents different types of constraints that can be applied to physics objects
+#[derive(Debug, Clone)]
+pub enum Constraint {
+ /// Fixed position constraint - object cannot move from a specific point
+ FixedPosition(Vector2),
+ /// Distance constraint - maintains a fixed distance between two objects
+ Distance { target_distance: f32 },
+ /// Axis constraint - restricts movement to a specific axis
+ Axis { axis: Vector2, position: f32 },
+ /// Boundary constraint - keeps object within rectangular bounds
+ Boundary {
+ min: Vector2,
+ max: Vector2,
+ },
+}
+
+/// A constraint that can be applied to a physics object
+#[derive(Debug, Clone)]
+pub struct PhysicsConstraint {
+ pub constraint_type: Constraint,
+ pub stiffness: f32, // How strongly the constraint is enforced (0.0 to 1.0)
+ pub damping: f32, // Damping factor for constraint forces
+}
+
+impl Default for PhysicsConstraint {
+ fn default() -> Self {
+ Self {
+ constraint_type: Constraint::Boundary {
+ min: Vector2::new(-100.0, -100.0),
+ max: Vector2::new(100.0, 100.0),
+ },
+ stiffness: 1.0,
+ damping: 0.1,
+ }
+ }
+}
+
+impl PhysicsConstraint {
+ /// Creates a new boundary constraint with the given bounds
+ pub fn boundary(min: Vector2, max: Vector2) -> Self {
+ Self {
+ constraint_type: Constraint::Boundary { min, max },
+ stiffness: 1.0,
+ damping: 0.1,
+ }
+ }
+
+ /// Applies the constraint to a position and velocity, returning the constrained values
+ pub fn apply(&self, position: &mut Vector2, velocity: &mut Vector2, delta_time: f32) {
+ match &self.constraint_type {
+ Constraint::Boundary { min, max } => {
+ self.apply_boundary_constraint(position, velocity, *min, *max, delta_time);
+ }
+ Constraint::FixedPosition(fixed_pos) => {
+ *position = *fixed_pos;
+ *velocity = Vector2::zeros();
+ }
+ _ => {
+ // Other constraint types not yet implemented
+ }
+ }
+ }
+
+ fn apply_boundary_constraint(
+ &self,
+ position: &mut Vector2,
+ velocity: &mut Vector2,
+ min: Vector2,
+ max: Vector2,
+ _delta_time: f32,
+ ) {
+ // Clamp position to bounds
+ position.x = position.x.max(min.x).min(max.x);
+ position.y = position.y.max(min.y).min(max.y);
+
+ // Apply damping when hitting boundaries
+ if position.x <= min.x || position.x >= max.x {
+ velocity.x *= 1.0 - self.damping;
+ }
+ if position.y <= min.y || position.y >= max.y {
+ velocity.y *= 1.0 - self.damping;
+ }
+ }
+}
diff --git a/engine/lib.rs b/engine/lib.rs
index 7f6cd46..b52016c 100644
--- a/engine/lib.rs
+++ b/engine/lib.rs
@@ -1,4 +1,6 @@
+pub mod constraints;
pub mod managers;
+pub mod numerics;
pub mod physics;
#[cfg(feature = "python")]
diff --git a/engine/managers/simulation_manager.rs b/engine/managers/simulation_manager.rs
index 1c9ee7d..41e502b 100644
--- a/engine/managers/simulation_manager.rs
+++ b/engine/managers/simulation_manager.rs
@@ -26,7 +26,7 @@ impl Simulation {
state: SimulationState::Stopped,
time_step,
duration,
- physics_engine: PhysicsEngine::new(),
+ physics_engine: PhysicsEngine::new().expect("Failed to create physics engine"),
}
}
@@ -35,7 +35,9 @@ impl Simulation {
println!("Simulation {} started", self.id);
while self.state == SimulationState::Running {
self.update();
- self.physics_engine.simulate(self.time_step);
+ self.physics_engine
+ .simulate(self.time_step)
+ .expect("Simulation step failed");
thread::sleep(Duration::from_millis((self.time_step * 1000.0) as u64));
}
}
diff --git a/engine/numerics/constants.rs b/engine/numerics/constants.rs
new file mode 100644
index 0000000..4ba6945
--- /dev/null
+++ b/engine/numerics/constants.rs
@@ -0,0 +1,21 @@
+//! Numerical constants for physics and general calculations
+//! Centralizes all magic numbers and thresholds used throughout the engine
+
+/// Minimum distance threshold to avoid division by very small values in collision calculations
+pub const MIN_DISTANCE_EPSILON: f32 = 1e-6;
+
+/// Maximum velocity magnitude to prevent numerical explosions
+pub const MAX_VELOCITY: f32 = 1000.0;
+
+/// Minimum mass threshold (objects below this are considered invalid)
+pub const MIN_MASS: f32 = 1e-3;
+
+/// Maximum mass threshold to prevent numerical issues
+pub const MAX_MASS: f32 = 1e6;
+
+/// Gravity constant (standard Earth gravity)
+pub const EARTH_GRAVITY: f32 = 9.81;
+
+/// Time step epsilon for floating point comparisons
+#[allow(dead_code)]
+pub const TIME_STEP_EPSILON: f32 = 1e-7;
diff --git a/engine/numerics/mod.rs b/engine/numerics/mod.rs
new file mode 100644
index 0000000..52fd404
--- /dev/null
+++ b/engine/numerics/mod.rs
@@ -0,0 +1,7 @@
+/// Numerics module providing constants and utilities for numerical computations
+pub mod constants;
+pub mod utilities;
+
+// Re-export commonly used items for convenience
+pub use constants::*;
+pub use utilities::*;
diff --git a/engine/numerics/utilities.rs b/engine/numerics/utilities.rs
new file mode 100644
index 0000000..3fc14dd
--- /dev/null
+++ b/engine/numerics/utilities.rs
@@ -0,0 +1,69 @@
+use crate::numerics::constants::*;
+/// Numerical utilities and helper functions
+/// Provides safe mathematical operations and validation functions
+use nalgebra::Vector2;
+
+/// Clamps a value between min and max
+#[inline]
+pub fn clamp(value: T, min: T, max: T) -> T {
+ if value < min {
+ min
+ } else if value > max {
+ max
+ } else {
+ value
+ }
+}
+
+/// Safely divides two floats, returning a default value if denominator is too small
+#[inline]
+pub fn safe_divide(numerator: f32, denominator: f32, default: f32) -> f32 {
+ if denominator.abs() < MIN_DISTANCE_EPSILON {
+ default
+ } else {
+ numerator / denominator
+ }
+}
+
+/// Clamps velocity components to prevent numerical instability
+#[inline]
+pub fn clamp_velocity(velocity: &mut Vector2) {
+ velocity.x = clamp(velocity.x, -MAX_VELOCITY, MAX_VELOCITY);
+ velocity.y = clamp(velocity.y, -MAX_VELOCITY, MAX_VELOCITY);
+}
+
+/// Validates that mass is within acceptable bounds
+#[inline]
+pub fn validate_mass(mass: f32) -> Result {
+ if mass < MIN_MASS {
+ Err(format!(
+ "Mass {} is below minimum threshold {}",
+ mass, MIN_MASS
+ ))
+ } else if mass > MAX_MASS {
+ Err(format!(
+ "Mass {} exceeds maximum threshold {}",
+ mass, MAX_MASS
+ ))
+ } else if !mass.is_finite() {
+ Err("Mass must be finite".to_string())
+ } else {
+ Ok(mass)
+ }
+}
+
+/// Validates that a vector contains only finite values
+#[inline]
+pub fn validate_vector(vec: &Vector2) -> Result<(), String> {
+ if !vec.x.is_finite() || !vec.y.is_finite() {
+ Err(format!("Vector contains non-finite values: {:?}", vec))
+ } else {
+ Ok(())
+ }
+}
+
+/// Checks if two floats are approximately equal within epsilon
+#[inline]
+pub fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
+ (a - b).abs() < epsilon
+}
diff --git a/engine/physics/data_cleaner.rs b/engine/physics/data_cleaner.rs
index fe7822d..f682067 100644
--- a/engine/physics/data_cleaner.rs
+++ b/engine/physics/data_cleaner.rs
@@ -190,9 +190,13 @@ mod tests {
#[test]
fn test_cleanup_stats() {
- let mut engine = PhysicsEngine::new();
- engine.add_object(Vector2::new(0.0, 0.0), Vector2::new(1.0, 0.0), 1.0);
- engine.add_object(Vector2::new(100.0, 100.0), Vector2::new(0.0, 0.0), 1.0); // Stationary, far away
+ let mut engine = PhysicsEngine::new().unwrap();
+ engine
+ .add_object(Vector2::new(0.0, 0.0), Vector2::new(1.0, 0.0), 1.0)
+ .unwrap();
+ engine
+ .add_object(Vector2::new(100.0, 100.0), Vector2::new(0.0, 0.0), 1.0)
+ .unwrap(); // Stationary, far away
let cleaner = DataCleaner::new().with_cleanup_threshold(10);
let stats = cleaner.get_stats(&engine);
@@ -206,9 +210,13 @@ mod tests {
#[test]
fn test_cleanup_inactive_objects() {
- let mut engine = PhysicsEngine::new();
- engine.add_object(Vector2::new(0.0, 0.0), Vector2::new(1.0, 0.0), 1.0); // Active
- engine.add_object(Vector2::new(100.0, 100.0), Vector2::new(0.0, 0.0), 1.0); // Inactive, far away
+ let mut engine = PhysicsEngine::new().unwrap();
+ engine
+ .add_object(Vector2::new(0.0, 0.0), Vector2::new(1.0, 0.0), 1.0)
+ .unwrap(); // Active
+ engine
+ .add_object(Vector2::new(100.0, 100.0), Vector2::new(0.0, 0.0), 1.0)
+ .unwrap(); // Inactive, far away
let cleaner = DataCleaner::new();
let removed_count = cleaner.cleanup_inactive_objects(&mut engine, 0.0);
@@ -222,11 +230,13 @@ mod tests {
#[test]
fn test_needs_cleanup() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
// Add many objects to trigger cleanup threshold
for i in 0..1500 {
- engine.add_object(Vector2::new(i as f32, 0.0), Vector2::new(0.0, 0.0), 1.0);
+ engine
+ .add_object(Vector2::new(i as f32, 0.0), Vector2::new(0.0, 0.0), 1.0)
+ .unwrap();
}
let cleaner = DataCleaner::new().with_cleanup_threshold(1000);
diff --git a/engine/physics/physics_engine.rs b/engine/physics/physics_engine.rs
index 34e436a..25af506 100644
--- a/engine/physics/physics_engine.rs
+++ b/engine/physics/physics_engine.rs
@@ -1,7 +1,47 @@
use nalgebra::Vector2;
-/// Minimum distance threshold to avoid division by very small values in collision calculations
+// TODO: These are duplicated from the numerics module.
+// The proper solution is to import from crate::numerics::*,
+// but there's currently a module resolution issue preventing this.
+// Once resolved, these inline implementations should be removed
+// and replaced with: use crate::numerics::*;
const MIN_DISTANCE_EPSILON: f32 = 1e-6;
+const MAX_VELOCITY: f32 = 1000.0;
+
+fn clamp(value: T, min: T, max: T) -> T {
+ if value < min {
+ min
+ } else if value > max {
+ max
+ } else {
+ value
+ }
+}
+
+fn safe_divide(numerator: f32, denominator: f32, default: f32) -> f32 {
+ if denominator.abs() < MIN_DISTANCE_EPSILON {
+ default
+ } else {
+ numerator / denominator
+ }
+}
+
+fn clamp_velocity(velocity: &mut Vector2) {
+ velocity.x = clamp(velocity.x, -MAX_VELOCITY, MAX_VELOCITY);
+ velocity.y = clamp(velocity.y, -MAX_VELOCITY, MAX_VELOCITY);
+}
+
+fn validate_vector(vec: &Vector2) -> Result<(), String> {
+ if !vec.x.is_finite() || !vec.y.is_finite() {
+ Err(format!("Vector contains non-finite values: {:?}", vec))
+ } else {
+ Ok(())
+ }
+}
+
+fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
+ (a - b).abs() < epsilon
+}
#[derive(Clone, Debug)]
pub struct Object {
@@ -12,13 +52,36 @@ pub struct Object {
}
impl Object {
- pub fn new(position: Vector2, velocity: Vector2, mass: f32) -> Self {
- Object {
+ pub fn new(position: Vector2, velocity: Vector2, mass: f32) -> Result {
+ // Temporary inline validation
+ if mass < 1e-3 {
+ return Err(format!("Mass {} is below minimum threshold {}", mass, 1e-3));
+ }
+ if mass > 1e6 {
+ return Err(format!("Mass {} exceeds maximum threshold {}", mass, 1e6));
+ }
+ if !mass.is_finite() {
+ return Err("Mass must be finite".to_string());
+ }
+ if !position.x.is_finite() || !position.y.is_finite() {
+ return Err(format!(
+ "Position contains non-finite values: {:?}",
+ position
+ ));
+ }
+ if !velocity.x.is_finite() || !velocity.y.is_finite() {
+ return Err(format!(
+ "Velocity contains non-finite values: {:?}",
+ velocity
+ ));
+ }
+
+ Ok(Object {
position,
velocity,
mass,
radius: 0.5,
- }
+ })
}
}
@@ -31,26 +94,31 @@ pub struct PhysicsEngine {
impl Default for PhysicsEngine {
fn default() -> Self {
- Self::new()
+ Self::new().expect("Failed to create default PhysicsEngine")
}
}
impl PhysicsEngine {
- pub fn new() -> Self {
- PhysicsEngine {
- objects: vec![Object::new(
- Vector2::new(0.0, 0.0),
- Vector2::new(0.0, 0.0),
- 1.0,
- )], // default object
+ pub fn new() -> Result {
+ let default_object = Object::new(Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0), 1.0)?;
+
+ Ok(PhysicsEngine {
+ objects: vec![default_object], // default object
gravity: Vector2::new(0.0, -9.81), // downward acceleration
bounds: (Vector2::new(-100.0, -100.0), Vector2::new(100.0, 100.0)),
- }
+ })
}
#[allow(dead_code)]
- pub fn add_object(&mut self, position: Vector2, velocity: Vector2, mass: f32) {
- self.objects.push(Object::new(position, velocity, mass));
+ pub fn add_object(
+ &mut self,
+ position: Vector2,
+ velocity: Vector2,
+ mass: f32,
+ ) -> Result<(), String> {
+ let object = Object::new(position, velocity, mass)?;
+ self.objects.push(object);
+ Ok(())
}
#[allow(dead_code)]
@@ -63,77 +131,153 @@ impl PhysicsEngine {
let len = self.objects.len();
for i in 0..len {
for j in (i + 1)..len {
- let pos1 = self.objects[i].position;
- let pos2 = self.objects[j].position;
- let rad1 = self.objects[i].radius;
- let rad2 = self.objects[j].radius;
- let diff = pos1 - pos2;
- let distance = diff.norm();
- if distance < rad1 + rad2 && distance > MIN_DISTANCE_EPSILON {
- // Elastic collision in 2D
+ if let Some((impulse, separation)) = self.detect_and_resolve_collision(i, j) {
+ // Apply impulse to velocities
let m1 = self.objects[i].mass;
let m2 = self.objects[j].mass;
- let v1 = self.objects[i].velocity;
- let v2 = self.objects[j].velocity;
- let normal = diff / distance; // unit vector from 2 to 1
- let relative_velocity = v1 - v2;
- let velocity_along_normal = relative_velocity.dot(&normal);
- if velocity_along_normal > 0.0 {
- continue; // objects separating
- }
- let restitution = 1.0; // elastic
- let impulse_scalar =
- -(1.0 + restitution) * velocity_along_normal / (1.0 / m1 + 1.0 / m2);
- let impulse = impulse_scalar * normal;
self.objects[i].velocity += impulse / m1;
self.objects[j].velocity -= impulse / m2;
- // Separate them to avoid sticking
- let overlap = (rad1 + rad2) - distance;
- let separation = normal * (overlap / 2.0);
+
+ // Separate positions to avoid sticking
self.objects[i].position += separation;
self.objects[j].position -= separation;
+
+ // Clamp velocities after collision
+ clamp_velocity(&mut self.objects[i].velocity);
+ clamp_velocity(&mut self.objects[j].velocity);
}
}
}
}
- pub fn simulate(&mut self, time_step: f32) {
+ /// Detect collision between two objects and compute resolution impulse and separation
+ fn detect_and_resolve_collision(
+ &self,
+ i: usize,
+ j: usize,
+ ) -> Option<(Vector2, Vector2)> {
+ let pos1 = self.objects[i].position;
+ let pos2 = self.objects[j].position;
+ let rad1 = self.objects[i].radius;
+ let rad2 = self.objects[j].radius;
+ let diff = pos1 - pos2;
+ let distance = diff.norm();
+
+ // Check if objects are colliding
+ if distance >= rad1 + rad2 || distance <= MIN_DISTANCE_EPSILON {
+ return None;
+ }
+
+ // Unit normal vector from object j to i
+ let normal = safe_divide(1.0, distance, 1.0) * diff;
+
+ // Relative velocity
+ let v1 = self.objects[i].velocity;
+ let v2 = self.objects[j].velocity;
+ let relative_velocity = v1 - v2;
+ let velocity_along_normal = relative_velocity.dot(&normal);
+
+ // Don't resolve if objects are separating
+ if velocity_along_normal > 0.0 {
+ return None;
+ }
+
+ // Elastic collision impulse calculation
+ let m1 = self.objects[i].mass;
+ let m2 = self.objects[j].mass;
+ let restitution = 1.0; // perfectly elastic
+ let impulse_scalar = -(1.0 + restitution) * velocity_along_normal * (m1 * m2) / (m1 + m2);
+ let impulse = impulse_scalar * normal;
+
+ // Calculate separation to prevent overlap
+ let overlap = (rad1 + rad2) - distance;
+ let separation = normal * (overlap / 2.0);
+
+ Some((impulse, separation))
+ }
+
+ pub fn simulate(&mut self, time_step: f32) -> Result<(), String> {
+ if time_step <= 0.0 || !time_step.is_finite() {
+ return Err(format!("Invalid time step: {}", time_step));
+ }
+
log::info!("Running physics simulation...");
+
+ // Phase 1: Integration (apply forces and update positions)
+ self.integrate(time_step)?;
+
+ // Phase 2: Collision Detection and Resolution
+ self.handle_wall_collisions();
+ self.handle_collisions();
+
+ // Phase 3: Validation and clamping
+ self.validate_and_clamp();
+
+ for obj in &self.objects {
+ log::info!(
+ "Object position: {:?}, velocity: {:?}",
+ obj.position,
+ obj.velocity
+ );
+ }
+
+ Ok(())
+ }
+
+ /// Phase 1: Integration - apply forces and update positions/velocities
+ fn integrate(&mut self, time_step: f32) -> Result<(), String> {
for obj in &mut self.objects {
// Apply gravity
obj.velocity += self.gravity * time_step;
- // Update position
+
+ // Clamp velocity before integration to prevent explosions
+ clamp_velocity(&mut obj.velocity);
+
+ // Update position using symplectic Euler integration
obj.position += obj.velocity * time_step;
- // Wall collisions (clamp to bounds)
- if obj.position.x < self.bounds.0.x {
- obj.position.x = self.bounds.0.x;
- } else if obj.position.x > self.bounds.1.x {
- obj.position.x = self.bounds.1.x;
- }
- if obj.position.y < self.bounds.0.y {
- obj.position.y = self.bounds.0.y;
- } else if obj.position.y > self.bounds.1.y {
- obj.position.y = self.bounds.1.y;
- }
- // If hit wall, reverse velocity component and dampen
- if obj.position.x <= self.bounds.0.x || obj.position.x >= self.bounds.1.x {
+ // Validate position is finite
+ validate_vector(&obj.position)?;
+ }
+ Ok(())
+ }
+
+ /// Phase 2: Handle wall boundary collisions
+ fn handle_wall_collisions(&mut self) {
+ for obj in &mut self.objects {
+ let mut bounced = false;
+
+ // Clamp position to bounds
+ obj.position.x = clamp(obj.position.x, self.bounds.0.x, self.bounds.1.x);
+ obj.position.y = clamp(obj.position.y, self.bounds.0.y, self.bounds.1.y);
+
+ // Bounce off walls with damping
+ if approx_eq(obj.position.x, self.bounds.0.x, MIN_DISTANCE_EPSILON)
+ || approx_eq(obj.position.x, self.bounds.1.x, MIN_DISTANCE_EPSILON)
+ {
obj.velocity.x = -obj.velocity.x * 0.8;
+ bounced = true;
}
- if obj.position.y <= self.bounds.0.y || obj.position.y >= self.bounds.1.y {
+ if approx_eq(obj.position.y, self.bounds.0.y, MIN_DISTANCE_EPSILON)
+ || approx_eq(obj.position.y, self.bounds.1.y, MIN_DISTANCE_EPSILON)
+ {
obj.velocity.y = -obj.velocity.y * 0.8;
+ bounced = true;
}
- }
- // Handle object-object collisions
- self.handle_collisions();
+ if bounced {
+ clamp_velocity(&mut obj.velocity);
+ }
+ }
+ }
- for obj in &self.objects {
- log::info!(
- "Object position: {:?}, velocity: {:?}",
- obj.position,
- obj.velocity
- );
+ /// Phase 3: Validate and clamp all values to maintain numerical stability
+ fn validate_and_clamp(&mut self) {
+ for obj in &mut self.objects {
+ clamp_velocity(&mut obj.velocity);
+ // Ensure position stays within reasonable bounds (expand if needed)
+ obj.position.x = clamp(obj.position.x, -1e6, 1e6);
+ obj.position.y = clamp(obj.position.y, -1e6, 1e6);
}
}
@@ -154,9 +298,9 @@ mod tests {
#[test]
fn test_gravity() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
let time_step = 0.1;
- engine.simulate(time_step);
+ engine.simulate(time_step).unwrap();
// After one step, velocity.y should be gravity.y * time_step
let expected_velocity_y = -9.81 * time_step;
assert!((engine.objects[0].velocity.y - expected_velocity_y).abs() < 0.01);
@@ -169,11 +313,11 @@ mod tests {
#[test]
fn test_wall_collision() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
engine.objects[0].position.x = 99.5;
engine.objects[0].velocity.x = 10.0; // towards upper bound
let time_step = 0.1;
- engine.simulate(time_step);
+ engine.simulate(time_step).unwrap();
// Should bounce off upper bound
assert!((engine.objects[0].position.x - 100.0).abs() < 0.01);
assert!(engine.objects[0].velocity.x < 0.0); // reversed and dampened
@@ -181,9 +325,11 @@ mod tests {
#[test]
fn test_add_object() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
let initial_count = engine.objects.len();
- engine.add_object(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0), 5.0);
+ engine
+ .add_object(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0), 5.0)
+ .unwrap();
assert_eq!(engine.objects.len(), initial_count + 1);
let obj = &engine.objects[1];
assert_eq!(obj.position, Vector2::new(1.0, 2.0));
@@ -193,9 +339,79 @@ mod tests {
#[test]
fn test_set_gravity() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
let new_gravity = Vector2::new(0.0, -5.0);
engine.set_gravity(new_gravity);
assert_eq!(engine.gravity, new_gravity);
}
+
+ #[test]
+ fn test_invalid_mass() {
+ assert!(Object::new(Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0), 0.0).is_err());
+ assert!(Object::new(Vector2::new(0.0, 0.0), Vector2::new(0.0, 0.0), -1.0).is_err());
+ assert!(Object::new(
+ Vector2::new(0.0, 0.0),
+ Vector2::new(0.0, 0.0),
+ f32::INFINITY
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn test_invalid_time_step() {
+ let mut engine = PhysicsEngine::new().unwrap();
+ assert!(engine.simulate(0.0).is_err());
+ assert!(engine.simulate(-1.0).is_err());
+ assert!(engine.simulate(f32::INFINITY).is_err());
+ }
+
+ #[test]
+ fn test_velocity_clamping() {
+ let mut engine = PhysicsEngine::new().unwrap();
+ engine.objects[0].velocity = Vector2::new(2000.0, -1500.0);
+ engine.simulate(0.1).unwrap();
+ // Velocity should be clamped to MAX_VELOCITY
+ assert!(engine.objects[0].velocity.x.abs() <= MAX_VELOCITY);
+ assert!(engine.objects[0].velocity.y.abs() <= MAX_VELOCITY);
+ }
+
+ #[test]
+ fn test_stress_large_timestep() {
+ let mut engine = PhysicsEngine::new().unwrap();
+ // Test with a very large time step
+ engine.simulate(10.0).unwrap();
+ // Should not have NaN or infinite values
+ for obj in &engine.objects {
+ assert!(obj.position.x.is_finite());
+ assert!(obj.position.y.is_finite());
+ assert!(obj.velocity.x.is_finite());
+ assert!(obj.velocity.y.is_finite());
+ }
+ }
+
+ #[test]
+ fn test_collision_resolution() {
+ let mut engine = PhysicsEngine::new().unwrap();
+ // Add two objects that will collide
+ engine
+ .add_object(Vector2::new(1.0, 0.0), Vector2::new(-1.0, 0.0), 1.0)
+ .unwrap();
+ // Position them to overlap slightly
+ engine.objects[0].position = Vector2::new(0.0, 0.0);
+ engine.objects[1].position = Vector2::new(0.9, 0.0); // Close enough to collide
+
+ let initial_vel_0 = engine.objects[0].velocity;
+ let initial_vel_1 = engine.objects[1].velocity;
+
+ engine.simulate(0.1).unwrap();
+
+ // Velocities should have changed due to collision
+ assert_ne!(engine.objects[0].velocity, initial_vel_0);
+ assert_ne!(engine.objects[1].velocity, initial_vel_1);
+
+ // Objects should be separated
+ let distance = (engine.objects[0].position - engine.objects[1].position).norm();
+ assert!(distance >= engine.objects[0].radius + engine.objects[1].radius - 0.1);
+ // Allow small tolerance
+ }
}
diff --git a/examples/custom_physics.rs b/examples/custom_physics.rs
index f61d75e..19a0d80 100644
--- a/examples/custom_physics.rs
+++ b/examples/custom_physics.rs
@@ -4,18 +4,22 @@ use simulation_engine::physics::physics_engine::PhysicsEngine;
fn main() {
println!("Starting custom physics example...");
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().expect("Failed to create physics engine");
// Add multiple objects
- engine.add_object(Vector2::new(0.0, 0.0), Vector2::new(1.0, 0.0), 1.0);
- engine.add_object(Vector2::new(5.0, 0.0), Vector2::new(-0.5, 0.0), 1.5);
+ engine
+ .add_object(Vector2::new(0.0, 0.0), Vector2::new(1.0, 0.0), 1.0)
+ .expect("Failed to add object");
+ engine
+ .add_object(Vector2::new(5.0, 0.0), Vector2::new(-0.5, 0.0), 1.5)
+ .expect("Failed to add object");
// Set custom gravity
engine.set_gravity(Vector2::new(0.0, -5.0));
// Run simulation for 10 steps
for step in 0..10 {
- engine.simulate(0.1);
+ engine.simulate(0.1).expect("Simulation failed");
let objects = engine.get_objects();
println!("Step {}:", step);
for (i, obj) in objects.iter().enumerate() {
diff --git a/examples/data_cleaner.rs b/examples/data_cleaner.rs
index bf449fb..231d27e 100644
--- a/examples/data_cleaner.rs
+++ b/examples/data_cleaner.rs
@@ -4,7 +4,7 @@ use simulation_engine::physics::{data_cleaner::DataCleaner, physics_engine::Phys
fn main() {
println!("Starting data cleaner example...");
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().expect("Failed to create physics engine");
// Add many objects to simulate a busy simulation
println!("Adding 2000 objects to simulation...");
@@ -18,7 +18,9 @@ fn main() {
// Most objects are stationary (inactive)
Vector2::new(0.0, 0.0)
};
- engine.add_object(position, velocity, 1.0);
+ engine
+ .add_object(position, velocity, 1.0)
+ .expect("Failed to add object");
}
// Create data cleaner
@@ -37,7 +39,7 @@ fn main() {
// Run simulation for a bit to let objects move
println!("\nRunning simulation...");
for _ in 0..50 {
- engine.simulate(0.1);
+ engine.simulate(0.1).expect("Simulation failed");
}
// Check cleanup status
diff --git a/target/debug/simulation_engine b/target/debug/simulation_engine
index ea9c4d0..376d15d 100755
Binary files a/target/debug/simulation_engine and b/target/debug/simulation_engine differ
diff --git a/tests/sdk_tests.rs b/tests/sdk_tests.rs
index 925f604..8d9c229 100644
--- a/tests/sdk_tests.rs
+++ b/tests/sdk_tests.rs
@@ -11,24 +11,26 @@ fn test_simulation_manager_creation() {
#[test]
fn test_physics_engine_creation() {
- let engine = PhysicsEngine::new();
+ let engine = PhysicsEngine::new().unwrap();
let objects = engine.get_objects();
assert_eq!(objects.len(), 1); // Default object
}
#[test]
fn test_physics_engine_add_object() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
let initial_count = engine.get_objects().len();
- engine.add_object(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0), 5.0);
+ engine
+ .add_object(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0), 5.0)
+ .unwrap();
assert_eq!(engine.get_objects().len(), initial_count + 1);
}
#[test]
fn test_physics_engine_simulate() {
- let mut engine = PhysicsEngine::new();
+ let mut engine = PhysicsEngine::new().unwrap();
let initial_pos = engine.get_objects()[0].position;
- engine.simulate(0.1);
+ engine.simulate(0.1).unwrap();
let new_pos = engine.get_objects()[0].position;
assert_ne!(initial_pos, new_pos); // Position should change due to gravity
}