#[cfg(feature = "fluids")]
pub use pravash::shallow::ShallowWater;
#[cfg(feature = "fluids")]
pub use pravash::sph::SphSolver;
pub use pravash::{FluidConfig, FluidMaterial, FluidParticle};
use serde::{Deserialize, Serialize};
use crate::world::World;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidEmitter {
pub material: FluidMaterial,
pub rate: f32,
pub velocity: [f32; 3],
pub spread: f32,
pub max_particles: usize,
pub active: bool,
#[serde(skip)]
pub accumulator: f32,
}
impl FluidEmitter {
pub fn new(material: FluidMaterial) -> Self {
Self {
material,
rate: 100.0,
velocity: [0.0, -1.0, 0.0],
spread: 0.1,
max_particles: 1000,
active: true,
accumulator: 0.0,
}
}
pub fn with_rate(mut self, rate: f32) -> Self {
self.rate = rate;
self
}
pub fn with_velocity(mut self, velocity: [f32; 3]) -> Self {
self.velocity = velocity;
self
}
pub fn with_spread(mut self, spread: f32) -> Self {
self.spread = spread;
self
}
pub fn with_max_particles(mut self, max: usize) -> Self {
self.max_particles = max;
self
}
#[must_use]
pub fn particles_to_emit(&mut self, dt: f32) -> u32 {
if !self.active {
return 0;
}
self.accumulator += self.rate * dt;
let count = self.accumulator as u32;
self.accumulator -= count as f32;
count
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidBody {
pub volume: f32,
pub drag_coefficient: f32,
#[serde(skip)]
pub submerged: bool,
#[serde(skip)]
pub buoyancy_force: [f32; 3],
}
impl FluidBody {
pub fn new(volume: f32) -> Self {
Self {
volume,
drag_coefficient: 0.5,
submerged: false,
buoyancy_force: [0.0; 3],
}
}
pub fn with_drag(mut self, drag: f32) -> Self {
self.drag_coefficient = drag;
self
}
#[must_use]
pub fn compute_buoyancy(
&self,
fluid_density: f64,
gravity: [f64; 3],
submersion_fraction: f32,
) -> [f32; 3] {
let displaced_volume = self.volume * submersion_fraction;
let buoyancy_mag = fluid_density as f32 * displaced_volume;
[
-gravity[0] as f32 * buoyancy_mag,
-gravity[1] as f32 * buoyancy_mag,
-gravity[2] as f32 * buoyancy_mag,
]
}
}
pub struct FluidSimulation {
pub solver: SphSolver,
pub config: FluidConfig,
pub particles: Vec<FluidParticle>,
pub viscosity: f64,
pub active: bool,
}
impl FluidSimulation {
pub fn new(config: FluidConfig, material: FluidMaterial) -> Self {
Self {
solver: SphSolver::new(),
config,
particles: Vec::new(),
viscosity: material.viscosity,
active: true,
}
}
pub fn water_2d() -> Self {
Self::new(FluidConfig::water_2d(), FluidMaterial::WATER)
}
pub fn step(&mut self) {
if !self.active || self.particles.is_empty() {
return;
}
let _ = self
.solver
.step(&mut self.particles, &self.config, self.viscosity);
}
#[must_use]
#[inline]
pub fn particle_count(&self) -> usize {
self.particles.len()
}
pub fn add_particle(&mut self, particle: FluidParticle) {
self.particles.push(particle);
}
pub fn clear(&mut self) {
self.particles.clear();
}
}
pub fn step_fluid_simulation(world: &mut World) {
if let Some(sim) = world.get_resource_mut::<FluidSimulation>() {
sim.step();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fluid_emitter_new() {
let emitter = FluidEmitter::new(FluidMaterial::WATER);
assert!(emitter.active);
assert_eq!(emitter.rate, 100.0);
assert_eq!(emitter.max_particles, 1000);
}
#[test]
fn fluid_emitter_builders() {
let emitter = FluidEmitter::new(FluidMaterial::OIL)
.with_rate(50.0)
.with_velocity([1.0, 0.0, 0.0])
.with_spread(0.5)
.with_max_particles(500);
assert_eq!(emitter.rate, 50.0);
assert_eq!(emitter.velocity, [1.0, 0.0, 0.0]);
assert_eq!(emitter.max_particles, 500);
}
#[test]
fn fluid_emitter_particles_to_emit() {
let mut emitter = FluidEmitter::new(FluidMaterial::WATER);
emitter.rate = 60.0; let count = emitter.particles_to_emit(1.0 / 60.0); assert_eq!(count, 1);
}
#[test]
fn fluid_emitter_inactive() {
let mut emitter = FluidEmitter::new(FluidMaterial::WATER);
emitter.active = false;
assert_eq!(emitter.particles_to_emit(1.0), 0);
}
#[test]
fn fluid_emitter_accumulates() {
let mut emitter = FluidEmitter::new(FluidMaterial::WATER);
emitter.rate = 30.0;
let c1 = emitter.particles_to_emit(1.0 / 60.0);
let c2 = emitter.particles_to_emit(1.0 / 60.0);
assert_eq!(c1 + c2, 1);
}
#[test]
fn fluid_body_new() {
let body = FluidBody::new(0.5);
assert_eq!(body.volume, 0.5);
assert!(!body.submerged);
}
#[test]
fn fluid_body_buoyancy() {
let body = FluidBody::new(1.0); let force = body.compute_buoyancy(1000.0, [0.0, -9.81, 0.0], 1.0);
assert!((force[1] - 9810.0).abs() < 1.0);
}
#[test]
fn fluid_body_partial_submersion() {
let body = FluidBody::new(1.0);
let full = body.compute_buoyancy(1000.0, [0.0, -9.81, 0.0], 1.0);
let half = body.compute_buoyancy(1000.0, [0.0, -9.81, 0.0], 0.5);
assert!((half[1] - full[1] * 0.5).abs() < 1.0);
}
#[test]
fn fluid_simulation_new() {
let sim = FluidSimulation::water_2d();
assert!(sim.active);
assert_eq!(sim.particle_count(), 0);
}
#[test]
fn fluid_simulation_add_particle() {
let mut sim = FluidSimulation::water_2d();
sim.add_particle(FluidParticle::new_2d(0.5, 0.5, 1.0));
assert_eq!(sim.particle_count(), 1);
}
#[test]
fn fluid_simulation_step() {
let mut sim = FluidSimulation::water_2d();
sim.add_particle(FluidParticle::new_2d(0.5, 0.8, 1.0));
sim.step();
let p = &sim.particles[0];
assert!(p.position[1] < 0.8); }
#[test]
fn fluid_simulation_inactive() {
let mut sim = FluidSimulation::water_2d();
sim.add_particle(FluidParticle::new_2d(0.5, 0.5, 1.0));
sim.active = false;
let pos_before = sim.particles[0].position;
sim.step();
assert_eq!(sim.particles[0].position, pos_before);
}
#[test]
fn fluid_simulation_clear() {
let mut sim = FluidSimulation::water_2d();
sim.add_particle(FluidParticle::new_2d(0.5, 0.5, 1.0));
sim.clear();
assert_eq!(sim.particle_count(), 0);
}
#[test]
fn fluid_emitter_as_component() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, FluidEmitter::new(FluidMaterial::WATER))
.unwrap();
assert!(world.has_component::<FluidEmitter>(e));
}
#[test]
fn fluid_body_as_component() {
let mut world = World::new();
let e = world.spawn();
world.insert_component(e, FluidBody::new(0.1)).unwrap();
assert!(world.has_component::<FluidBody>(e));
}
#[test]
fn fluid_simulation_as_resource() {
let mut world = World::new();
world.insert_resource(FluidSimulation::water_2d());
let sim = world.get_resource::<FluidSimulation>().unwrap();
assert_eq!(sim.particle_count(), 0);
}
#[test]
fn step_fluid_system() {
let mut world = World::new();
let mut sim = FluidSimulation::water_2d();
sim.add_particle(FluidParticle::new_2d(0.5, 0.8, 1.0));
world.insert_resource(sim);
step_fluid_simulation(&mut world);
let sim = world.get_resource::<FluidSimulation>().unwrap();
assert!(sim.particles[0].position[1] < 0.8);
}
#[test]
fn fluid_material_presets() {
let water = FluidMaterial::WATER;
let air = FluidMaterial::AIR;
let honey = FluidMaterial::HONEY;
let lava = FluidMaterial::LAVA;
assert!(water.density > air.density);
assert!(honey.viscosity > water.viscosity);
assert!(lava.density > water.density);
}
#[test]
fn fluid_emitter_serde() {
let emitter = FluidEmitter::new(FluidMaterial::WATER).with_rate(200.0);
let json = serde_json::to_string(&emitter).unwrap();
let decoded: FluidEmitter = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.rate, 200.0);
}
#[test]
fn fluid_body_serde() {
let body = FluidBody::new(2.5).with_drag(0.8);
let json = serde_json::to_string(&body).unwrap();
let decoded: FluidBody = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.volume, 2.5);
assert_eq!(decoded.drag_coefficient, 0.8);
}
}