use crate::math::{Fix128, Vec3Fix};
use crate::solver::RigidBody;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ForceField {
Directional {
direction: Vec3Fix,
strength: Fix128,
},
Point {
center: Vec3Fix,
strength: Fix128,
repulsive: bool,
max_force: Fix128,
},
Drag {
coefficient: Fix128,
},
Buoyancy {
surface_y: Fix128,
density: Fix128,
drag: Fix128,
},
Vortex {
center: Vec3Fix,
axis: Vec3Fix,
strength: Fix128,
falloff_radius: Fix128,
},
Explosion {
center: Vec3Fix,
strength: Fix128,
radius: Fix128,
falloff_power: Fix128,
},
Magnetic {
position: Vec3Fix,
moment: Vec3Fix,
strength: Fix128,
},
}
#[derive(Clone, Debug)]
pub struct ForceFieldInstance {
pub field: ForceField,
pub affected_bodies: Option<Vec<usize>>,
pub enabled: bool,
}
impl ForceFieldInstance {
#[inline]
#[must_use]
pub const fn new(field: ForceField) -> Self {
Self {
field,
affected_bodies: None,
enabled: true,
}
}
#[must_use]
pub fn with_affected_bodies(mut self, bodies: Vec<usize>) -> Self {
self.affected_bodies = Some(bodies);
self
}
#[inline]
fn affects(&self, body_index: usize) -> bool {
if !self.enabled {
return false;
}
self.affected_bodies
.as_ref()
.map_or(true, |list| list.contains(&body_index))
}
}
#[must_use]
pub fn compute_force(field: &ForceField, body: &RigidBody) -> Vec3Fix {
match field {
ForceField::Directional {
direction,
strength,
} => *direction * *strength,
ForceField::Point {
center,
strength,
repulsive,
max_force,
} => {
let delta = *center - body.position;
let dist_sq = delta.length_squared();
if dist_sq.is_zero() {
return Vec3Fix::ZERO;
}
let dist = dist_sq.sqrt();
let direction = delta / dist;
let force_mag = *strength / dist_sq;
let clamped = if force_mag > *max_force {
*max_force
} else {
force_mag
};
if *repulsive {
-direction * clamped
} else {
direction * clamped
}
}
ForceField::Drag { coefficient } => {
let speed_sq = body.velocity.length_squared();
if speed_sq.is_zero() {
return Vec3Fix::ZERO;
}
let speed = speed_sq.sqrt();
let drag_dir = body.velocity / speed;
-drag_dir * (*coefficient * speed)
}
ForceField::Buoyancy {
surface_y,
density,
drag,
} => {
let depth = *surface_y - body.position.y;
if depth <= Fix128::ZERO {
return Vec3Fix::ZERO;
}
let buoyancy = Vec3Fix::new(Fix128::ZERO, *density * depth, Fix128::ZERO);
let water_drag = if body.velocity.length_squared().is_zero() {
Vec3Fix::ZERO
} else {
body.velocity * (-*drag)
};
buoyancy + water_drag
}
ForceField::Vortex {
center,
axis,
strength,
falloff_radius,
} => {
let delta = body.position - *center;
let axis_norm = axis.normalize();
let along_axis = axis_norm * delta.dot(axis_norm);
let radial = delta - along_axis;
let dist = radial.length();
if dist.is_zero() || falloff_radius.is_zero() {
return Vec3Fix::ZERO;
}
let tangent = axis_norm.cross(radial.normalize());
let falloff = if dist < *falloff_radius {
Fix128::ONE
} else {
*falloff_radius / dist
};
tangent * (*strength * falloff)
}
ForceField::Explosion {
center,
strength,
radius,
falloff_power,
} => {
let delta = body.position - *center;
let dist_sq = delta.length_squared();
if dist_sq.is_zero() {
return Vec3Fix::ZERO;
}
let dist = dist_sq.sqrt();
if dist >= *radius {
return Vec3Fix::ZERO;
}
let direction = delta / dist;
let ratio = Fix128::ONE - dist / *radius;
let power_int = falloff_power.hi.max(0) as u32;
let mut falloff = Fix128::ONE;
let mut i = 0u32;
while i < power_int {
falloff = falloff * ratio;
i += 1;
}
direction * (*strength * falloff)
}
ForceField::Magnetic {
position,
moment,
strength,
} => {
let delta = body.position - *position;
let dist_sq = delta.length_squared();
if dist_sq.is_zero() {
return Vec3Fix::ZERO;
}
let dist = dist_sq.sqrt();
let r_cubed = dist_sq * dist;
if r_cubed.is_zero() {
return Vec3Fix::ZERO;
}
let force_mag = *strength / r_cubed;
let moment_dir = moment.normalize();
let alignment = delta.dot(moment_dir);
let signed_mag = force_mag * alignment / dist;
moment_dir * signed_mag
}
}
}
pub fn apply_force_fields(fields: &[ForceFieldInstance], bodies: &mut [RigidBody], dt: Fix128) {
for (body_idx, body) in bodies.iter_mut().enumerate() {
if body.is_static() {
continue;
}
let mut total_force = Vec3Fix::ZERO;
for field_inst in fields {
if !field_inst.affects(body_idx) {
continue;
}
total_force = total_force + compute_force(&field_inst.field, body);
}
let acceleration = total_force * body.inv_mass;
body.velocity = body.velocity + acceleration * dt;
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_directional_force() {
let field = ForceField::Directional {
direction: Vec3Fix::UNIT_Y,
strength: Fix128::from_int(10),
};
let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
let force = compute_force(&field, &body);
assert_eq!(force.y.hi, 10);
}
#[test]
fn test_drag_force() {
let field = ForceField::Drag {
coefficient: Fix128::from_int(1),
};
let mut body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
body.velocity = Vec3Fix::from_int(10, 0, 0);
let force = compute_force(&field, &body);
assert!(force.x < Fix128::ZERO, "Drag should oppose motion");
}
#[test]
fn test_point_gravity() {
let field = ForceField::Point {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(100),
repulsive: false,
max_force: Fix128::from_int(1000),
};
let body = RigidBody::new(Vec3Fix::from_int(10, 0, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x < Fix128::ZERO,
"Point gravity should pull toward center"
);
}
#[test]
fn test_buoyancy() {
let field = ForceField::Buoyancy {
surface_y: Fix128::from_int(5),
density: Fix128::from_int(10),
drag: Fix128::ONE,
};
let body = RigidBody::new(Vec3Fix::from_int(0, 2, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(force.y > Fix128::ZERO, "Buoyancy should push up");
let body_above = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
let force_above = compute_force(&field, &body_above);
assert!(
force_above.y.is_zero() && force_above.x.is_zero(),
"No buoyancy above surface"
);
}
#[test]
fn test_apply_force_fields() {
let fields = vec![ForceFieldInstance::new(ForceField::Directional {
direction: Vec3Fix::UNIT_X,
strength: Fix128::from_int(10),
})];
let mut bodies = vec![RigidBody::new(Vec3Fix::ZERO, Fix128::ONE)];
let dt = Fix128::from_ratio(1, 60);
apply_force_fields(&fields, &mut bodies, dt);
assert!(
bodies[0].velocity.x > Fix128::ZERO,
"Force should accelerate body"
);
}
#[test]
fn test_affected_bodies_filter() {
let field = ForceFieldInstance::new(ForceField::Directional {
direction: Vec3Fix::UNIT_X,
strength: Fix128::from_int(100),
})
.with_affected_bodies(vec![0]);
assert!(field.affects(0));
assert!(!field.affects(1));
}
#[test]
fn test_vortex() {
let field = ForceField::Vortex {
center: Vec3Fix::ZERO,
axis: Vec3Fix::UNIT_Y,
strength: Fix128::from_int(10),
falloff_radius: Fix128::from_int(100),
};
let body = RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.z.abs() > Fix128::ZERO,
"Vortex should produce tangential force"
);
}
#[test]
fn test_explosion_pushes_outward() {
let field = ForceField::Explosion {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(100),
radius: Fix128::from_int(20),
falloff_power: Fix128::ONE,
};
let body = RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x > Fix128::ZERO,
"Explosion should push body away from center"
);
}
#[test]
fn test_explosion_zero_beyond_radius() {
let field = ForceField::Explosion {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(100),
radius: Fix128::from_int(10),
falloff_power: Fix128::ONE,
};
let body = RigidBody::new(Vec3Fix::from_int(15, 0, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
"No explosion force beyond radius"
);
}
#[test]
fn test_explosion_at_center() {
let field = ForceField::Explosion {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(100),
radius: Fix128::from_int(10),
falloff_power: Fix128::ONE,
};
let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
"No force at explosion center (zero distance)"
);
}
#[test]
fn test_explosion_falloff() {
let field_linear = ForceField::Explosion {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(100),
radius: Fix128::from_int(20),
falloff_power: Fix128::ONE,
};
let field_quadratic = ForceField::Explosion {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(100),
radius: Fix128::from_int(20),
falloff_power: Fix128::from_int(2),
};
let body = RigidBody::new(Vec3Fix::from_int(10, 0, 0), Fix128::ONE);
let force_linear = compute_force(&field_linear, &body);
let force_quadratic = compute_force(&field_quadratic, &body);
assert!(
force_quadratic.x < force_linear.x,
"Quadratic falloff should produce weaker force than linear at same distance"
);
}
#[test]
fn test_explosion_apply_to_body() {
let fields = vec![ForceFieldInstance::new(ForceField::Explosion {
center: Vec3Fix::ZERO,
strength: Fix128::from_int(1000),
radius: Fix128::from_int(50),
falloff_power: Fix128::ONE,
})];
let mut bodies = vec![RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE)];
let dt = Fix128::from_ratio(1, 60);
apply_force_fields(&fields, &mut bodies, dt);
assert!(
bodies[0].velocity.x > Fix128::ZERO,
"Explosion should accelerate body away"
);
}
#[test]
fn test_magnetic_along_dipole_axis() {
let field = ForceField::Magnetic {
position: Vec3Fix::ZERO,
moment: Vec3Fix::UNIT_X,
strength: Fix128::from_int(1000),
};
let body = RigidBody::new(Vec3Fix::from_int(2, 0, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x.abs() > Fix128::ZERO,
"Magnetic force should exist along dipole axis"
);
}
#[test]
fn test_magnetic_perpendicular_to_dipole() {
let field = ForceField::Magnetic {
position: Vec3Fix::ZERO,
moment: Vec3Fix::UNIT_X,
strength: Fix128::from_int(1000),
};
let body = RigidBody::new(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
"No magnetic force perpendicular to dipole axis"
);
}
#[test]
fn test_magnetic_at_dipole_position() {
let field = ForceField::Magnetic {
position: Vec3Fix::ZERO,
moment: Vec3Fix::UNIT_Z,
strength: Fix128::from_int(100),
};
let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
let force = compute_force(&field, &body);
assert!(
force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
"No force at the dipole location"
);
}
#[test]
fn test_magnetic_inverse_cube_falloff() {
let field = ForceField::Magnetic {
position: Vec3Fix::ZERO,
moment: Vec3Fix::UNIT_X,
strength: Fix128::from_int(10000),
};
let body_near = RigidBody::new(Vec3Fix::from_int(2, 0, 0), Fix128::ONE);
let body_far = RigidBody::new(Vec3Fix::from_int(4, 0, 0), Fix128::ONE);
let force_near = compute_force(&field, &body_near);
let force_far = compute_force(&field, &body_far);
assert!(
force_near.x.abs() > force_far.x.abs(),
"Magnetic force should be stronger at closer distance (1/r^3 falloff)"
);
}
#[test]
fn test_magnetic_apply_to_body() {
let fields = vec![ForceFieldInstance::new(ForceField::Magnetic {
position: Vec3Fix::ZERO,
moment: Vec3Fix::UNIT_X,
strength: Fix128::from_int(10000),
})];
let mut bodies = vec![RigidBody::new(Vec3Fix::from_int(3, 0, 0), Fix128::ONE)];
let dt = Fix128::from_ratio(1, 60);
apply_force_fields(&fields, &mut bodies, dt);
assert!(
bodies[0].velocity.x.abs() > Fix128::ZERO,
"Magnetic field should accelerate body"
);
}
}