1#[derive(Debug, Clone)]
14pub struct SpringDamper {
15 pub position: f32,
16 pub velocity: f32,
17 pub target: f32,
18 pub stiffness: f32,
19 pub damping: f32,
20}
21
22impl SpringDamper {
23 pub fn new(position: f32, stiffness: f32, damping: f32) -> Self {
24 Self { position, velocity: 0.0, target: position, stiffness, damping }
25 }
26
27 pub fn critical(position: f32, speed: f32) -> Self {
29 let k = speed * speed;
30 let d = 2.0 * speed;
31 Self::new(position, k, d)
32 }
33
34 pub fn bouncy(position: f32, frequency: f32, damping_ratio: f32) -> Self {
36 let k = frequency * frequency;
37 let d = 2.0 * damping_ratio * frequency;
38 Self::new(position, k, d)
39 }
40
41 pub fn tick(&mut self, dt: f32) {
43 let force = -self.stiffness * (self.position - self.target) - self.damping * self.velocity;
44 self.velocity += force * dt;
45 self.position += self.velocity * dt;
46 }
47
48 pub fn tick_get(&mut self, dt: f32) -> f32 {
50 self.tick(dt);
51 self.position
52 }
53
54 pub fn set_target(&mut self, target: f32) {
55 self.target = target;
56 }
57
58 pub fn teleport(&mut self, position: f32) {
59 self.position = position;
60 self.velocity = 0.0;
61 self.target = position;
62 }
63
64 pub fn is_settled(&self, threshold: f32) -> bool {
65 (self.position - self.target).abs() < threshold && self.velocity.abs() < threshold
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct Spring2D {
72 pub x: SpringDamper,
73 pub y: SpringDamper,
74}
75
76impl Spring2D {
77 pub fn new(px: f32, py: f32, stiffness: f32, damping: f32) -> Self {
78 Self {
79 x: SpringDamper::new(px, stiffness, damping),
80 y: SpringDamper::new(py, stiffness, damping),
81 }
82 }
83
84 pub fn critical(px: f32, py: f32, speed: f32) -> Self {
85 Self {
86 x: SpringDamper::critical(px, speed),
87 y: SpringDamper::critical(py, speed),
88 }
89 }
90
91 pub fn tick(&mut self, dt: f32) {
92 self.x.tick(dt);
93 self.y.tick(dt);
94 }
95
96 pub fn set_target(&mut self, tx: f32, ty: f32) {
97 self.x.set_target(tx);
98 self.y.set_target(ty);
99 }
100
101 pub fn position(&self) -> (f32, f32) {
102 (self.x.position, self.y.position)
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct Spring3D {
109 pub x: SpringDamper,
110 pub y: SpringDamper,
111 pub z: SpringDamper,
112}
113
114pub type SpringDamper3 = Spring3D;
116
117impl Spring3D {
118 pub fn new(px: f32, py: f32, pz: f32, stiffness: f32, damping: f32) -> Self {
120 Self {
121 x: SpringDamper::new(px, stiffness, damping),
122 y: SpringDamper::new(py, stiffness, damping),
123 z: SpringDamper::new(pz, stiffness, damping),
124 }
125 }
126
127 pub fn from_vec3(pos: glam::Vec3, stiffness: f32, damping: f32) -> Self {
129 Self::new(pos.x, pos.y, pos.z, stiffness, damping)
130 }
131
132 pub fn critical(px: f32, py: f32, pz: f32, speed: f32) -> Self {
133 Self {
134 x: SpringDamper::critical(px, speed),
135 y: SpringDamper::critical(py, speed),
136 z: SpringDamper::critical(pz, speed),
137 }
138 }
139
140 pub fn tick(&mut self, dt: f32) -> glam::Vec3 {
142 self.x.tick(dt);
143 self.y.tick(dt);
144 self.z.tick(dt);
145 self.position()
146 }
147
148 pub fn set_target(&mut self, t: glam::Vec3) {
150 self.x.set_target(t.x);
151 self.y.set_target(t.y);
152 self.z.set_target(t.z);
153 }
154
155 pub fn set_target_xyz(&mut self, tx: f32, ty: f32, tz: f32) {
156 self.x.set_target(tx);
157 self.y.set_target(ty);
158 self.z.set_target(tz);
159 }
160
161 pub fn position(&self) -> glam::Vec3 {
162 glam::Vec3::new(self.x.position, self.y.position, self.z.position)
163 }
164}
165
166#[derive(Debug, Clone)]
170pub struct ConstrainedSpring {
171 pub inner: SpringDamper,
172 pub min_pos: Option<f32>,
173 pub max_pos: Option<f32>,
174 pub min_vel: Option<f32>,
175 pub max_vel: Option<f32>,
176}
177
178impl ConstrainedSpring {
179 pub fn new(position: f32, stiffness: f32, damping: f32) -> Self {
180 Self {
181 inner: SpringDamper::new(position, stiffness, damping),
182 min_pos: None, max_pos: None,
183 min_vel: None, max_vel: None,
184 }
185 }
186
187 pub fn with_pos_limits(mut self, min: f32, max: f32) -> Self {
188 self.min_pos = Some(min);
189 self.max_pos = Some(max);
190 self
191 }
192
193 pub fn with_vel_limits(mut self, min: f32, max: f32) -> Self {
194 self.min_vel = Some(min);
195 self.max_vel = Some(max);
196 self
197 }
198
199 pub fn tick(&mut self, dt: f32) -> f32 {
200 self.inner.tick(dt);
201 if let Some(lo) = self.min_pos {
202 if self.inner.position < lo {
203 self.inner.position = lo;
204 self.inner.velocity = self.inner.velocity.max(0.0);
205 }
206 }
207 if let Some(hi) = self.max_pos {
208 if self.inner.position > hi {
209 self.inner.position = hi;
210 self.inner.velocity = self.inner.velocity.min(0.0);
211 }
212 }
213 if let Some(lo) = self.min_vel {
214 self.inner.velocity = self.inner.velocity.max(lo);
215 }
216 if let Some(hi) = self.max_vel {
217 self.inner.velocity = self.inner.velocity.min(hi);
218 }
219 self.inner.position
220 }
221
222 pub fn set_target(&mut self, t: f32) { self.inner.set_target(t); }
223 pub fn position(&self) -> f32 { self.inner.position }
224 pub fn velocity(&self) -> f32 { self.inner.velocity }
225 pub fn is_settled(&self, threshold: f32) -> bool { self.inner.is_settled(threshold) }
226}
227
228#[derive(Debug, Clone)]
234pub struct DistanceConstraint {
235 pub rest_length: f32,
237 pub stiffness: f32,
239 pub damping: f32,
241 pub allow_compression: bool,
243 pub allow_extension: bool,
245}
246
247impl DistanceConstraint {
248 pub fn new(rest_length: f32, stiffness: f32) -> Self {
249 Self {
250 rest_length,
251 stiffness,
252 damping: 0.3,
253 allow_compression: true,
254 allow_extension: true,
255 }
256 }
257
258 pub fn rod(rest_length: f32) -> Self {
260 Self { rest_length, stiffness: 1.0, damping: 0.5, allow_compression: true, allow_extension: true }
261 }
262
263 pub fn rope(rest_length: f32) -> Self {
265 Self { rest_length, stiffness: 0.9, damping: 0.4, allow_compression: false, allow_extension: true }
266 }
267
268 pub fn strut(rest_length: f32) -> Self {
270 Self { rest_length, stiffness: 0.9, damping: 0.4, allow_compression: true, allow_extension: false }
271 }
272
273 pub fn solve(
279 &self,
280 pa: glam::Vec3, pb: glam::Vec3,
281 mass_a: f32, mass_b: f32,
282 ) -> (glam::Vec3, glam::Vec3) {
283 let delta = pb - pa;
284 let dist = delta.length();
285 if dist < 1e-6 { return (glam::Vec3::ZERO, glam::Vec3::ZERO); }
286
287 let error = dist - self.rest_length;
288 let stretch = error > 0.0;
289 let compress = error < 0.0;
290
291 if stretch && !self.allow_extension { return (glam::Vec3::ZERO, glam::Vec3::ZERO); }
293 if compress && !self.allow_compression { return (glam::Vec3::ZERO, glam::Vec3::ZERO); }
294
295 let dir = delta / dist;
296 let total_mass = (mass_a + mass_b).max(1e-6);
297 let ratio_a = mass_b / total_mass;
298 let ratio_b = mass_a / total_mass;
299 let correction = dir * error * self.stiffness;
300
301 (correction * ratio_a, -correction * ratio_b)
302 }
303}
304
305#[derive(Debug, Clone)]
311pub struct PinConstraint {
312 pub anchor: glam::Vec3,
313 pub stiffness: f32,
315 pub dead_zone: f32,
317}
318
319impl PinConstraint {
320 pub fn new(anchor: glam::Vec3) -> Self {
321 Self { anchor, stiffness: 1.0, dead_zone: 0.0 }
322 }
323
324 pub fn soft(anchor: glam::Vec3, stiffness: f32) -> Self {
325 Self { anchor, stiffness, dead_zone: 0.0 }
326 }
327
328 pub fn with_dead_zone(mut self, zone: f32) -> Self {
329 self.dead_zone = zone;
330 self
331 }
332
333 pub fn solve(&self, pos: glam::Vec3) -> glam::Vec3 {
335 let delta = self.anchor - pos;
336 let dist = delta.length();
337 if dist <= self.dead_zone { return glam::Vec3::ZERO; }
338 let excess = dist - self.dead_zone;
339 let dir = delta / dist;
340 dir * excess * self.stiffness
341 }
342
343 pub fn move_anchor(&mut self, new_anchor: glam::Vec3) {
344 self.anchor = new_anchor;
345 }
346}
347
348#[derive(Debug, Clone)]
355pub struct SpringChain {
356 pub positions: Vec<glam::Vec3>,
358 pub velocities: Vec<glam::Vec3>,
360 pub rest_lengths: Vec<f32>,
362 pub stiffness: f32,
364 pub damping: f32,
366 pub masses: Vec<f32>,
368 pub pin_head: bool,
370 pub gravity: glam::Vec3,
372 pub iterations: usize,
374}
375
376impl SpringChain {
377 pub fn new(anchor: glam::Vec3, count: usize, segment_length: f32) -> Self {
382 let count = count.max(2);
383 let positions: Vec<glam::Vec3> = (0..count)
384 .map(|i| anchor + glam::Vec3::NEG_Y * (i as f32 * segment_length))
385 .collect();
386 let velocities = vec![glam::Vec3::ZERO; count];
387 let rest_lengths = vec![segment_length; count - 1];
388 let mut masses = vec![1.0_f32; count];
389 masses[0] = f32::INFINITY; Self {
391 positions, velocities, rest_lengths,
392 stiffness: 0.8, damping: 0.98,
393 masses, pin_head: true,
394 gravity: glam::Vec3::NEG_Y * 9.8,
395 iterations: 4,
396 }
397 }
398
399 pub fn horizontal(anchor: glam::Vec3, count: usize, segment_length: f32) -> Self {
401 let count = count.max(2);
402 let positions: Vec<glam::Vec3> = (0..count)
403 .map(|i| anchor + glam::Vec3::X * (i as f32 * segment_length))
404 .collect();
405 let velocities = vec![glam::Vec3::ZERO; count];
406 let rest_lengths = vec![segment_length; count - 1];
407 let mut masses = vec![1.0_f32; count];
408 masses[0] = f32::INFINITY;
409 Self {
410 positions, velocities, rest_lengths,
411 stiffness: 0.7, damping: 0.97,
412 masses, pin_head: true,
413 gravity: glam::Vec3::NEG_Y * 9.8,
414 iterations: 5,
415 }
416 }
417
418 pub fn set_anchor(&mut self, pos: glam::Vec3) {
420 self.positions[0] = pos;
421 }
422
423 pub fn tick(&mut self, dt: f32) {
427 let n = self.positions.len();
428
429 for i in 0..n {
431 if self.masses[i].is_infinite() { continue; }
432 self.velocities[i] += self.gravity * dt;
433 self.velocities[i] *= self.damping;
434 self.positions[i] += self.velocities[i] * dt;
435 }
436
437 for _ in 0..self.iterations {
439 for seg in 0..(n - 1) {
440 let pa = self.positions[seg];
441 let pb = self.positions[seg + 1];
442 let rest = self.rest_lengths[seg];
443 let ma = self.masses[seg];
444 let mb = self.masses[seg + 1];
445
446 let delta = pb - pa;
447 let dist = delta.length();
448 if dist < 1e-6 { continue; }
449
450 let error = dist - rest;
451 let dir = delta / dist;
452 let total_w = (1.0 / ma + 1.0 / mb).max(1e-6);
453 let correction = dir * error * self.stiffness / total_w;
454
455 if !ma.is_infinite() {
456 self.positions[seg] += correction / ma;
457 self.velocities[seg] += correction / ma / dt;
458 }
459 if !mb.is_infinite() {
460 self.positions[seg + 1] -= correction / mb;
461 self.velocities[seg + 1] -= correction / mb / dt;
462 }
463 }
464 }
465
466 if self.pin_head && n > 0 {
468 self.velocities[0] = glam::Vec3::ZERO;
470 }
471 }
472
473 pub fn apply_impulse(&mut self, index: usize, impulse: glam::Vec3) {
475 if index < self.velocities.len() && !self.masses[index].is_infinite() {
476 self.velocities[index] += impulse / self.masses[index];
477 }
478 }
479
480 pub fn apply_wind(&mut self, wind: glam::Vec3, dt: f32) {
482 for i in 0..self.velocities.len() {
483 if !self.masses[i].is_infinite() {
484 self.velocities[i] += wind * dt;
485 }
486 }
487 }
488
489 pub fn tip(&self) -> glam::Vec3 {
491 *self.positions.last().unwrap()
492 }
493
494 pub fn total_length(&self) -> f32 {
496 self.rest_lengths.iter().sum()
497 }
498
499 pub fn extension_ratio(&self) -> f32 {
501 let actual: f32 = self.positions.windows(2)
502 .map(|w| (w[1] - w[0]).length())
503 .sum();
504 let rest = self.total_length();
505 if rest < 1e-6 { 1.0 } else { actual / rest }
506 }
507}
508
509#[derive(Debug, Clone)]
513pub struct VerletPoint {
514 pub pos: glam::Vec3,
515 pub prev_pos: glam::Vec3,
516 pub pinned: bool,
517 pub mass: f32,
518}
519
520impl VerletPoint {
521 pub fn new(pos: glam::Vec3) -> Self {
522 Self { pos, prev_pos: pos, pinned: false, mass: 1.0 }
523 }
524
525 pub fn pinned(mut self) -> Self {
526 self.pinned = true;
527 self
528 }
529
530 pub fn with_mass(mut self, mass: f32) -> Self {
531 self.mass = mass;
532 self
533 }
534
535 pub fn integrate(&mut self, acceleration: glam::Vec3, dt: f32) {
537 if self.pinned { return; }
538 let vel = self.pos - self.prev_pos;
539 let next = self.pos + vel + acceleration * dt * dt;
540 self.prev_pos = self.pos;
541 self.pos = next;
542 }
543
544 pub fn velocity(&self, dt: f32) -> glam::Vec3 {
545 (self.pos - self.prev_pos) / dt.max(1e-6)
546 }
547}
548
549#[derive(Debug, Clone)]
556pub struct VerletCloth {
557 pub points: Vec<VerletPoint>,
558 pub cols: usize,
559 pub rows: usize,
560 pub rest_len: f32,
561 pub stiffness: f32,
562 pub iterations: usize,
563 pub gravity: glam::Vec3,
564 pub damping: f32,
565}
566
567impl VerletCloth {
568 pub fn new(origin: glam::Vec3, cols: usize, rows: usize, spacing: f32) -> Self {
570 let mut points = Vec::with_capacity(cols * rows);
571 for r in 0..rows {
572 for c in 0..cols {
573 let pos = origin + glam::Vec3::new(
574 c as f32 * spacing,
575 -(r as f32 * spacing),
576 0.0,
577 );
578 let mut pt = VerletPoint::new(pos);
579 if r == 0 { pt.pinned = true; }
581 points.push(pt);
582 }
583 }
584 Self {
585 points, cols, rows,
586 rest_len: spacing,
587 stiffness: 0.8,
588 iterations: 5,
589 gravity: glam::Vec3::NEG_Y * 9.8,
590 damping: 0.99,
591 }
592 }
593
594 fn idx(&self, r: usize, c: usize) -> usize { r * self.cols + c }
595
596 pub fn tick(&mut self, dt: f32) {
598 for pt in &mut self.points {
600 if pt.pinned { continue; }
601 let vel = (pt.pos - pt.prev_pos) * self.damping;
602 let next = pt.pos + vel + self.gravity * dt * dt;
603 pt.prev_pos = pt.pos;
604 pt.pos = next;
605 }
606
607 for _ in 0..self.iterations {
609 for r in 0..self.rows {
611 for c in 0..(self.cols - 1) {
612 let ia = self.idx(r, c);
613 let ib = self.idx(r, c + 1);
614 self.solve_constraint(ia, ib);
615 }
616 }
617 for r in 0..(self.rows - 1) {
619 for c in 0..self.cols {
620 let ia = self.idx(r, c);
621 let ib = self.idx(r + 1, c);
622 self.solve_constraint(ia, ib);
623 }
624 }
625 for r in 0..(self.rows - 1) {
627 for c in 0..(self.cols - 1) {
628 let ia = self.idx(r, c);
629 let ib = self.idx(r + 1, c + 1);
630 self.solve_constraint_len(ia, ib, self.rest_len * std::f32::consts::SQRT_2);
631 let ic = self.idx(r, c + 1);
632 let id = self.idx(r + 1, c);
633 self.solve_constraint_len(ic, id, self.rest_len * std::f32::consts::SQRT_2);
634 }
635 }
636 }
637 }
638
639 fn solve_constraint(&mut self, ia: usize, ib: usize) {
640 self.solve_constraint_len(ia, ib, self.rest_len);
641 }
642
643 fn solve_constraint_len(&mut self, ia: usize, ib: usize, rest: f32) {
644 let pa = self.points[ia].pos;
645 let pb = self.points[ib].pos;
646 let delta = pb - pa;
647 let dist = delta.length();
648 if dist < 1e-6 { return; }
649 let error = (dist - rest) / dist;
650 let correction = delta * error * self.stiffness * 0.5;
651 if !self.points[ia].pinned { self.points[ia].pos += correction; }
652 if !self.points[ib].pinned { self.points[ib].pos -= correction; }
653 }
654
655 pub fn apply_wind_gust(&mut self, center: glam::Vec3, strength: f32, radius: f32) {
657 for pt in &mut self.points {
658 if pt.pinned { continue; }
659 let d = pt.pos - center;
660 let dist = d.length();
661 if dist < radius && dist > 1e-6 {
662 let factor = (1.0 - dist / radius) * strength;
663 pt.pos += d / dist * factor;
664 }
665 }
666 }
667
668 pub fn tear(&mut self, center: glam::Vec3, radius: f32) {
670 for pt in &mut self.points {
671 let dist = (pt.pos - center).length();
672 if dist < radius {
673 pt.pinned = false;
674 let dir = (pt.pos - center).normalize_or_zero();
676 pt.pos += dir * 0.05;
677 }
678 }
679 }
680
681 pub fn point(&self, row: usize, col: usize) -> glam::Vec3 {
682 self.points[self.idx(row, col)].pos
683 }
684
685 pub fn pin(&mut self, row: usize, col: usize) {
686 let i = self.idx(row, col);
687 self.points[i].pinned = true;
688 }
689
690 pub fn unpin(&mut self, row: usize, col: usize) {
691 let i = self.idx(row, col);
692 self.points[i].pinned = false;
693 }
694}
695
696#[derive(Debug, Clone)]
703pub struct SpringNetwork {
704 pub positions: Vec<glam::Vec3>,
705 pub velocities: Vec<glam::Vec3>,
706 pub masses: Vec<f32>,
707 pub edges: Vec<(usize, usize, f32, f32)>,
709 pub gravity: glam::Vec3,
710 pub damping: f32,
711 pub iterations: usize,
712}
713
714impl SpringNetwork {
715 pub fn new() -> Self {
716 Self {
717 positions: Vec::new(),
718 velocities: Vec::new(),
719 masses: Vec::new(),
720 edges: Vec::new(),
721 gravity: glam::Vec3::NEG_Y * 9.8,
722 damping: 0.98,
723 iterations: 4,
724 }
725 }
726
727 pub fn add_node(&mut self, pos: glam::Vec3, mass: f32) -> usize {
728 let i = self.positions.len();
729 self.positions.push(pos);
730 self.velocities.push(glam::Vec3::ZERO);
731 self.masses.push(mass);
732 i
733 }
734
735 pub fn add_edge(&mut self, a: usize, b: usize, stiffness: f32) {
737 let rest = (self.positions[b] - self.positions[a]).length();
738 self.edges.push((a, b, rest, stiffness));
739 }
740
741 pub fn add_edge_with_length(&mut self, a: usize, b: usize, rest_length: f32, stiffness: f32) {
742 self.edges.push((a, b, rest_length, stiffness));
743 }
744
745 pub fn tick(&mut self, dt: f32) {
746 let n = self.positions.len();
747
748 for i in 0..n {
750 if self.masses[i].is_infinite() { continue; }
751 self.velocities[i] += self.gravity * dt;
752 self.velocities[i] *= self.damping;
753 self.positions[i] += self.velocities[i] * dt;
754 }
755
756 for _ in 0..self.iterations {
758 for &(a, b, rest, stiffness) in &self.edges {
759 let pa = self.positions[a];
760 let pb = self.positions[b];
761 let delta = pb - pa;
762 let dist = delta.length();
763 if dist < 1e-6 { continue; }
764 let error = dist - rest;
765 let dir = delta / dist;
766 let ma = self.masses[a];
767 let mb = self.masses[b];
768 let total_w = (1.0 / ma.min(1e6) + 1.0 / mb.min(1e6)).max(1e-6);
769 let correction = dir * error * stiffness / total_w;
770 if !ma.is_infinite() { self.positions[a] += correction / ma.min(1e6); }
771 if !mb.is_infinite() { self.positions[b] -= correction / mb.min(1e6); }
772 }
773 }
774 }
775
776 pub fn apply_impulse(&mut self, node: usize, impulse: glam::Vec3) {
777 if node < self.velocities.len() && !self.masses[node].is_infinite() {
778 self.velocities[node] += impulse / self.masses[node];
779 }
780 }
781
782 pub fn explode(&mut self, center: glam::Vec3, strength: f32, radius: f32) {
784 for i in 0..self.positions.len() {
785 if self.masses[i].is_infinite() { continue; }
786 let d = self.positions[i] - center;
787 let dist = d.length();
788 if dist < radius && dist > 1e-6 {
789 let factor = (1.0 - dist / radius) * strength;
790 self.velocities[i] += d / dist * factor / self.masses[i];
791 }
792 }
793 }
794
795 pub fn node_count(&self) -> usize { self.positions.len() }
796 pub fn edge_count(&self) -> usize { self.edges.len() }
797}
798
799impl Default for SpringNetwork {
800 fn default() -> Self { Self::new() }
801}
802
803#[derive(Debug, Clone)]
810pub struct CoupledOscillators {
811 pub phases: Vec<f32>,
813 pub frequencies: Vec<f32>,
815 pub coupling: f32,
817 pub edges: Vec<(usize, usize)>,
819}
820
821impl CoupledOscillators {
822 pub fn ring(n: usize, frequency: f32, coupling: f32) -> Self {
824 let phases: Vec<f32> = (0..n).map(|i| {
825 let base = i as f32 / n as f32 * std::f32::consts::TAU;
829 base + 0.1 * ((i as f32 + 1.0) * 1.618).sin()
830 }).collect();
831 let frequencies = vec![frequency; n];
832 let edges: Vec<(usize, usize)> = (0..n).map(|i| (i, (i + 1) % n)).collect();
833 Self { phases, frequencies, coupling, edges }
834 }
835
836 pub fn chain(n: usize, base_freq: f32, freq_spread: f32, coupling: f32) -> Self {
838 let phases: Vec<f32> = (0..n).map(|i| i as f32 * 0.3).collect();
839 let frequencies: Vec<f32> = (0..n).map(|i| {
840 let t = if n > 1 { i as f32 / (n - 1) as f32 } else { 0.0 };
841 base_freq + (t - 0.5) * freq_spread
842 }).collect();
843 let edges: Vec<(usize, usize)> = (0..(n - 1)).map(|i| (i, i + 1)).collect();
844 Self { phases, frequencies, coupling, edges }
845 }
846
847 pub fn tick(&mut self, dt: f32) {
849 let n = self.phases.len();
850 let mut dphi = vec![0.0_f32; n];
851
852 for i in 0..n {
853 dphi[i] += self.frequencies[i] * std::f32::consts::TAU;
854 }
855
856 for &(a, b) in &self.edges {
857 let diff = self.phases[b] - self.phases[a];
858 let coupling_term = self.coupling * diff.sin();
859 dphi[a] += coupling_term;
860 dphi[b] -= coupling_term;
861 }
862
863 for i in 0..n {
864 self.phases[i] += dphi[i] * dt;
865 self.phases[i] %= std::f32::consts::TAU;
867 if self.phases[i] < 0.0 { self.phases[i] += std::f32::consts::TAU; }
868 }
869 }
870
871 pub fn value(&self, i: usize) -> f32 {
873 self.phases.get(i).map(|&p| p.sin()).unwrap_or(0.0)
874 }
875
876 pub fn synchrony(&self) -> f32 {
878 if self.phases.is_empty() { return 0.0; }
879 let sx: f32 = self.phases.iter().map(|p| p.cos()).sum();
880 let sy: f32 = self.phases.iter().map(|p| p.sin()).sum();
881 let n = self.phases.len() as f32;
882 (sx * sx + sy * sy).sqrt() / n
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn spring_converges() {
892 let mut s = SpringDamper::critical(0.0, 5.0);
893 s.set_target(1.0);
894 for _ in 0..500 {
895 s.tick(0.016);
896 }
897 assert!((s.position - 1.0).abs() < 0.01, "spring did not converge: {}", s.position);
898 }
899
900 #[test]
901 fn underdamped_overshoots() {
902 let mut s = SpringDamper::bouncy(0.0, 8.0, 0.3);
903 s.set_target(1.0);
904 let mut max = 0.0f32;
905 for _ in 0..200 {
906 s.tick(0.016);
907 max = max.max(s.position);
908 }
909 assert!(max > 1.0, "underdamped spring should overshoot, max={}", max);
910 }
911
912 #[test]
913 fn constrained_spring_clamps_position() {
914 let mut cs = ConstrainedSpring::new(0.5, 5.0, 2.0)
915 .with_pos_limits(0.0, 1.0);
916 cs.set_target(2.0); for _ in 0..200 {
918 cs.tick(0.016);
919 }
920 assert!(cs.position() <= 1.001, "position should be clamped: {}", cs.position());
921 }
922
923 #[test]
924 fn spring_chain_falls() {
925 let mut chain = SpringChain::new(glam::Vec3::ZERO, 4, 0.5);
926 let initial_tip = chain.tip();
927 for _ in 0..60 {
928 chain.tick(0.016);
929 }
930 let new_tip = chain.tip();
931 assert!(new_tip.y < initial_tip.y, "chain tip should fall under gravity");
933 }
934
935 #[test]
936 fn spring_chain_anchor_stays_put() {
937 let anchor = glam::Vec3::new(0.0, 5.0, 0.0);
938 let mut chain = SpringChain::new(anchor, 4, 0.5);
939 for _ in 0..100 {
940 chain.tick(0.016);
941 }
942 let head = chain.positions[0];
943 assert!((head - anchor).length() < 0.001, "anchor should stay fixed");
944 }
945
946 #[test]
947 fn distance_constraint_rope_ignores_compression() {
948 let rope = DistanceConstraint::rope(1.0);
949 let (da, db) = rope.solve(
950 glam::Vec3::ZERO,
951 glam::Vec3::new(0.5, 0.0, 0.0), 1.0, 1.0,
953 );
954 assert!(da.length() < 1e-5, "rope should not correct compression");
956 assert!(db.length() < 1e-5, "rope should not correct compression");
957 }
958
959 #[test]
960 fn spring_network_explode() {
961 let mut net = SpringNetwork::new();
962 let a = net.add_node(glam::Vec3::new(-1.0, 0.0, 0.0), 1.0);
963 let b = net.add_node(glam::Vec3::new( 1.0, 0.0, 0.0), 1.0);
964 net.add_edge(a, b, 0.5);
965 let initial_dist = (net.positions[b] - net.positions[a]).length();
966 net.explode(glam::Vec3::ZERO, 10.0, 5.0);
967 net.tick(0.016);
968 let new_dist = (net.positions[b] - net.positions[a]).length();
969 assert!(new_dist > initial_dist, "explosion should push nodes apart");
970 }
971
972 #[test]
973 fn coupled_oscillators_ring_synchrony() {
974 let mut osc = CoupledOscillators::ring(6, 1.0, 2.0);
975 for _ in 0..1000 {
977 osc.tick(0.016);
978 }
979 let r = osc.synchrony();
980 assert!(r > 0.5, "ring oscillators should show some synchrony: r={:.3}", r);
981 }
982
983 #[test]
984 fn verlet_cloth_top_row_stays_pinned() {
985 let mut cloth = VerletCloth::new(glam::Vec3::ZERO, 4, 3, 0.5);
986 let initial_y = cloth.point(0, 0).y;
987 for _ in 0..100 {
988 cloth.tick(0.016);
989 }
990 let final_y = cloth.point(0, 0).y;
991 assert!((final_y - initial_y).abs() < 0.001, "pinned top row should not move");
992 }
993
994 #[test]
995 fn verlet_cloth_bottom_falls() {
996 let mut cloth = VerletCloth::new(glam::Vec3::ZERO, 2, 3, 0.5);
997 let init = cloth.point(2, 0).y;
998 for _ in 0..100 {
999 cloth.tick(0.016);
1000 }
1001 let after = cloth.point(2, 0).y;
1002 assert!(after < init, "bottom row should fall under gravity");
1003 }
1004}