1use crate::math::{Fix128, Vec3Fix};
17use crate::solver::RigidBody;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ChargedBody {
22 pub body: usize,
24 pub charge_c: Fix128,
26}
27
28impl ChargedBody {
29 #[must_use]
31 pub const fn new(body: usize, charge_c: Fix128) -> Self {
32 Self { body, charge_c }
33 }
34}
35
36#[derive(Debug, Clone, Copy)]
38pub enum EmSource {
39 Uniform {
41 electric: Vec3Fix,
43 magnetic: Vec3Fix,
45 },
46 PointCharge {
50 position: Vec3Fix,
52 charge_c: Fix128,
54 },
55 MagneticDipole {
59 position: Vec3Fix,
61 moment: Vec3Fix,
63 },
64}
65
66impl EmSource {
67 #[must_use]
69 pub fn sample(&self, point: Vec3Fix) -> (Vec3Fix, Vec3Fix) {
70 match *self {
71 Self::Uniform { electric, magnetic } => (electric, magnetic),
72 Self::PointCharge { position, charge_c } => {
73 let r = point - position;
74 let r_sq = r.length_squared();
75 if r_sq <= Fix128::ZERO {
76 return (Vec3Fix::ZERO, Vec3Fix::ZERO);
77 }
78 let k = Fix128::from_int(8_990_000_000);
80 let r_mag = r_sq.sqrt();
81 let scale = k * charge_c / (r_sq * r_mag);
82 (r * scale, Vec3Fix::ZERO)
83 }
84 Self::MagneticDipole { position, moment } => {
85 let r = point - position;
86 let r_sq = r.length_squared();
87 if r_sq <= Fix128::ZERO {
88 return (Vec3Fix::ZERO, Vec3Fix::ZERO);
89 }
90 let r_mag = r_sq.sqrt();
91 let r_hat = r * (Fix128::ONE / r_mag);
92 let m_dot_r = moment.dot(r_hat);
93 let inv_r3 = Fix128::ONE / (r_sq * r_mag);
94 let mu0_over_4pi = Fix128::from_ratio(1, 10_000_000);
96 let three = Fix128::from_int(3);
97 let b = (r_hat * (three * m_dot_r) - moment) * (mu0_over_4pi * inv_r3);
98 (Vec3Fix::ZERO, b)
99 }
100 }
101 }
102}
103
104#[must_use]
106pub fn lorentz_force(charged: ChargedBody, body: &RigidBody, source: &EmSource) -> Vec3Fix {
107 let (e_field, b_field) = source.sample(body.position);
108 let v_cross_b = body.velocity.cross(b_field);
109 (e_field + v_cross_b) * charged.charge_c
110}
111
112#[must_use]
114pub fn lorentz_force_sum(charged: ChargedBody, body: &RigidBody, sources: &[EmSource]) -> Vec3Fix {
115 sources.iter().fold(Vec3Fix::ZERO, |acc, s| {
116 acc + lorentz_force(charged, body, s)
117 })
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::math::QuatFix;
124 use crate::solver::{BodyType, RigidBody};
125
126 fn body_at(x: i64, y: i64, z: i64, vx: i64, vy: i64, vz: i64) -> RigidBody {
127 RigidBody {
128 position: Vec3Fix::new(
129 Fix128::from_int(x),
130 Fix128::from_int(y),
131 Fix128::from_int(z),
132 ),
133 velocity: Vec3Fix::new(
134 Fix128::from_int(vx),
135 Fix128::from_int(vy),
136 Fix128::from_int(vz),
137 ),
138 inv_mass: Fix128::ONE,
139 inv_inertia: Vec3Fix::ZERO,
140 prev_position: Vec3Fix::ZERO,
141 rotation: QuatFix::IDENTITY,
142 angular_velocity: Vec3Fix::ZERO,
143 prev_rotation: QuatFix::IDENTITY,
144 restitution: Fix128::ZERO,
145 friction: Fix128::ZERO,
146 gravity_scale: Fix128::ONE,
147 linear_damping: Fix128::ONE,
148 angular_damping: Fix128::ONE,
149 is_sensor: false,
150 body_type: BodyType::Dynamic,
151 kinematic_target: None,
152 }
153 }
154
155 #[test]
156 fn uniform_electric_field_pushes_positive_charge() {
157 let source = EmSource::Uniform {
158 electric: Vec3Fix::new(Fix128::from_int(10), Fix128::ZERO, Fix128::ZERO),
159 magnetic: Vec3Fix::ZERO,
160 };
161 let charged = ChargedBody::new(0, Fix128::ONE);
162 let body = body_at(0, 0, 0, 0, 0, 0);
163 let f = lorentz_force(charged, &body, &source);
164 assert!(f.x > Fix128::ZERO);
165 assert_eq!(f.y, Fix128::ZERO);
166 assert_eq!(f.z, Fix128::ZERO);
167 }
168
169 #[test]
170 fn uniform_electric_field_pulls_negative_charge() {
171 let source = EmSource::Uniform {
172 electric: Vec3Fix::new(Fix128::from_int(10), Fix128::ZERO, Fix128::ZERO),
173 magnetic: Vec3Fix::ZERO,
174 };
175 let charged = ChargedBody::new(0, -Fix128::ONE);
176 let body = body_at(0, 0, 0, 0, 0, 0);
177 let f = lorentz_force(charged, &body, &source);
178 assert!(f.x < Fix128::ZERO);
179 }
180
181 #[test]
182 fn magnetic_field_produces_perpendicular_force() {
183 let source = EmSource::Uniform {
185 electric: Vec3Fix::ZERO,
186 magnetic: Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE),
187 };
188 let charged = ChargedBody::new(0, Fix128::ONE);
189 let body = body_at(0, 0, 0, 1, 0, 0);
190 let f = lorentz_force(charged, &body, &source);
191 assert!(f.y < Fix128::ZERO);
192 assert_eq!(f.x, Fix128::ZERO);
193 assert_eq!(f.z, Fix128::ZERO);
194 }
195
196 #[test]
197 fn stationary_body_feels_no_magnetic_force() {
198 let source = EmSource::Uniform {
199 electric: Vec3Fix::ZERO,
200 magnetic: Vec3Fix::new(Fix128::ONE, Fix128::ZERO, Fix128::ZERO),
201 };
202 let charged = ChargedBody::new(0, Fix128::ONE);
203 let body = body_at(0, 0, 0, 0, 0, 0);
204 let f = lorentz_force(charged, &body, &source);
205 assert_eq!(f, Vec3Fix::ZERO);
206 }
207
208 #[test]
209 fn point_charge_produces_radial_field() {
210 let source = EmSource::PointCharge {
211 position: Vec3Fix::ZERO,
212 charge_c: Fix128::from_ratio(1, 1000),
213 };
214 let (e, b) = source.sample(Vec3Fix::new(
215 Fix128::from_int(1),
216 Fix128::ZERO,
217 Fix128::ZERO,
218 ));
219 assert!(e.x > Fix128::ZERO);
220 assert_eq!(e.y, Fix128::ZERO);
221 assert_eq!(b, Vec3Fix::ZERO);
222 }
223
224 #[test]
225 fn magnetic_dipole_axis_alignment() {
226 let source = EmSource::MagneticDipole {
227 position: Vec3Fix::ZERO,
228 moment: Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE),
229 };
230 let (_e, b) = source.sample(Vec3Fix::new(
232 Fix128::ZERO,
233 Fix128::ZERO,
234 Fix128::from_int(1),
235 ));
236 assert!(b.z > Fix128::ZERO);
237 }
238
239 #[test]
240 fn lorentz_force_sum_accumulates_multiple_sources() {
241 let s1 = EmSource::Uniform {
242 electric: Vec3Fix::new(Fix128::from_int(1), Fix128::ZERO, Fix128::ZERO),
243 magnetic: Vec3Fix::ZERO,
244 };
245 let s2 = EmSource::Uniform {
246 electric: Vec3Fix::new(Fix128::from_int(2), Fix128::ZERO, Fix128::ZERO),
247 magnetic: Vec3Fix::ZERO,
248 };
249 let charged = ChargedBody::new(0, Fix128::ONE);
250 let body = body_at(0, 0, 0, 0, 0, 0);
251 let f = lorentz_force_sum(charged, &body, &[s1, s2]);
252 assert_eq!(f.x, Fix128::from_int(3));
253 }
254}