1use glam::Vec3;
8use crate::math::ForceField;
9use crate::scene::FieldId;
10
11pub struct ManagedField {
15 pub id: FieldId,
16 pub field: ForceField,
17 pub ttl: Option<f32>,
19 pub age: f32,
21 pub tags: Vec<String>,
23 pub strength_scale: f32,
25 pub fade_in: f32,
27 pub fade_out: f32,
29}
30
31impl ManagedField {
32 pub fn is_expired(&self) -> bool {
34 self.ttl.map(|ttl| self.age >= ttl).unwrap_or(false)
35 }
36
37 pub fn effective_scale(&self) -> f32 {
39 let base = self.strength_scale;
40 let fade_in_factor = if self.fade_in > 0.0 {
42 (self.age / self.fade_in).min(1.0)
43 } else {
44 1.0
45 };
46 let fade_out_factor = if let Some(ttl) = self.ttl {
48 if self.fade_out > 0.0 {
49 let remaining = ttl - self.age;
50 (remaining / self.fade_out).clamp(0.0, 1.0)
51 } else {
52 1.0
53 }
54 } else {
55 1.0
56 };
57 base * fade_in_factor * fade_out_factor
58 }
59
60 pub fn has_tag(&self, tag: &str) -> bool {
62 self.tags.iter().any(|t| t == tag)
63 }
64}
65
66#[derive(Debug, Clone)]
70pub struct FieldSample {
71 pub force: Vec3,
73 pub temperature: f32,
75 pub entropy: f32,
77 pub field_count: usize,
79}
80
81impl Default for FieldSample {
82 fn default() -> Self {
83 Self { force: Vec3::ZERO, temperature: 0.0, entropy: 0.0, field_count: 0 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq)]
91pub enum FieldBlend {
92 Additive,
94 Average,
96 Dominant,
98 ComponentMax,
100}
101
102const CELL_SIZE: f32 = 10.0;
105
106fn world_to_cell(pos: Vec3) -> (i32, i32, i32) {
107 (
108 (pos.x / CELL_SIZE).floor() as i32,
109 (pos.y / CELL_SIZE).floor() as i32,
110 (pos.z / CELL_SIZE).floor() as i32,
111 )
112}
113
114pub struct FieldManager {
118 fields: Vec<ManagedField>,
119 next_id: u32,
120 pub blend_mode: FieldBlend,
121 pub global_scale: f32,
123}
124
125impl FieldManager {
126 pub fn new() -> Self {
127 Self {
128 fields: Vec::new(),
129 next_id: 1,
130 blend_mode: FieldBlend::Additive,
131 global_scale: 1.0,
132 }
133 }
134
135 pub fn add(&mut self, field: ForceField) -> FieldId {
139 self.add_full(field, None, vec![], 1.0, 0.0, 0.0)
140 }
141
142 pub fn add_timed(&mut self, field: ForceField, ttl: f32) -> FieldId {
144 self.add_full(field, Some(ttl), vec![], 1.0, 0.0, 0.0)
145 }
146
147 pub fn add_faded(&mut self, field: ForceField, ttl: f32, fade_in: f32, fade_out: f32) -> FieldId {
149 self.add_full(field, Some(ttl), vec![], 1.0, fade_in, fade_out)
150 }
151
152 pub fn add_tagged(&mut self, field: ForceField, ttl: Option<f32>, tags: Vec<String>) -> FieldId {
154 self.add_full(field, ttl, tags, 1.0, 0.0, 0.0)
155 }
156
157 fn add_full(
158 &mut self,
159 field: ForceField,
160 ttl: Option<f32>,
161 tags: Vec<String>,
162 strength_scale: f32,
163 fade_in: f32,
164 fade_out: f32,
165 ) -> FieldId {
166 let id = FieldId(self.next_id);
167 self.next_id += 1;
168 self.fields.push(ManagedField {
169 id,
170 field,
171 ttl,
172 age: 0.0,
173 tags,
174 strength_scale,
175 fade_in,
176 fade_out,
177 });
178 id
179 }
180
181 pub fn remove(&mut self, id: FieldId) -> bool {
183 let before = self.fields.len();
184 self.fields.retain(|f| f.id != id);
185 self.fields.len() < before
186 }
187
188 pub fn remove_tagged(&mut self, tag: &str) {
190 self.fields.retain(|f| !f.has_tag(tag));
191 }
192
193 pub fn clear(&mut self) {
195 self.fields.clear();
196 }
197
198 pub fn get(&self, id: FieldId) -> Option<&ManagedField> {
200 self.fields.iter().find(|f| f.id == id)
201 }
202
203 pub fn get_mut(&mut self, id: FieldId) -> Option<&mut ManagedField> {
205 self.fields.iter_mut().find(|f| f.id == id)
206 }
207
208 pub fn tick(&mut self, dt: f32) {
212 for f in &mut self.fields {
213 f.age += dt;
214 }
215 self.fields.retain(|f| !f.is_expired());
216 }
217
218 pub fn sample(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> FieldSample {
222 let mut sample = FieldSample::default();
223 let mut forces: Vec<Vec3> = Vec::new();
224
225 for mf in &self.fields {
226 let scale = mf.effective_scale() * self.global_scale;
227 if scale == 0.0 { continue; }
228
229 let force = mf.field.force_at(pos, mass, charge, t) * scale;
230 let temp = mf.field.temperature_at(pos) * scale;
231 let entr = mf.field.entropy_at(pos) * scale;
232
233 sample.temperature += temp;
234 sample.entropy += entr;
235 sample.field_count += 1;
236 forces.push(force);
237 }
238
239 sample.force = combine_forces(&forces, self.blend_mode);
240 sample
241 }
242
243 pub fn sample_tagged(&self, pos: Vec3, tag: &str, mass: f32, charge: f32, t: f32) -> FieldSample {
245 let mut sample = FieldSample::default();
246 let mut forces: Vec<Vec3> = Vec::new();
247
248 for mf in self.fields.iter().filter(|f| f.has_tag(tag)) {
249 let scale = mf.effective_scale() * self.global_scale;
250 if scale == 0.0 { continue; }
251
252 let force = mf.field.force_at(pos, mass, charge, t) * scale;
253 sample.temperature += mf.field.temperature_at(pos) * scale;
254 sample.entropy += mf.field.entropy_at(pos) * scale;
255 sample.field_count += 1;
256 forces.push(force);
257 }
258
259 sample.force = combine_forces(&forces, self.blend_mode);
260 sample
261 }
262
263 pub fn force_at(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> Vec3 {
265 self.sample(pos, mass, charge, t).force
266 }
267
268 pub fn fields_near(&self, pos: Vec3, radius: f32) -> Vec<FieldId> {
270 self.fields.iter()
271 .filter(|mf| field_center(&mf.field)
272 .map(|c| (c - pos).length() < radius)
273 .unwrap_or(true))
274 .map(|mf| mf.id)
275 .collect()
276 }
277
278 pub fn len(&self) -> usize { self.fields.len() }
282
283 pub fn is_empty(&self) -> bool { self.fields.is_empty() }
285
286 pub fn iter(&self) -> impl Iterator<Item = &ManagedField> {
288 self.fields.iter()
289 }
290
291 pub fn interference_at(&self, pos: Vec3, t: f32) -> f32 {
296 if self.fields.len() < 2 { return 0.0; }
297 let forces: Vec<Vec3> = self.fields.iter()
298 .map(|mf| mf.field.force_at(pos, 1.0, 0.0, t) * mf.effective_scale())
299 .collect();
300 if forces.is_empty() { return 0.0; }
301 let sum: Vec3 = forces.iter().copied().sum();
302 let mag_sum: f32 = forces.iter().map(|f| f.length()).sum();
303 if mag_sum < 0.001 { return 0.0; }
304 (sum.length() / mag_sum) * 2.0 - 1.0
307 }
308
309 pub fn resonance_at(&self, pos: Vec3, t: f32) -> f32 {
312 let mags: Vec<f32> = self.fields.iter()
313 .map(|mf| mf.field.force_at(pos, 1.0, 0.0, t).length() * mf.effective_scale())
314 .collect();
315 if mags.is_empty() { return 0.0; }
316 let mean = mags.iter().sum::<f32>() / mags.len() as f32;
317 let var = mags.iter().map(|m| (m - mean).powi(2)).sum::<f32>() / mags.len() as f32;
318 1.0 / (1.0 + var) }
320}
321
322impl Default for FieldManager {
323 fn default() -> Self { Self::new() }
324}
325
326fn combine_forces(forces: &[Vec3], blend: FieldBlend) -> Vec3 {
329 if forces.is_empty() { return Vec3::ZERO; }
330 match blend {
331 FieldBlend::Additive => forces.iter().copied().sum(),
332 FieldBlend::Average => forces.iter().copied().sum::<Vec3>() / forces.len() as f32,
333 FieldBlend::Dominant => forces.iter().copied()
334 .max_by(|a, b| a.length_squared().partial_cmp(&b.length_squared()).unwrap())
335 .unwrap_or(Vec3::ZERO),
336 FieldBlend::ComponentMax => forces.iter().copied().fold(Vec3::ZERO, |acc, f| {
337 Vec3::new(
338 if f.x.abs() > acc.x.abs() { f.x } else { acc.x },
339 if f.y.abs() > acc.y.abs() { f.y } else { acc.y },
340 if f.z.abs() > acc.z.abs() { f.z } else { acc.z },
341 )
342 }),
343 }
344}
345
346fn field_center(field: &ForceField) -> Option<Vec3> {
348 use crate::math::ForceField as FF;
349 match field {
350 FF::Gravity { center, .. } => Some(*center),
351 FF::Vortex { center, .. } => Some(*center),
352 FF::Repulsion { center, .. } => Some(*center),
353 FF::Electromagnetic { center, .. }=> Some(*center),
354 FF::HeatSource { center, .. } => Some(*center),
355 FF::MathField { center, .. } => Some(*center),
356 FF::StrangeAttractor { center, .. }=> Some(*center),
357 FF::EntropyField { center, .. } => Some(*center),
358 FF::Damping { center, .. } => Some(*center),
359 FF::Flow { .. } => None,
360 FF::Pulsing { center, .. } => Some(*center),
361 FF::Shockwave { center, .. } => Some(*center),
362 FF::Warp { center, .. } => Some(*center),
363 FF::Tidal { center, .. } => Some(*center),
364 FF::MagneticDipole { center, .. }=> Some(*center),
365 FF::Saddle { center, .. } => Some(*center),
366 FF::Wind { .. } => None,
367 }
368}
369
370#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::math::fields::Falloff;
376
377 #[test]
378 fn add_and_remove() {
379 let mut mgr = FieldManager::new();
380 let id = mgr.add(ForceField::Gravity {
381 center: Vec3::ZERO, strength: 1.0, falloff: Falloff::InverseSquare,
382 });
383 assert_eq!(mgr.len(), 1);
384 assert!(mgr.remove(id));
385 assert_eq!(mgr.len(), 0);
386 }
387
388 #[test]
389 fn timed_field_expires() {
390 let mut mgr = FieldManager::new();
391 mgr.add_timed(ForceField::Flow {
392 direction: Vec3::X, strength: 1.0, turbulence: 0.0,
393 }, 0.5);
394 mgr.tick(0.3);
395 assert_eq!(mgr.len(), 1);
396 mgr.tick(0.3);
397 assert_eq!(mgr.len(), 0);
398 }
399
400 #[test]
401 fn sample_returns_force() {
402 let mut mgr = FieldManager::new();
403 mgr.add(ForceField::Flow { direction: Vec3::X, strength: 2.0, turbulence: 0.0 });
404 let sample = mgr.sample(Vec3::ZERO, 1.0, 0.0, 0.0);
405 assert!(sample.force.x > 0.0);
406 }
407
408 #[test]
409 fn fade_in_scales_force() {
410 let mut mgr = FieldManager::new();
411 mgr.add_faded(
412 ForceField::Flow { direction: Vec3::X, strength: 2.0, turbulence: 0.0 },
413 2.0, 1.0, 0.0,
414 );
415 let sample_early = mgr.sample(Vec3::ZERO, 1.0, 0.0, 0.0);
417 mgr.tick(1.0);
418 let sample_late = mgr.sample(Vec3::ZERO, 1.0, 0.0, 1.0);
419 assert!(sample_late.force.x > sample_early.force.x);
420 }
421}