1#![doc = include_str!("../README.md")]
2#![cfg_attr(feature = "no_std", no_std)]
3
4#[cfg(any(
5 all(feature = "std", feature = "no_std"),
6 all(not(feature = "std"), not(feature = "no_std"))
7))]
8compile_error!("only one of `std` or `no_std` features must be enabled");
9
10use num_traits::Float;
37
38pub mod prelude {
39 pub use crate::{Spring, SpringCollection, SpringConfig, SpringParams, SpringTimeStep};
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct SpringConfig<F> {
46 angular_freq: F,
47 damping_ratio: F,
48}
49
50impl<F: Float> SpringConfig<F> {
51 pub fn new(angular_freq: F, damping_ratio: F) -> Self {
53 Self {
54 angular_freq: angular_freq.max(F::zero()),
55 damping_ratio: damping_ratio.max(F::zero()),
56 }
57 }
58
59 #[inline]
61 pub fn angular_freq(&self) -> F {
62 self.angular_freq
63 }
64
65 #[inline]
67 pub fn damping_ratio(&self) -> F {
68 self.damping_ratio
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum SpringParams<F> {
77 Static,
79 OverDamped { zb: F, z1: F, z2: F },
81 CriticallyDamped { angular_freq: F },
83 UnderDamped { oz: F, a: F },
85}
86
87impl<F: Float> From<SpringConfig<F>> for SpringParams<F> {
88 fn from(
89 SpringConfig {
90 angular_freq,
91 damping_ratio,
92 }: SpringConfig<F>,
93 ) -> Self {
94 if angular_freq < F::epsilon() {
95 return Self::Static;
96 }
97
98 if damping_ratio > F::one() + F::epsilon() {
99 let za = -angular_freq * damping_ratio;
101 let zb = angular_freq * (damping_ratio * damping_ratio - F::one()).sqrt();
102 let z1 = za - zb;
103 let z2 = za + zb;
104
105 Self::OverDamped { zb, z1, z2 }
106 } else if damping_ratio < F::one() - F::epsilon() {
107 let oz = angular_freq * damping_ratio;
109 let a = angular_freq * (F::one() - damping_ratio * damping_ratio).sqrt();
110
111 Self::UnderDamped { oz, a }
112 } else {
113 Self::CriticallyDamped { angular_freq }
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq)]
145pub struct SpringTimeStep<F> {
146 pp: F,
147 pv: F,
148 vp: F,
149 vv: F,
150}
151
152impl<F: Float> Default for SpringTimeStep<F> {
153 fn default() -> Self {
154 Self {
155 pp: F::one(),
156 pv: F::zero(),
157 vp: F::zero(),
158 vv: F::one(),
159 }
160 }
161}
162
163impl<F: Float> SpringTimeStep<F> {
164 pub fn new(state: impl Into<SpringParams<F>>, delta: F) -> Self {
166 match state.into() {
167 SpringParams::Static => Self::default(),
168
169 SpringParams::OverDamped { zb, z1, z2 } => {
170 let e1 = (z1 * delta).exp();
171 let e2 = (z2 * delta).exp();
172 let inv_2zb = F::one() / ((F::one() + F::one()) * zb);
173
174 let e1_2zb = e1 * inv_2zb;
175 let e2_2zb = e2 * inv_2zb;
176
177 let z1e1_2zb = z1 * e1_2zb;
178 let z2e2_2zb = z2 * e2_2zb;
179
180 Self {
181 pp: e1_2zb * z2 - z2e2_2zb + e2,
182 pv: -e1_2zb + e2_2zb,
183 vp: (z1e1_2zb - z2e2_2zb + e2) * z2,
184 vv: -z1e1_2zb + z2e2_2zb,
185 }
186 }
187
188 SpringParams::UnderDamped { oz, a } => {
189 let exp = (-oz * delta).exp();
190 let cos = (a * delta).cos();
191 let sin = (a * delta).sin();
192
193 let inv_alpha = F::one() / a;
194
195 let exp_sin = exp * sin;
196 let exp_cos = exp * cos;
197 let exp_ozs_alpha = exp * oz * sin * inv_alpha;
198
199 Self {
200 pp: exp_cos + exp_ozs_alpha,
201 pv: exp_sin * inv_alpha,
202 vp: -exp_sin * a - oz * exp_ozs_alpha,
203 vv: exp_cos - exp_ozs_alpha,
204 }
205 }
206
207 SpringParams::CriticallyDamped { angular_freq } => {
208 let exp = (-angular_freq * delta).exp();
209 let time_exp = delta * exp;
210 let time_exp_freq = time_exp * angular_freq;
211
212 Self {
213 pp: time_exp_freq * exp,
214 pv: time_exp,
215 vp: -angular_freq * time_exp_freq,
216 vv: -time_exp_freq * exp,
217 }
218 }
219 }
220 }
221
222 #[inline]
225 pub fn update_many(self, springs: &mut [&mut Spring<F>]) {
226 for spring in springs {
227 spring.update(self);
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq)]
234pub struct Spring<F> {
235 pub position: F,
236 pub velocity: F,
237 pub equilibrium: F,
238}
239
240impl<F: Float> Default for Spring<F> {
241 fn default() -> Self {
242 Self {
243 position: F::zero(),
244 velocity: F::zero(),
245 equilibrium: F::zero(),
246 }
247 }
248}
249
250impl<F: Float> Spring<F> {
251 pub fn new() -> Self {
253 Self::default()
254 }
255
256 pub fn from_equilibrium(equilibrium: F) -> Self {
258 Self {
259 position: F::zero(),
260 velocity: F::zero(),
261 equilibrium,
262 }
263 }
264
265 pub fn with_position(mut self, position: F) -> Self {
267 self.position = position;
268 self
269 }
270
271 pub fn with_velocity(mut self, velocity: F) -> Self {
273 self.velocity = velocity;
274 self
275 }
276
277 pub fn with_equilibrium(mut self, equilibrium: F) -> Self {
279 self.equilibrium = equilibrium;
280 self
281 }
282
283 #[inline]
284 fn update_internal(
285 position: &mut F,
286 velocity: &mut F,
287 equilibrium: F,
288 time_step: SpringTimeStep<F>,
289 ) {
290 let op = *position - equilibrium;
291 let ov = *velocity;
292
293 *position = op * time_step.pp + ov * time_step.pv + equilibrium;
294 *velocity = op * time_step.vp + ov * time_step.vv;
295 }
296
297 pub fn update(&mut self, time_step: SpringTimeStep<F>) {
299 Self::update_internal(
300 &mut self.position,
301 &mut self.velocity,
302 self.equilibrium,
303 time_step,
304 );
305 }
306
307 #[inline]
314 pub fn update_single(&mut self, state: SpringParams<F>, delta: F) {
315 self.update(SpringTimeStep::new(state, delta));
316 }
317}
318
319#[derive(Debug, Clone, PartialEq)]
322pub struct SpringCollection<F, const N: usize> {
323 params: SpringParams<F>,
324 positions: [F; N],
325 velocities: [F; N],
326 equilibriums: [F; N],
327}
328
329impl<F: Float, const N: usize, T> From<T> for SpringCollection<F, N>
330where
331 T: Into<SpringParams<F>>,
332{
333 fn from(params: T) -> Self {
334 Self {
335 params: params.into(),
336 positions: [F::zero(); N],
337 velocities: [F::zero(); N],
338 equilibriums: [F::zero(); N],
339 }
340 }
341}
342
343impl<F: Float, const N: usize> SpringCollection<F, N> {
344 pub fn from_equilibrium(params: impl Into<SpringParams<F>>, equilibrium: F) -> Self {
346 Self {
347 params: params.into(),
348 positions: [F::zero(); N],
349 velocities: [F::zero(); N],
350 equilibriums: [equilibrium; N],
351 }
352 }
353
354 pub fn from_equilibriums(params: impl Into<SpringParams<F>>, equilibriums: [F; N]) -> Self {
356 Self {
357 params: params.into(),
358 positions: [F::zero(); N],
359 velocities: [F::zero(); N],
360 equilibriums,
361 }
362 }
363
364 #[inline]
367 pub fn update(&mut self, delta: F) {
368 self.update_with(SpringTimeStep::new(self.params, delta));
369 }
370
371 #[inline]
376 pub fn update_with(&mut self, time_step: SpringTimeStep<F>) {
377 for i in 0..N {
378 Spring::update_internal(
379 &mut self.positions[i],
380 &mut self.velocities[i],
381 self.equilibriums[i],
382 time_step,
383 );
384 }
385 }
386}
387
388macro_rules! impl_collection_props {
389 ( $prop:ident , $prop_mut:ident ) => {
390 impl<F: Float, const N: usize> SpringCollection<F, N> {
391 #[doc = concat!("The array of current spring ", stringify!($plural), ".")]
392 #[inline]
393 pub fn $prop(&self) -> &[F; N] {
394 &self.$prop
395 }
396
397 #[doc = concat!("Mutable reference of current spring ", stringify!($prop), ".")]
398 #[inline]
399 pub fn $prop_mut(&mut self) -> &mut [F; N] {
400 &mut self.$prop
401 }
402 }
403 };
404}
405
406impl_collection_props!(positions, positions_mut);
407impl_collection_props!(velocities, velocities_mut);
408impl_collection_props!(equilibriums, equilibriums_mut);
409
410impl<F: Float, const N: usize> From<SpringCollection<F, N>> for [Spring<F>; N] {
411 fn from(value: SpringCollection<F, N>) -> Self {
412 let mut i = 0;
413
414 value.positions.map(|position| {
415 let spring = Spring {
416 position,
417 velocity: value.velocities[i],
418 equilibrium: value.equilibriums[i],
419 };
420 i += 1;
421 spring
422 })
423 }
424}