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!(
95 false,
96 "NaN/Inf detected in linear_velocity after integration"
97 );
98 body.linear_velocity = SVector::zeros();
99 }
100 if !body.angular_velocity.is_finite() {
101 debug_assert!(
102 false,
103 "NaN/Inf detected in angular_velocity after integration"
104 );
105 body.angular_velocity = Bivector::zero();
106 }
107 if !body.transform.translation.0.iter().all(|v| v.is_finite()) {
108 debug_assert!(false, "NaN/Inf detected in position after integration");
109 body.linear_velocity = SVector::zeros();
111 }
112
113 body.clear_accumulators();
115}
116
117pub fn separate<const D: usize>(body: &mut RigidBody<D>, correction: &SVector<f64, D>) {
119 if body.is_dynamic() {
120 body.transform.translation = Point(body.transform.translation.0 + correction);
121 }
122}
123
124pub fn apply_impulse<const D: usize>(body: &mut RigidBody<D>, impulse: &SVector<f64, D>) {
126 if body.is_dynamic() {
127 body.linear_velocity += impulse * body.inv_mass;
128 }
129}
130
131pub fn apply_angular_impulse<const D: usize>(body: &mut RigidBody<D>, impulse: &Bivector<D>) {
133 if body.is_dynamic() {
134 let inv_i_avg = body.inv_inertia.sum() / D as f64;
135 body.angular_velocity = body.angular_velocity.add(&impulse.scale(inv_i_avg));
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::body::BodyHandle;
143
144 #[test]
145 fn free_fall_3d() {
146 let mut body = RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0);
147 let gravity = SVector::from([0.0, -9.81, 0.0]);
148 let dt = 0.01;
149
150 for _ in 0..100 {
152 integrate(&mut body, &gravity, dt);
153 }
154
155 let y = body.transform.translation.coord(1);
157 assert!(y < -3.0, "y = {y}, expected < -3.0");
158 assert!(y > -6.0, "y = {y}, expected > -6.0 (damping bounds)");
159 }
160
161 #[test]
162 fn stationary_without_forces() {
163 let mut body =
164 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([5.0, 5.0, 5.0]), 1.0, 1.0);
165 let zero_gravity = SVector::zeros();
166 let dt = 0.01;
167
168 for _ in 0..100 {
169 integrate(&mut body, &zero_gravity, dt);
170 }
171
172 assert!((body.transform.translation.coord(0) - 5.0).abs() < 0.1);
174 }
175
176 #[test]
177 fn applied_force_accelerates() {
178 let mut body = RigidBody::<3>::dynamic_sphere(
179 BodyHandle(0),
180 Point::origin(),
181 1.0,
182 2.0, );
184 let zero_gravity = SVector::zeros();
185 let dt = 0.01;
186
187 for _ in 0..100 {
189 body.apply_force(SVector::from([10.0, 0.0, 0.0]));
190 integrate(&mut body, &zero_gravity, dt);
191 }
192
193 let vx = body.linear_velocity[0];
195 assert!(vx > 3.0, "vx = {vx}, expected > 3.0");
196 assert!(vx < 6.0, "vx = {vx}, expected < 6.0");
197 }
198
199 #[test]
200 fn angular_velocity_rotates_body() {
201 let mut body = RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0);
202 body.angular_velocity = Bivector::<3>::unit_plane(0, 1).scale(1.0);
204
205 let zero_gravity = SVector::zeros();
206 let dt = 0.01;
207
208 for _ in 0..100 {
209 integrate(&mut body, &zero_gravity, dt);
210 }
211
212 let mat = body.transform.rotation.to_matrix();
214 let trace = mat.trace();
215 assert!(trace < 2.99, "trace = {trace}, body didn't rotate");
217 }
218
219 #[test]
220 fn impulse_changes_velocity() {
221 let mut body = RigidBody::<4>::dynamic_sphere(
222 BodyHandle(0),
223 Point::origin(),
224 1.0,
225 5.0, );
227 apply_impulse(&mut body, &SVector::from([50.0, 0.0, 0.0, 0.0]));
228 assert!((body.linear_velocity[0] - 10.0).abs() < 1e-10);
230 }
231
232 #[test]
233 fn energy_conservation_no_damping() {
234 let mut body =
235 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([0.0, 100.0, 0.0]), 1.0, 1.0);
236 body.linear_damping = 0.0;
237 body.angular_damping = 0.0;
238
239 let gravity = SVector::from([0.0, -9.81, 0.0]);
240 let dt = 0.001; let initial_energy =
243 body.kinetic_energy() + body.mass * 9.81 * body.transform.translation.coord(1);
244
245 for _ in 0..1000 {
246 integrate(&mut body, &gravity, dt);
247 }
248
249 let final_energy =
250 body.kinetic_energy() + body.mass * 9.81 * body.transform.translation.coord(1);
251 let drift = ((final_energy - initial_energy) / initial_energy).abs();
252 assert!(drift < 0.01, "energy drift {drift} > 1%");
253 }
254
255 #[test]
256 fn static_body_not_affected() {
257 use symtropy_math::Sphere;
258 let mut body = RigidBody::<3>::static_body(
259 BodyHandle(0),
260 Point::new([5.0, 5.0, 5.0]),
261 Box::new(Sphere::unit()),
262 );
263 body.apply_force(SVector::from([1000.0, 0.0, 0.0]));
264 let gravity = SVector::from([0.0, -100.0, 0.0]);
265 integrate(&mut body, &gravity, 1.0);
266 assert!((body.transform.translation.coord(0) - 5.0).abs() < 1e-12);
267 }
268
269 #[test]
270 fn nan_force_is_sanitized() {
271 let mut body =
272 RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::new([1.0, 2.0, 3.0]), 1.0, 1.0);
273 body.apply_force(SVector::from([f64::NAN, 0.0, 0.0]));
275 let gravity = SVector::zeros();
276 integrate(&mut body, &gravity, 0.01);
277
278 assert!(
280 body.linear_velocity.iter().all(|v| v.is_finite()),
281 "NaN force propagated to velocity"
282 );
283 assert!(
284 body.transform.translation.0.iter().all(|v| v.is_finite()),
285 "NaN force propagated to position"
286 );
287 }
288
289 #[test]
290 fn inf_force_is_sanitized() {
291 let mut body = RigidBody::<3>::dynamic_sphere(BodyHandle(0), Point::origin(), 1.0, 1.0);
292 body.apply_force(SVector::from([f64::INFINITY, 0.0, 0.0]));
293 let gravity = SVector::zeros();
294 integrate(&mut body, &gravity, 0.01);
295
296 assert!(
297 body.linear_velocity.iter().all(|v| v.is_finite()),
298 "Inf force propagated to velocity"
299 );
300 }
301}