From 4109e29d4dcc6fc37874e0dce7ac23139d0471ac Mon Sep 17 00:00:00 2001 From: Rendo Date: Wed, 29 Jul 2026 18:17:05 +0500 Subject: [PATCH] refactor: split structures and functions into related modules --- src/grid/element.rs | 29 +++ src/grid/manipulation.rs | 31 ++++ src/grid/mod.rs | 86 +++++++++ src/main.rs | 376 +-------------------------------------- src/simulation.rs | 71 ++++++++ src/view/mod.rs | 0 6 files changed, 223 insertions(+), 370 deletions(-) create mode 100644 src/grid/element.rs create mode 100644 src/grid/manipulation.rs create mode 100644 src/grid/mod.rs create mode 100644 src/simulation.rs create mode 100644 src/view/mod.rs diff --git a/src/grid/element.rs b/src/grid/element.rs new file mode 100644 index 0000000..385677d --- /dev/null +++ b/src/grid/element.rs @@ -0,0 +1,29 @@ +use crate::MAX_FLUID_AMOUNT; + +#[derive(Default,Clone,Copy)] +pub enum GridElement{ + #[default] + Air, + Water{amount: u32}, + Solid, +} + +impl GridElement { + pub fn try_fill(&self, fill_amount: u32) -> Option { + return match self { + GridElement::Water { amount } => { + if *amount == MAX_FLUID_AMOUNT { + return None; + } + else if MAX_FLUID_AMOUNT - *amount < fill_amount { + return Some(MAX_FLUID_AMOUNT - *amount); + } + else { + return Some(fill_amount); + } + }, + GridElement::Air => Some(fill_amount), + _ => None, + } + } +} diff --git a/src/grid/manipulation.rs b/src/grid/manipulation.rs new file mode 100644 index 0000000..3238ae0 --- /dev/null +++ b/src/grid/manipulation.rs @@ -0,0 +1,31 @@ +use bevy::math::UVec3; +use crate::grid::{Grid, GridElement}; + +pub struct GridManipulationTools<'a> { + grid: &'a mut Grid +} + +impl<'a> GridManipulationTools<'a> { + pub fn new(grid: &'a mut Grid) -> Self { + Self { grid } + } + pub fn set_xyz(&mut self,x: u32, y: u32, z: u32, element: GridElement) { + let index = self.grid.indexify(UVec3::new(x,y,z)); + self.grid.data[index] = element; + } + pub fn fill_xyz(&mut self,x1: u32, y1: u32, z1: u32, x2: u32, y2: u32, z2: u32, element: GridElement) { + if x1 > x2 || y1 > y2 || z1 > z2 { + return; + } + + for x in x1..=x2 { + for y in y1..=y2 { + for z in z1..=z2 { + let index = self.grid.indexify(UVec3::new(x,y,z)); + + self.grid.data[index] = element.clone(); + } + } + } + } +} diff --git a/src/grid/mod.rs b/src/grid/mod.rs new file mode 100644 index 0000000..79e8333 --- /dev/null +++ b/src/grid/mod.rs @@ -0,0 +1,86 @@ +use bevy::{math::UVec3,prelude::*}; + +use crate::MAX_FLUID_AMOUNT; +use manipulation::*; + +pub use element::{GridElement}; + + +pub mod element; +pub mod manipulation; + + +#[derive(Resource,Default,Clone)] +pub struct Grid { + pub(crate) size: UVec3, + pub(crate) data: Vec +} + +impl Grid { + pub fn cube(side: u32) -> Self { + let mut data = Vec::new(); + let uside = side as usize; + data.resize(uside*uside*uside, GridElement::Air); + Self { + size: UVec3::new(side,side,side), + data + } + } + pub fn indexify(&self, vector: UVec3) -> usize{ + let clamped_vector = vector.min(self.size - UVec3::ONE); + return (clamped_vector.x + self.size.x * clamped_vector.y + self.size.x * self.size.y * clamped_vector.z) as usize; + } + pub fn deindexify(&self, index: usize) -> UVec3 { + let undex = index as u32; + return UVec3::new( + undex % self.size.x, + undex / self.size.x % self.size.y, + undex / (self.size.x*self.size.y) + ); + } + pub fn tranfer_fluid(&mut self,from: usize, to: usize, transfer_amount: u32) { + let bounds = self.indexify(self.size); + if from >= bounds || to >= bounds { + return; + } + + let from_new_value; + let to_new_value; + + match (self.data[to],self.data[from]) { + (GridElement::Air,GridElement::Water { amount: mut water_amount }) => { + to_new_value = Some(GridElement::Water { amount: transfer_amount }); + water_amount = water_amount.checked_sub(transfer_amount).unwrap_or(0); + from_new_value = if water_amount == 0 {Some(GridElement::Air)} else {Some(GridElement::Water { amount: water_amount })}; + }, + (GridElement::Water { amount: mut to_amount }, GridElement::Water { amount: mut from_amount }) => { + let transfer_amount = from_amount.min(transfer_amount); + let delta = MAX_FLUID_AMOUNT - to_amount; + if delta < transfer_amount { + to_amount = MAX_FLUID_AMOUNT; + from_amount -= delta; + } + else { + to_amount += transfer_amount; + from_amount -= transfer_amount; + } + to_new_value = Some(GridElement::Water { amount: to_amount }); + from_new_value = if from_amount == 0 {Some(GridElement::Air)} else {Some(GridElement::Water { amount: from_amount })}; + } + _ => {return;}, + + } + + if let Some(value) = to_new_value { + self.data[to] = value; + } + if let Some(value) = from_new_value { + self.data[from] = value; + } + + } + pub fn manipulate<'a>(&'a mut self) -> GridManipulationTools<'a> { + GridManipulationTools::new(self) + } +} + diff --git a/src/main.rs b/src/main.rs index bb3a028..3823b7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ -use bevy::{math::UVec3, prelude::*}; +use bevy::prelude::*; +use crate::grid::Grid; +use crate::simulation::tick; -//vibecode -use bevy::color::palettes::css::{DARK_GRAY,ROYAL_BLUE}; +pub mod simulation; +pub mod view; +pub mod grid; fn main() { App::new() @@ -9,9 +12,6 @@ fn main() { .insert_resource(Grid::cube(16)) .insert_resource(Time::::from_hz(120.)) .add_systems(FixedUpdate, tick) - //Vibecode - .add_systems(Startup, setup_visuals) - .add_systems(Update, sync_visuals) .run(); } @@ -19,371 +19,7 @@ const MAX_FLUID_AMOUNT: u32 = 1000; const MAX_VERTICAL_FLOW: u32 = MAX_FLUID_AMOUNT; const MAX_HORIZONTAL_FLOW: u32 = MAX_FLUID_AMOUNT/2; -#[derive(Default,Clone,Copy)] -enum GridElement{ - #[default] - Air, - Water{amount: u32}, - Solid, -} -impl GridElement { - pub fn try_fill(&self, fill_amount: u32) -> Option { - return match self { - GridElement::Water { amount } => { - if *amount == MAX_FLUID_AMOUNT { - return None; - } - else if MAX_FLUID_AMOUNT - *amount < fill_amount { - return Some(MAX_FLUID_AMOUNT - *amount); - } - else { - return Some(fill_amount); - } - }, - GridElement::Air => Some(fill_amount), - _ => None, - } - } -} -#[derive(Resource,Default,Clone)] -struct Grid { - size: UVec3, - data: Vec -} -impl Grid { - pub fn cube(side: u32) -> Self { - let mut data = Vec::new(); - let uside = side as usize; - data.resize(uside*uside*uside, GridElement::Air); - Self { - size: UVec3::new(side,side,side), - data - } - } - pub fn indexify(&self, vector: UVec3) -> usize{ - let clamped_vector = vector.min(self.size - UVec3::ONE); - return (clamped_vector.x + self.size.x * clamped_vector.y + self.size.x * self.size.y * clamped_vector.z) as usize; - } - pub fn deindexify(&self, index: usize) -> UVec3 { - let undex = index as u32; - return UVec3::new( - undex % self.size.x, - undex / self.size.x % self.size.y, - undex / (self.size.x*self.size.y) - ); - } - /*pub fn extract(&mut self, index: usize, extract_amount: u32) -> u32 { - if index >= self.indexify(self.size) { - return 0; - } - - let mut cleanup = false; - let mut extracted_amount = 0; - if let GridElement::Water { amount } = &mut self.data[index] { - if extract_amount >= *amount { - extracted_amount = amount.clone(); - *amount = 0; - cleanup = true; - } - else { - *amount -= extract_amount; - extracted_amount = extract_amount; - } - } - if cleanup { - self.data[index] = GridElement::None; - } - - return extracted_amount; - } - pub fn fill(&mut self, index: usize, fill_amount: u32) -> u32 { - if index >= self.indexify(self.size) { - return fill_amount; - } - - if let GridElement::None = self.data[index] { - self.data[index] = GridElement::Water { amount: fill_amount } - } - else if let GridElement::Water { amount } = &mut self.data[index] { - *amount = MAX_FLUID_AMOUNT.min(*amount + fill_amount); - } - }*/ - pub fn tranfer_fluid(&mut self,from: usize, to: usize, transfer_amount: u32) { - let bounds = self.indexify(self.size); - if from >= bounds || to >= bounds { - return; - } - - let from_new_value; - let to_new_value; - - match (self.data[to],self.data[from]) { - (GridElement::Air,GridElement::Water { amount: mut water_amount }) => { - to_new_value = Some(GridElement::Water { amount: transfer_amount }); - water_amount = water_amount.checked_sub(transfer_amount).unwrap_or(0); - from_new_value = if water_amount == 0 {Some(GridElement::Air)} else {Some(GridElement::Water { amount: water_amount })}; - }, - (GridElement::Water { amount: mut to_amount }, GridElement::Water { amount: mut from_amount }) => { - let transfer_amount = from_amount.min(transfer_amount); - let delta = MAX_FLUID_AMOUNT - to_amount; - if delta < transfer_amount { - to_amount = MAX_FLUID_AMOUNT; - from_amount -= delta; - } - else { - to_amount += transfer_amount; - from_amount -= transfer_amount; - } - to_new_value = Some(GridElement::Water { amount: to_amount }); - from_new_value = if from_amount == 0 {Some(GridElement::Air)} else {Some(GridElement::Water { amount: from_amount })}; - } - _ => {return;}, - - } - - if let Some(value) = to_new_value { - self.data[to] = value; - } - if let Some(value) = from_new_value { - self.data[from] = value; - } - - } - pub fn manipulate<'a>(&'a mut self) -> GridManipulationTools<'a> { - GridManipulationTools { grid: self } - } -} - -struct GridManipulationTools<'a> { - grid: &'a mut Grid -} - -impl<'a> GridManipulationTools<'a> { - pub fn set_xyz(&mut self,x: u32, y: u32, z: u32, element: GridElement) { - let index = self.grid.indexify(UVec3::new(x,y,z)); - self.grid.data[index] = element; - } - pub fn fill_xyz(&mut self,x1: u32, y1: u32, z1: u32, x2: u32, y2: u32, z2: u32, element: GridElement) { - if x1 > x2 || y1 > y2 || z1 > z2 { - return; - } - - for x in x1..=x2 { - for y in y1..=y2 { - for z in z1..=z2 { - let index = self.grid.indexify(UVec3::new(x,y,z)); - - self.grid.data[index] = element.clone(); - } - } - } - } -} - -enum GridAction{ - MoveLiquid{ - from: UVec3, - to: UVec3, - amount: u32, - } -} - -fn tick(mut grid: ResMut) { - let mut stack: Vec = Vec::new(); - - for x in 0..grid.size.x { - for y in 0..grid.size.y { - for z in 0..grid.size.z { - let vector = UVec3::new(x,y,z); - let index = grid.indexify(vector); - - match grid.data[index as usize] { - GridElement::Water { amount } => { - let mut transfer_amount = amount.clone(); - - if let Some(gravity_vector) = vector.checked_sub(UVec3::new(0,1,0)) { - if let Some(gravity_amount) = grid.data - [grid.indexify(gravity_vector)] - .try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) { - stack.push(GridAction::MoveLiquid { from: vector, to: gravity_vector, amount: gravity_amount }); - transfer_amount -= gravity_amount; - } - } - - transfer_amount = transfer_amount.min(MAX_HORIZONTAL_FLOW) / 4; - if transfer_amount > 0 { - for lookup_vector in [UVec3::new(1,0,0),UVec3::new(0,0,1)] { - if let Some(toward_vector) = vector.checked_add(lookup_vector) { - if let Some(toward_amount) = grid.data - [grid.indexify(toward_vector)] - .try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) { - stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount }); - } - } - - if let Some(toward_vector) = vector.checked_sub(lookup_vector) { - if let Some(toward_amount) = grid.data - [grid.indexify(toward_vector)] - .try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) { - stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount }); - } - } - } - } - }, - _ => {}, - } - - } - } - } - - for action in stack { - match action { - GridAction::MoveLiquid { from, to, amount } => { - let (from_index, to_index) = (grid.indexify(from), grid.indexify(to)); - grid.tranfer_fluid(from_index, to_index, amount); - }, - } - } -} - -//Vibe code - -const VISUAL_LEVELS: usize = 8; - -/// Materials bucketed by fill level, plus a material for solid blocks. -#[derive(Resource)] -struct GridMaterials { - water_levels: Vec>, - solid: Handle, -} - -/// Marker for a per-cell visual cube. -#[derive(Component)] -struct CellVisual; - -/// Maps grid index -> spawned entity, so `sync_visuals` can update in O(1) -/// per cell without searching. -#[derive(Resource)] -struct CellEntities(Vec); - -fn setup_visuals( - mut commands: Commands, - mut grid: ResMut, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let mut builder = grid.manipulate(); - builder.fill_xyz(0, 0, 0, 6, 6, 6, GridElement::Solid); - builder.fill_xyz(1, 1, 1, 5, 6, 5, GridElement::Water { amount: MAX_FLUID_AMOUNT }); - builder.set_xyz(6,1,3,GridElement::Air); - - let center = grid.size.as_vec3() * 0.5; - - // Camera positioned to see the whole cube from an angle. - commands.spawn(( - Camera3d::default(), - Transform::from_xyz( - center.x + grid.size.x as f32 * 1.4, - grid.size.y as f32 * 1.8, - center.z + grid.size.z as f32 * 1.4, - ) - .looking_at(center, Vec3::Y), - )); - - // Simple directional light. - commands.spawn(( - DirectionalLight { - illuminance: 8_000.0, - shadow_maps_enabled: false, - ..default() - }, - Transform::from_xyz(10.0, 30.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), - )); - - let cube_mesh = meshes.add(Cuboid::new(0.9, 0.9, 0.9)); - - // Pre-build a handful of translucent blue materials, one per "fullness" bucket, - // so we don't need a unique material asset per cell. - let water_levels: Vec> = (0..VISUAL_LEVELS) - .map(|i| { - let t = i as f32 / (VISUAL_LEVELS - 1) as f32; - materials.add(StandardMaterial { - base_color: Color::from(ROYAL_BLUE).with_alpha(0.15 + 0.75 * t), - alpha_mode: AlphaMode::Blend, - ..default() - }) - }) - .collect(); - - let solid = materials.add(StandardMaterial { - base_color: Color::from(DARK_GRAY), - ..default() - }); - - // Spawn one cube per grid cell, in the same order that `Grid::indexify` - // expects (x fastest, then y, then z), so `entities[i]` lines up with - // `grid.data[i]`. All start hidden. - let mut entities = Vec::with_capacity(grid.data.len()); - for z in 0..grid.size.z { - for y in 0..grid.size.y { - for x in 0..grid.size.x { - let pos = UVec3::new(x, y, z).as_vec3() + Vec3::splat(0.5); - let entity = commands - .spawn(( - Mesh3d(cube_mesh.clone()), - MeshMaterial3d(water_levels[0].clone()), - Transform::from_translation(pos), - Visibility::Hidden, - CellVisual, - )) - .id(); - entities.push(entity); - } - } - } - - commands.insert_resource(GridMaterials { water_levels, solid }); - commands.insert_resource(CellEntities(entities)); -} - -fn sync_visuals( - grid: Res, - grid_materials: Res, - cell_entities: Res, - mut query: Query<(&mut Visibility, &mut MeshMaterial3d), With>, -) { - // Grid is a plain Resource (not ResMut here), so `is_changed` only - // fires on frames where `tick` actually ran and mutated it. - if !grid.is_changed() { - return; - } - - for (index, element) in grid.data.iter().enumerate() { - let entity = cell_entities.0[index]; - let Ok((mut visibility, mut material)) = query.get_mut(entity) else { - continue; - }; - - match element { - GridElement::Water { amount } => { - *visibility = Visibility::Visible; - let level = ((*amount as u64 * (VISUAL_LEVELS as u64 - 1)) - / MAX_FLUID_AMOUNT as u64) as usize; - material.0 = grid_materials.water_levels[level].clone(); - } - GridElement::Solid => { - *visibility = Visibility::Visible; - material.0 = grid_materials.solid.clone(); - } - GridElement::Air => { - *visibility = Visibility::Hidden; - } - } - } -} diff --git a/src/simulation.rs b/src/simulation.rs new file mode 100644 index 0000000..b369d6b --- /dev/null +++ b/src/simulation.rs @@ -0,0 +1,71 @@ +use bevy::{prelude::*,math::UVec3}; +use crate::{MAX_VERTICAL_FLOW,MAX_HORIZONTAL_FLOW,grid::{Grid,GridElement}}; + +enum GridAction{ + MoveLiquid{ + from: UVec3, + to: UVec3, + amount: u32, + } +} + +pub fn tick(mut grid: ResMut) { + let mut stack: Vec = Vec::new(); + + for x in 0..grid.size.x { + for y in 0..grid.size.y { + for z in 0..grid.size.z { + let vector = UVec3::new(x,y,z); + let index = grid.indexify(vector); + + match grid.data[index as usize] { + GridElement::Water { amount } => { + let mut transfer_amount = amount.clone(); + + if let Some(gravity_vector) = vector.checked_sub(UVec3::new(0,1,0)) { + if let Some(gravity_amount) = grid.data + [grid.indexify(gravity_vector)] + .try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) { + stack.push(GridAction::MoveLiquid { from: vector, to: gravity_vector, amount: gravity_amount }); + transfer_amount -= gravity_amount; + } + } + + transfer_amount = transfer_amount.min(MAX_HORIZONTAL_FLOW) / 4; + if transfer_amount > 0 { + for lookup_vector in [UVec3::new(1,0,0),UVec3::new(0,0,1)] { + if let Some(toward_vector) = vector.checked_add(lookup_vector) { + if let Some(toward_amount) = grid.data + [grid.indexify(toward_vector)] + .try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) { + stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount }); + } + } + + if let Some(toward_vector) = vector.checked_sub(lookup_vector) { + if let Some(toward_amount) = grid.data + [grid.indexify(toward_vector)] + .try_fill(transfer_amount.min(MAX_VERTICAL_FLOW)) { + stack.push(GridAction::MoveLiquid { from: vector, to: toward_vector, amount: toward_amount }); + } + } + } + } + }, + _ => {}, + } + + } + } + } + + for action in stack { + match action { + GridAction::MoveLiquid { from, to, amount } => { + let (from_index, to_index) = (grid.indexify(from), grid.indexify(to)); + grid.tranfer_fluid(from_index, to_index, amount); + }, + } + } +} + diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..e69de29