1use crate::body::RigidBody;
11use nalgebra::SVector;
12use symtropy_math::{Bivector, Point, Rotor};
13
14pub fn integrate<const D: usize>(body: &mut RigidBody<D>, gravity: &SVector<f64, D>, dt: f64) {
19 if !body.is_dynamic() {
20 body.clear_accumulators();
21 return;
22 }
23
24 if !body.force_accumulator.iter().all(|v| v.is_finite()) {
28 body.force_accumulator = SVector::zeros();
29 }
30 if !body.torque_accumulator.is_finite() {
31 body.torque_accumulator = Bivector::zero();
32 }
33
34 let accel = body.force_accumulator * body.inv_mass + gravity;
38
39 body.linear_velocity += accel * dt;
41
42 if body.linear_damping > 1e-15 {
45 let tau = 1.0 / body.linear_damping;
46 body.linear_velocity *= (-dt / tau).exp();
47 }
48
49 let speed = body.linear_velocity.norm();
51 if speed > 1000.0 {
52 body.linear_velocity *= 1000.0 / speed;
53 }
54
55 let displacement = body.linear_velocity * dt;
57 body.transform.translation = Point(body.transform.translation.0 + displacement);
58
59 let inv_i_avg = body.inv_inertia.sum() / D as f64;
66 let ang_accel = body.torque_accumulator.scale(inv_i_avg);
67
68 body.angular_velocity = body.angular_velocity.add(&ang_accel.scale(dt));
70
71 if body.angular_damping > 1e-15 {
73 let ang_tau = 1.0 / body.angular_damping;
74 body.angular_velocity = body.angular_velocity.scale((-dt / ang_tau).exp());
75 }
76
77 let ang_speed = body.angular_velocity.norm();
79 if ang_speed > 100.0 {
80 body.angular_velocity = body.angular_velocity.scale(100.0 / ang_speed);
81 }
82
83 if ang_speed > 1e-12 {
85 let delta_angle = ang_speed * dt;
86 let delta_rotation = Rotor::from_plane_angle(&body.angular_velocity, delta_angle);
87 body.transform.rotation = delta_rotation.compose(&body.transform.rotation);
88 }
89
90 if !body.linear_velocity.iter().all(|v| v.is_finite()) {
94 debug_assert!(false, "NaN/Inf detected in linear_velocity after integration");
95 body.linear_velocity = SVector::zeros();
96 }
97 if !body.angular_velocity.is_finite() {
98 debug_assert!(false, "NaN/Inf detected in angular_velocity after integration");
99 body.angular_velocity = Bivector::zero();
100 }
101 if !body.transform.translation.0.iter().all(|v| v.is_finite()) {
102 debug_assert!(false, "NaN/Inf detected in position after integration");
103 body.linear_velocity = SVector::zeros();
105 }
106
107 body.clear_accumulators();
109}
110
111pub fn separate<const D: usize>(
113 body: &mut RigidBody<D>,
114 correction: &SVector<f64, D>,
115) {
116 if body.is_dynamic() {
117 body.transform.translation = Point(body.transform.translation.0 + correction);
118 }
119}
120
121pub fn apply_impulse<const D: usize>(
123 body: &mut RigidBody<D>,
124 impulse: &SVector<f64, D>,
125) {
126 if body.is_dynamic() {
127 body.linear_velocity += impulse * body.inv_mass;
128 }
129}
130
131pub fn apply_angular_impulse<const D: usize>(
133 body: &mut RigidBody<D>,
134 impulse: &Bivector<D>,
135) {
136 if body.is_dynamic() {
137 let inv_i_avg = body.inv_inertia.sum() / D as f64;
138 body.angular_velocity = body.angular_velocity.add(&impulse.scale(inv_i_avg));
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::body::BodyHandle;
146
147 #[test]
148 fn free_fall_3d() {
149 let mut body = RigidBody::<3>::dynamic_sphere(
150 BodyHandle(0),
151 Point::origin(),
152 1.0,
153 1.0,
154 );
155 let gravity = SVector::from([0.0, -9.81, 0.0]);
156 let dt = 0.01;
157
158 for _ in 0..100 {
160 integrate(&mut body, &gravity, dt);
161 }
162
163 let y = body.transform.translation.coord(1);
165 assert!(y < -3.0, "y = {y}, expected < -3.0");
166 assert!(y > -6.0, "y = {y}, expected > -6.0 (damping bounds)");
167 }
168
169 #[test]
170 fn stationary_without_forces() {
171 let mut body = RigidBody::<3>::dynamic_sphere(
172 BodyHandle(0),
173 Point::new([5.0, 5.0, 5.0]),
174 1.0,
175 1.0,
176 );
177 let zero_gravity = SVector::zeros();
178 let dt = 0.01;
179
180 for _ in 0..100 {
181 integrate(&mut body, &zero_gravity, dt);
182 }
183
184 assert!((body.transform.translation.coord(0) - 5.0).abs() < 0.1);
186 }
187
188 #[test]
189 fn applied_force_accelerates() {
190 let mut body = RigidBody::<3>::dynamic_sphere(
191 BodyHandle(0),
192 Point::origin(),
193 1.0,
194 2.0, );
196 let zero_gravity = SVector::zeros();
197 let dt = 0.01;
198
199 for _ in 0..100 {
201 body.apply_force(SVector::from([10.0, 0.0, 0.0]));
202 integrate(&mut body, &zero_gravity, dt);
203 }
204
205 let vx = body.linear_velocity[0];
207 assert!(vx > 3.0, "vx = {vx}, expected > 3.0");
208 assert!(vx < 6.0, "vx = {vx}, expected < 6.0");
209 }
210
211 #[test]
212 fn angular_velocity_rotates_body() {
213 let mut body = RigidBody::<3>::dynamic_sphere(
214 BodyHandle(0),
215 Point::origin(),
216 1.0,
217 1.0,
218 );
219 body.angular_velocity = Bivector::<3>::unit_plane(0, 1).scale(1.0);
221
222 let zero_gravity = SVector::zeros();
223 let dt = 0.01;
224
225 for _ in 0..100 {
226 integrate(&mut body, &zero_gravity, dt);
227 }
228
229 let mat = body.transform.rotation.to_matrix();
231 let trace = mat.trace();
232 assert!(trace < 2.99, "trace = {trace}, body didn't rotate");
234 }
235
236 #[test]
237 fn impulse_changes_velocity() {
238 let mut body = RigidBody::<4>::dynamic_sphere(
239 BodyHandle(0),
240 Point::origin(),
241 1.0,
242 5.0, );
244 apply_impulse(&mut body, &SVector::from([50.0, 0.0, 0.0, 0.0]));
245 assert!((body.linear_velocity[0] - 10.0).abs() < 1e-10);
247 }
248
249 #[test]
250 fn energy_conservation_no_damping() {
251 let mut body = RigidBody::<3>::dynamic_sphere(
252 BodyHandle(0),
253 Point::new([0.0, 100.0, 0.0]),
254 1.0,
255 1.0,
256 );
257 body.linear_damping = 0.0;
258 body.angular_damping = 0.0;
259
260 let gravity = SVector::from([0.0, -9.81, 0.0]);
261 let dt = 0.001; let initial_energy = body.kinetic_energy() + body.mass * 9.81 * body.transform.translation.coord(1);
264
265 for _ in 0..1000 {
266 integrate(&mut body, &gravity, dt);
267 }
268
269 let final_energy = body.kinetic_energy() + body.mass * 9.81 * body.transform.translation.coord(1);
270 let drift = ((final_energy - initial_energy) / initial_energy).abs();
271 assert!(drift < 0.01, "energy drift {drift} > 1%");
272 }
273
274 #[test]
275 fn static_body_not_affected() {
276 use symtropy_math::Sphere;
277 let mut body = RigidBody::<3>::static_body(
278 BodyHandle(0),
279 Point::new([5.0, 5.0, 5.0]),
280 Box::new(Sphere::unit()),
281 );
282 body.apply_force(SVector::from([1000.0, 0.0, 0.0]));
283 let gravity = SVector::from([0.0, -100.0, 0.0]);
284 integrate(&mut body, &gravity, 1.0);
285 assert!((body.transform.translation.coord(0) - 5.0).abs() < 1e-12);
286 }
287
288 #[test]
289 fn nan_force_is_sanitized() {
290 let mut body = RigidBody::<3>::dynamic_sphere(
291 BodyHandle(0),
292 Point::new([1.0, 2.0, 3.0]),
293 1.0,
294 1.0,
295 );
296 body.apply_force(SVector::from([f64::NAN, 0.0, 0.0]));
298 let gravity = SVector::zeros();
299 integrate(&mut body, &gravity, 0.01);
300
301 assert!(body.linear_velocity.iter().all(|v| v.is_finite()),
303 "NaN force propagated to velocity");
304 assert!(body.transform.translation.0.iter().all(|v| v.is_finite()),
305 "NaN force propagated to position");
306 }
307
308 #[test]
309 fn inf_force_is_sanitized() {
310 let mut body = RigidBody::<3>::dynamic_sphere(
311 BodyHandle(0),
312 Point::origin(),
313 1.0,
314 1.0,
315 );
316 body.apply_force(SVector::from([f64::INFINITY, 0.0, 0.0]));
317 let gravity = SVector::zeros();
318 integrate(&mut body, &gravity, 0.01);
319
320 assert!(body.linear_velocity.iter().all(|v| v.is_finite()),
321 "Inf force propagated to velocity");
322 }
323}