1use glam::{Vec2, Vec3};
14use super::attractors::AttractorType;
15use super::MathFunction;
16
17#[derive(Clone, Debug)]
21pub enum ForceField {
22 Gravity { center: Vec3, strength: f32, falloff: Falloff },
24 Flow { direction: Vec3, strength: f32, turbulence: f32 },
26 Vortex { center: Vec3, axis: Vec3, strength: f32, radius: f32 },
28 Repulsion { center: Vec3, strength: f32, radius: f32 },
30 Electromagnetic { center: Vec3, charge: f32, strength: f32 },
32 HeatSource { center: Vec3, temperature: f32, radius: f32 },
34 MathField { center: Vec3, radius: f32, function: MathFunction, target: FieldTarget },
36 StrangeAttractor { attractor_type: AttractorType, scale: f32, strength: f32, center: Vec3 },
38 EntropyField { center: Vec3, radius: f32, strength: f32 },
40 Damping { center: Vec3, radius: f32, strength: f32 },
42 Pulsing { center: Vec3, frequency: f32, amplitude: f32, radius: f32 },
44 Shockwave { center: Vec3, speed: f32, thickness: f32, strength: f32, born_at: f32 },
46 Wind { direction: Vec3, base_strength: f32, gust_frequency: f32, gust_amplitude: f32 },
48 Warp { center: Vec3, exit: Vec3, radius: f32, strength: f32 },
50 Tidal { center: Vec3, axis: Vec3, strength: f32, radius: f32 },
52 MagneticDipole { center: Vec3, axis: Vec3, moment: f32 },
54 Saddle { center: Vec3, strength_x: f32, strength_y: f32 },
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
60pub enum Falloff {
61 None,
62 Linear,
63 InverseSquare,
64 Exponential(f32),
65 Gaussian(f32),
66 SmoothStep(f32),
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
72pub enum FieldTarget {
73 PositionX, PositionY, PositionZ,
74 ColorR, ColorG, ColorB, ColorA,
75 Scale, Rotation, Emission, Temperature, Entropy,
76}
77
78impl ForceField {
79 pub fn force_at(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> Vec3 {
82 match self {
83 ForceField::Gravity { center, strength, falloff } => {
84 let delta = *center - pos;
85 let dist = delta.length().max(0.01);
86 let dir = delta / dist;
87 let s = falloff_factor(*falloff, dist, 1.0) * strength * mass;
88 dir * s
89 }
90
91 ForceField::Flow { direction, strength, turbulence: _ } => {
92 direction.normalize_or_zero() * *strength
93 }
94
95 ForceField::Vortex { center, axis, strength, radius } => {
96 let delta = pos - *center;
97 let dist = delta.length();
98 if dist > *radius || dist < 0.001 { return Vec3::ZERO; }
99 let tangent = axis.normalize().cross(delta).normalize_or_zero();
100 tangent * *strength * (1.0 - dist / radius)
101 }
102
103 ForceField::Repulsion { center, strength, radius } => {
104 let delta = pos - *center;
105 let dist = delta.length();
106 if dist > *radius || dist < 0.001 { return Vec3::ZERO; }
107 let dir = delta / dist;
108 dir * *strength * (1.0 - dist / radius)
109 }
110
111 ForceField::Electromagnetic { center, charge: field_charge, strength } => {
112 let delta = pos - *center;
113 let dist = delta.length().max(0.01);
114 let dir = delta / dist;
115 let sign = if charge * field_charge > 0.0 { 1.0 } else { -1.0 };
116 dir * sign * *strength / (dist * dist)
117 }
118
119 ForceField::HeatSource { .. } | ForceField::EntropyField { .. } => Vec3::ZERO,
120
121 ForceField::Damping { center, radius, strength: _ } => {
122 let dist = (pos - *center).length();
123 if dist > *radius { return Vec3::ZERO; }
124 Vec3::ZERO }
126
127 ForceField::MathField { .. } => Vec3::ZERO,
128
129 ForceField::StrangeAttractor { attractor_type, scale, strength, center } => {
130 let local = (pos - *center) / scale.max(0.001);
131 let (_next, delta) = super::attractors::step(*attractor_type, local, 0.016);
132 delta * *strength
133 }
134
135 ForceField::Pulsing { center, frequency, amplitude, radius } => {
136 let dist = (pos - *center).length();
137 if dist > *radius || dist < 0.001 { return Vec3::ZERO; }
138 let dir = (pos - *center).normalize_or_zero();
139 let wave = (t * frequency * std::f32::consts::TAU).sin();
140 dir * *amplitude * wave * (1.0 - dist / radius)
141 }
142
143 ForceField::Shockwave { center, speed, thickness, strength, born_at } => {
144 let dist = (pos - *center).length();
145 let wave_r = (t - born_at) * speed;
146 let diff = (dist - wave_r).abs();
147 if diff > *thickness { return Vec3::ZERO; }
148 let dir = (pos - *center).normalize_or_zero();
149 let falloff = 1.0 - diff / thickness;
150 dir * *strength * falloff / (wave_r + 1.0)
151 }
152
153 ForceField::Wind { direction, base_strength, gust_frequency, gust_amplitude } => {
154 let gust = (t * gust_frequency * std::f32::consts::TAU
155 + pos.x * 0.3 + pos.z * 0.2).sin() * gust_amplitude;
156 direction.normalize_or_zero() * (base_strength + gust)
157 }
158
159 ForceField::Warp { center, exit: _, radius, strength } => {
160 let delta = pos - *center;
161 let dist = delta.length();
162 if dist > *radius || dist < 0.001 { return Vec3::ZERO; }
163 let dir = -delta.normalize_or_zero(); dir * *strength * (1.0 - dist / radius).powi(2)
165 }
166
167 ForceField::Tidal { center, axis, strength, radius } => {
168 let delta = pos - *center;
169 let dist = delta.length();
170 if dist > *radius { return Vec3::ZERO; }
171 let ax = axis.normalize();
172 let along = ax * ax.dot(delta);
173 let perp = delta - along;
174 (along * 2.0 - perp) * *strength * (1.0 - dist / radius)
176 }
177
178 ForceField::MagneticDipole { center, axis, moment } => {
179 let r = pos - *center;
180 let dist = r.length().max(0.01);
181 let r_hat = r / dist;
182 let m = axis.normalize() * *moment;
183 let factor = 1.0 / (dist * dist * dist);
184 (3.0 * r_hat * r_hat.dot(m) - m) * factor
185 }
186
187 ForceField::Saddle { center, strength_x, strength_y } => {
188 let d = pos - *center;
189 Vec3::new(d.x * strength_x, -d.y * strength_y, 0.0)
190 }
191 }
192 }
193
194 pub fn temperature_at(&self, pos: Vec3) -> f32 {
196 if let ForceField::HeatSource { center, temperature, radius } = self {
197 let dist = (pos - *center).length();
198 if dist < *radius {
199 return temperature * (1.0 - dist / radius);
200 }
201 }
202 0.0
203 }
204
205 pub fn entropy_at(&self, pos: Vec3) -> f32 {
207 if let ForceField::EntropyField { center, radius, strength } = self {
208 let dist = (pos - *center).length();
209 if dist < *radius {
210 return strength * (1.0 - dist / radius);
211 }
212 }
213 0.0
214 }
215
216 pub fn damping_at(&self, pos: Vec3) -> f32 {
218 if let ForceField::Damping { center, radius, strength } = self {
219 let dist = (pos - *center).length();
220 if dist < *radius {
221 return 1.0 - strength * (1.0 - dist / radius);
222 }
223 }
224 1.0
225 }
226
227 pub fn is_non_positional(&self) -> bool {
229 matches!(self,
230 ForceField::HeatSource { .. }
231 | ForceField::EntropyField { .. }
232 | ForceField::MathField { .. }
233 | ForceField::Damping { .. }
234 )
235 }
236
237 pub fn label(&self) -> &'static str {
239 match self {
240 ForceField::Gravity { .. } => "Gravity",
241 ForceField::Flow { .. } => "Flow",
242 ForceField::Vortex { .. } => "Vortex",
243 ForceField::Repulsion { .. } => "Repulsion",
244 ForceField::Electromagnetic { .. } => "EM",
245 ForceField::HeatSource { .. } => "Heat",
246 ForceField::MathField { .. } => "Math",
247 ForceField::StrangeAttractor { .. } => "Attractor",
248 ForceField::EntropyField { .. } => "Entropy",
249 ForceField::Damping { .. } => "Damping",
250 ForceField::Pulsing { .. } => "Pulsing",
251 ForceField::Shockwave { .. } => "Shockwave",
252 ForceField::Wind { .. } => "Wind",
253 ForceField::Warp { .. } => "Warp",
254 ForceField::Tidal { .. } => "Tidal",
255 ForceField::MagneticDipole { .. } => "Dipole",
256 ForceField::Saddle { .. } => "Saddle",
257 }
258 }
259
260 pub fn center(&self) -> Option<Vec3> {
262 match self {
263 ForceField::Gravity { center, .. } => Some(*center),
264 ForceField::Vortex { center, .. } => Some(*center),
265 ForceField::Repulsion { center, .. } => Some(*center),
266 ForceField::Electromagnetic { center, .. } => Some(*center),
267 ForceField::HeatSource { center, .. } => Some(*center),
268 ForceField::MathField { center, .. } => Some(*center),
269 ForceField::StrangeAttractor { center, .. } => Some(*center),
270 ForceField::EntropyField { center, .. } => Some(*center),
271 ForceField::Damping { center, .. } => Some(*center),
272 ForceField::Pulsing { center, .. } => Some(*center),
273 ForceField::Shockwave { center, .. } => Some(*center),
274 ForceField::Warp { center, .. } => Some(*center),
275 ForceField::Tidal { center, .. } => Some(*center),
276 ForceField::MagneticDipole { center, .. } => Some(*center),
277 ForceField::Saddle { center, .. } => Some(*center),
278 _ => None,
279 }
280 }
281}
282
283pub fn falloff_factor(falloff: Falloff, distance: f32, max_distance: f32) -> f32 {
286 match falloff {
287 Falloff::None => 1.0,
288 Falloff::Linear => (1.0 - distance / max_distance).max(0.0),
289 Falloff::InverseSquare => 1.0 / (distance * distance).max(0.0001),
290 Falloff::Exponential(r) => (-distance * r).exp(),
291 Falloff::Gaussian(sig) => {
292 let x = distance / sig;
293 (-0.5 * x * x).exp()
294 }
295 Falloff::SmoothStep(r) => {
296 let t = (1.0 - distance / r).clamp(0.0, 1.0);
297 t * t * (3.0 - 2.0 * t)
298 }
299 }
300}
301
302#[derive(Clone, Debug)]
306pub struct FieldComposer {
307 pub layers: Vec<FieldLayer>,
308}
309
310#[derive(Clone, Debug)]
311pub struct FieldLayer {
312 pub field: ForceField,
313 pub blend: FieldBlend,
314 pub weight: f32,
315 pub enabled: bool,
316}
317
318#[derive(Clone, Copy, Debug, PartialEq)]
319pub enum FieldBlend {
320 Add,
322 Multiply,
324 Max,
326 Override,
328 Subtract,
330}
331
332impl FieldComposer {
333 pub fn new() -> Self { Self { layers: Vec::new() } }
334
335 pub fn add(mut self, field: ForceField) -> Self {
336 self.layers.push(FieldLayer { field, blend: FieldBlend::Add, weight: 1.0, enabled: true });
337 self
338 }
339
340 pub fn add_weighted(mut self, field: ForceField, weight: f32) -> Self {
341 self.layers.push(FieldLayer { field, blend: FieldBlend::Add, weight, enabled: true });
342 self
343 }
344
345 pub fn add_blended(mut self, field: ForceField, blend: FieldBlend, weight: f32) -> Self {
346 self.layers.push(FieldLayer { field, blend, weight, enabled: true });
347 self
348 }
349
350 pub fn force_at(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> Vec3 {
352 let mut acc = Vec3::ZERO;
353 for layer in &self.layers {
354 if !layer.enabled { continue; }
355 let f = layer.field.force_at(pos, mass, charge, t) * layer.weight;
356 acc = match layer.blend {
357 FieldBlend::Add => acc + f,
358 FieldBlend::Subtract => acc - f,
359 FieldBlend::Multiply => acc * f,
360 FieldBlend::Max => acc.max(f),
361 FieldBlend::Override => f,
362 };
363 }
364 acc
365 }
366
367 pub fn enable_layer(&mut self, idx: usize, enabled: bool) {
368 if let Some(l) = self.layers.get_mut(idx) { l.enabled = enabled; }
369 }
370
371 pub fn set_weight(&mut self, idx: usize, weight: f32) {
372 if let Some(l) = self.layers.get_mut(idx) { l.weight = weight; }
373 }
374}
375
376pub struct FieldSampler {
380 pub width: usize,
381 pub height: usize,
382 pub x_min: f32,
383 pub x_max: f32,
384 pub y_min: f32,
385 pub y_max: f32,
386 pub z: f32,
387 pub forces: Vec<Vec3>,
389 pub magnitudes: Vec<f32>,
390}
391
392impl FieldSampler {
393 pub fn new(width: usize, height: usize, bounds: (f32, f32, f32, f32)) -> Self {
394 let n = width * height;
395 Self {
396 width, height,
397 x_min: bounds.0, x_max: bounds.2,
398 y_min: bounds.1, y_max: bounds.3,
399 z: 0.0,
400 forces: vec![Vec3::ZERO; n],
401 magnitudes: vec![0.0; n],
402 }
403 }
404
405 pub fn sample(&mut self, field: &ForceField) {
407 let dx = (self.x_max - self.x_min) / self.width as f32;
408 let dy = (self.y_max - self.y_min) / self.height as f32;
409 for y in 0..self.height {
410 for x in 0..self.width {
411 let wx = self.x_min + (x as f32 + 0.5) * dx;
412 let wy = self.y_min + (y as f32 + 0.5) * dy;
413 let f = field.force_at(Vec3::new(wx, wy, self.z), 1.0, 0.0, 0.0);
414 let i = y * self.width + x;
415 self.forces[i] = f;
416 self.magnitudes[i] = f.length();
417 }
418 }
419 }
420
421 pub fn sample_composer(&mut self, composer: &FieldComposer) {
423 let dx = (self.x_max - self.x_min) / self.width as f32;
424 let dy = (self.y_max - self.y_min) / self.height as f32;
425 for y in 0..self.height {
426 for x in 0..self.width {
427 let wx = self.x_min + (x as f32 + 0.5) * dx;
428 let wy = self.y_min + (y as f32 + 0.5) * dy;
429 let f = composer.force_at(Vec3::new(wx, wy, self.z), 1.0, 0.0, 0.0);
430 let i = y * self.width + x;
431 self.forces[i] = f;
432 self.magnitudes[i] = f.length();
433 }
434 }
435 }
436
437 pub fn max_magnitude(&self) -> f32 {
439 self.magnitudes.iter().cloned().fold(0.0_f32, f32::max)
440 }
441
442 pub fn force_at_cell(&self, x: usize, y: usize) -> Vec3 {
444 self.forces.get(y * self.width + x).copied().unwrap_or(Vec3::ZERO)
445 }
446
447 pub fn streamline(&self, start: Vec2, steps: usize, dt: f32) -> Vec<Vec2> {
449 let mut pts = Vec::with_capacity(steps);
450 let mut pos = start;
451 for _ in 0..steps {
452 pts.push(pos);
453 let f = self.sample_at_world(pos);
454 if f.length_squared() < 1e-6 { break; }
455 pos += f * dt;
456 }
457 pts
458 }
459
460 fn sample_at_world(&self, pos: Vec2) -> Vec2 {
461 let tx = (pos.x - self.x_min) / (self.x_max - self.x_min);
462 let ty = (pos.y - self.y_min) / (self.y_max - self.y_min);
463 let cx = (tx * self.width as f32).clamp(0.0, self.width as f32 - 1.001);
464 let cy = (ty * self.height as f32).clamp(0.0, self.height as f32 - 1.001);
465 let x0 = cx.floor() as usize;
466 let y0 = cy.floor() as usize;
467 let x1 = (x0 + 1).min(self.width - 1);
468 let y1 = (y0 + 1).min(self.height - 1);
469 let fx = cx.fract();
470 let fy = cy.fract();
471 let f00 = self.forces[y0 * self.width + x0].truncate();
472 let f10 = self.forces[y0 * self.width + x1].truncate();
473 let f01 = self.forces[y1 * self.width + x0].truncate();
474 let f11 = self.forces[y1 * self.width + x1].truncate();
475 let f0 = Vec2::lerp(f00, f10, fx);
476 let f1 = Vec2::lerp(f01, f11, fx);
477 Vec2::lerp(f0, f1, fy)
478 }
479
480 pub fn to_rgba(&self) -> Vec<u8> {
482 let n = self.width * self.height;
483 let max_mag = self.max_magnitude().max(0.001);
484 let mut out = vec![0u8; n * 4];
485 for i in 0..n {
486 let f = self.forces[i];
487 let r = (f.x / max_mag * 0.5 + 0.5).clamp(0.0, 1.0);
488 let g = (f.y / max_mag * 0.5 + 0.5).clamp(0.0, 1.0);
489 let b = (self.magnitudes[i] / max_mag).clamp(0.0, 1.0);
490 out[i * 4 ] = (r * 255.0) as u8;
491 out[i * 4 + 1] = (g * 255.0) as u8;
492 out[i * 4 + 2] = (b * 255.0) as u8;
493 out[i * 4 + 3] = 255;
494 }
495 out
496 }
497}
498
499#[derive(Clone, Debug)]
503pub struct AnimatedField {
504 pub field: ForceField,
505 pub timeline: Vec<AnimationKey>,
506}
507
508#[derive(Clone, Debug)]
509pub struct AnimationKey {
510 pub time: f32,
511 pub strength: f32,
512 pub offset: Vec3,
513}
514
515impl AnimatedField {
516 pub fn new(field: ForceField) -> Self {
517 Self { field, timeline: Vec::new() }
518 }
519
520 pub fn key(mut self, time: f32, strength: f32, offset: Vec3) -> Self {
521 self.timeline.push(AnimationKey { time, strength, offset });
522 self.timeline.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
523 self
524 }
525
526 pub fn force_at(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> Vec3 {
527 let (strength, offset) = self.eval_at(t);
528 let shifted_pos = pos - offset;
529 self.field.force_at(shifted_pos, mass, charge, t) * strength
530 }
531
532 fn eval_at(&self, t: f32) -> (f32, Vec3) {
533 if self.timeline.is_empty() { return (1.0, Vec3::ZERO); }
534 if self.timeline.len() == 1 {
535 let k = &self.timeline[0];
536 return (k.strength, k.offset);
537 }
538 if t <= self.timeline[0].time {
539 let k = &self.timeline[0];
540 return (k.strength, k.offset);
541 }
542 let last = self.timeline.last().unwrap();
543 if t >= last.time { return (last.strength, last.offset); }
544
545 let i = self.timeline.partition_point(|k| k.time <= t) - 1;
546 let k0 = &self.timeline[i];
547 let k1 = &self.timeline[i + 1];
548 let span = k1.time - k0.time;
549 let ft = if span < 1e-6 { 0.0 } else { (t - k0.time) / span };
550 let strength = k0.strength + (k1.strength - k0.strength) * ft;
551 let offset = Vec3::lerp(k0.offset, k1.offset, ft);
552 (strength, offset)
553 }
554}
555
556pub struct FieldPresets;
560
561impl FieldPresets {
562 pub fn planet(center: Vec3, mass: f32) -> ForceField {
564 ForceField::Gravity {
565 center,
566 strength: mass * 6.674e-3,
567 falloff: Falloff::InverseSquare,
568 }
569 }
570
571 pub fn tornado(center: Vec3, strength: f32, radius: f32) -> ForceField {
573 ForceField::Vortex {
574 center,
575 axis: Vec3::Y,
576 strength,
577 radius,
578 }
579 }
580
581 pub fn explosion(center: Vec3, strength: f32, born_at: f32) -> ForceField {
583 ForceField::Shockwave {
584 center,
585 speed: 15.0,
586 thickness: 3.0,
587 strength,
588 born_at,
589 }
590 }
591
592 pub fn river(direction: Vec3, speed: f32) -> ForceField {
594 ForceField::Flow { direction, strength: speed, turbulence: 0.1 }
595 }
596
597 pub fn bonfire(center: Vec3, heat: f32) -> ForceField {
599 ForceField::HeatSource { center, temperature: heat, radius: 3.0 }
600 }
601
602 pub fn galaxy_arm(center: Vec3, scale: f32) -> ForceField {
604 ForceField::StrangeAttractor {
605 attractor_type: AttractorType::Lorenz,
606 scale,
607 strength: 0.5,
608 center,
609 }
610 }
611
612 pub fn frost_aura(center: Vec3, radius: f32) -> ForceField {
614 ForceField::Damping { center, radius, strength: 0.7 }
615 }
616
617 pub fn chaos_zone(center: Vec3, radius: f32) -> ForceField {
619 ForceField::EntropyField { center, radius, strength: 2.0 }
620 }
621
622 pub fn pendulum(center: Vec3, amplitude: f32, frequency: f32, radius: f32) -> ForceField {
624 ForceField::Pulsing { center, frequency, amplitude, radius }
625 }
626}
627
628#[cfg(test)]
631mod tests {
632 use super::*;
633
634 fn origin() -> Vec3 { Vec3::ZERO }
635
636 #[test]
637 fn test_gravity_pulls_toward_center() {
638 let field = ForceField::Gravity {
639 center: Vec3::new(5.0, 0.0, 0.0),
640 strength: 1.0,
641 falloff: Falloff::None,
642 };
643 let f = field.force_at(Vec3::ZERO, 1.0, 0.0, 0.0);
644 assert!(f.x > 0.0); }
646
647 #[test]
648 fn test_repulsion_pushes_away() {
649 let field = ForceField::Repulsion { center: origin(), strength: 1.0, radius: 10.0 };
650 let f = field.force_at(Vec3::new(1.0, 0.0, 0.0), 1.0, 0.0, 0.0);
651 assert!(f.x > 0.0); }
653
654 #[test]
655 fn test_repulsion_zero_outside_radius() {
656 let field = ForceField::Repulsion { center: origin(), strength: 1.0, radius: 1.0 };
657 let f = field.force_at(Vec3::new(5.0, 0.0, 0.0), 1.0, 0.0, 0.0);
658 assert_eq!(f, Vec3::ZERO);
659 }
660
661 #[test]
662 fn test_flow_direction() {
663 let field = ForceField::Flow {
664 direction: Vec3::X,
665 strength: 2.0,
666 turbulence: 0.0,
667 };
668 let f = field.force_at(origin(), 1.0, 0.0, 0.0);
669 assert!((f.x - 2.0).abs() < 0.01);
670 assert!(f.y.abs() < 0.01);
671 }
672
673 #[test]
674 fn test_vortex_tangential() {
675 let field = ForceField::Vortex {
676 center: Vec3::ZERO,
677 axis: Vec3::Z,
678 strength: 1.0,
679 radius: 10.0,
680 };
681 let f = field.force_at(Vec3::new(1.0, 0.0, 0.0), 1.0, 0.0, 0.0);
682 assert!(f.y.abs() > 0.1);
684 assert!(f.x.abs() < 0.1);
685 }
686
687 #[test]
688 fn test_pulsing_varies_over_time() {
689 let field = ForceField::Pulsing {
690 center: origin(),
691 frequency: 1.0,
692 amplitude: 1.0,
693 radius: 10.0,
694 };
695 let f0 = field.force_at(Vec3::X, 1.0, 0.0, 0.0);
696 let f1 = field.force_at(Vec3::X, 1.0, 0.0, 0.25);
697 assert!((f0.x - f1.x).abs() > 0.01);
698 }
699
700 #[test]
701 fn test_composer_add() {
702 let c = FieldComposer::new()
703 .add(ForceField::Flow { direction: Vec3::X, strength: 1.0, turbulence: 0.0 })
704 .add(ForceField::Flow { direction: Vec3::X, strength: 1.0, turbulence: 0.0 });
705 let f = c.force_at(origin(), 1.0, 0.0, 0.0);
706 assert!((f.x - 2.0).abs() < 0.01);
707 }
708
709 #[test]
710 fn test_field_sampler() {
711 let mut sampler = FieldSampler::new(8, 8, (-4.0, -4.0, 4.0, 4.0));
712 let field = ForceField::Flow { direction: Vec3::X, strength: 1.0, turbulence: 0.0 };
713 sampler.sample(&field);
714 let max = sampler.max_magnitude();
715 assert!((max - 1.0).abs() < 0.01);
716 }
717
718 #[test]
719 fn test_animated_field() {
720 let af = AnimatedField::new(
721 ForceField::Flow { direction: Vec3::X, strength: 1.0, turbulence: 0.0 }
722 )
723 .key(0.0, 0.0, Vec3::ZERO)
724 .key(1.0, 2.0, Vec3::ZERO);
725 let f0 = af.force_at(Vec3::ZERO, 1.0, 0.0, 0.0);
726 let f1 = af.force_at(Vec3::ZERO, 1.0, 0.0, 1.0);
727 assert!(f0.x < f1.x); }
729
730 #[test]
731 fn test_falloff_factors() {
732 assert!((falloff_factor(Falloff::None, 5.0, 10.0) - 1.0).abs() < 1e-6);
733 assert!((falloff_factor(Falloff::Linear, 5.0, 10.0) - 0.5).abs() < 1e-6);
734 assert!(falloff_factor(Falloff::InverseSquare, 2.0, 10.0) < 1.0);
735 let g = falloff_factor(Falloff::Gaussian(2.0), 0.0, 10.0);
736 assert!((g - 1.0).abs() < 1e-5);
737 }
738
739 #[test]
740 fn test_preset_planet_gravity() {
741 let field = FieldPresets::planet(Vec3::new(10.0, 0.0, 0.0), 100.0);
742 let f = field.force_at(Vec3::ZERO, 1.0, 0.0, 0.0);
743 assert!(f.x > 0.0);
744 }
745
746 #[test]
747 fn test_shockwave_zero_before_wave_arrives() {
748 let field = FieldPresets::explosion(origin(), 10.0, 0.0);
749 let f = field.force_at(Vec3::new(100.0, 0.0, 0.0), 1.0, 0.0, 0.0);
750 assert_eq!(f, Vec3::ZERO);
751 }
752
753 #[test]
754 fn test_field_label() {
755 let field = ForceField::Flow { direction: Vec3::X, strength: 1.0, turbulence: 0.0 };
756 assert_eq!(field.label(), "Flow");
757 }
758}