|
3 | 3 | //! Spawns blobs of particles at regular intervals. Each particle is |
4 | 4 | //! individually advected through the flow field using RK2 integration. |
5 | 5 |
|
6 | | -use super::velocity::{velocity_at, is_inside_airfoil, psi_at}; |
| 6 | +use super::velocity::{ |
| 7 | + is_inside_airfoil, is_inside_polygon, psi_at, psi_at_with_sources, velocity_at, |
| 8 | + velocity_at_with_sources, WakePanels, |
| 9 | +}; |
7 | 10 | use rustfoil_core::Point; |
8 | 11 |
|
9 | 12 | /// A particle in the smoke system. |
@@ -151,6 +154,77 @@ impl SmokeSystem { |
151 | 154 | self.particles.retain(|p| p.age < self.max_age); |
152 | 155 | } |
153 | 156 |
|
| 157 | + /// Update particles using a source-inclusive viscous field. |
| 158 | + pub fn update_with_sources( |
| 159 | + &mut self, |
| 160 | + nodes: &[Point], |
| 161 | + gamma: &[f64], |
| 162 | + sigma: &[f64], |
| 163 | + alpha: f64, |
| 164 | + v_inf: f64, |
| 165 | + wake_panels: Option<&WakePanels>, |
| 166 | + effective_body: Option<&[Point]>, |
| 167 | + dt: f64, |
| 168 | + ) { |
| 169 | + self.time_since_spawn += dt; |
| 170 | + |
| 171 | + if self.time_since_spawn >= self.spawn_interval { |
| 172 | + self.spawn_blobs(); |
| 173 | + self.time_since_spawn = 0.0; |
| 174 | + } |
| 175 | + |
| 176 | + for particle in &mut self.particles { |
| 177 | + particle.age += dt; |
| 178 | + if particle.age >= self.max_age { |
| 179 | + continue; |
| 180 | + } |
| 181 | + |
| 182 | + if is_inside_airfoil(particle.x, particle.y, nodes) |
| 183 | + || effective_body.is_some_and(|poly| is_inside_polygon(particle.x, particle.y, poly)) |
| 184 | + { |
| 185 | + particle.age = self.max_age; |
| 186 | + continue; |
| 187 | + } |
| 188 | + |
| 189 | + let (u1, v1) = velocity_at_with_sources( |
| 190 | + particle.x, |
| 191 | + particle.y, |
| 192 | + nodes, |
| 193 | + gamma, |
| 194 | + sigma, |
| 195 | + alpha, |
| 196 | + v_inf, |
| 197 | + wake_panels, |
| 198 | + ); |
| 199 | + |
| 200 | + let mid_x = particle.x + 0.5 * dt * u1; |
| 201 | + let mid_y = particle.y + 0.5 * dt * v1; |
| 202 | + |
| 203 | + let (u2, v2) = velocity_at_with_sources( |
| 204 | + mid_x, |
| 205 | + mid_y, |
| 206 | + nodes, |
| 207 | + gamma, |
| 208 | + sigma, |
| 209 | + alpha, |
| 210 | + v_inf, |
| 211 | + wake_panels, |
| 212 | + ); |
| 213 | + |
| 214 | + let new_x = particle.x + dt * u2; |
| 215 | + let new_y = particle.y + dt * v2; |
| 216 | + if effective_body.is_some_and(|poly| is_inside_polygon(new_x, new_y, poly)) { |
| 217 | + particle.age = self.max_age; |
| 218 | + continue; |
| 219 | + } |
| 220 | + |
| 221 | + particle.x = new_x; |
| 222 | + particle.y = new_y; |
| 223 | + } |
| 224 | + |
| 225 | + self.particles.retain(|p| p.age < self.max_age); |
| 226 | + } |
| 227 | + |
154 | 228 | /// Get all particle positions as a flat array [x0, y0, x1, y1, ...]. |
155 | 229 | pub fn get_positions(&self) -> Vec<f64> { |
156 | 230 | let mut positions = Vec::with_capacity(self.particles.len() * 2); |
@@ -196,6 +270,24 @@ impl SmokeSystem { |
196 | 270 | .collect() |
197 | 271 | } |
198 | 272 |
|
| 273 | + /// Get stream function values using a source-inclusive viscous field. |
| 274 | + pub fn get_psi_values_with_sources( |
| 275 | + &self, |
| 276 | + nodes: &[Point], |
| 277 | + gamma: &[f64], |
| 278 | + sigma: &[f64], |
| 279 | + alpha: f64, |
| 280 | + v_inf: f64, |
| 281 | + wake_panels: Option<&WakePanels>, |
| 282 | + ) -> Vec<f64> { |
| 283 | + self.particles |
| 284 | + .iter() |
| 285 | + .map(|p| { |
| 286 | + psi_at_with_sources(p.x, p.y, nodes, gamma, sigma, alpha, v_inf, wake_panels) |
| 287 | + }) |
| 288 | + .collect() |
| 289 | + } |
| 290 | + |
199 | 291 | /// Get the number of active particles. |
200 | 292 | pub fn particle_count(&self) -> usize { |
201 | 293 | self.particles.len() |
|
0 commit comments