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 pub mass: f32,
23}
24
25impl SpringDamper {
26 pub fn new(position: f32, stiffness: f32, damping: f32) -> Self {
27 Self { position, velocity: 0.0, target: position, stiffness, damping, mass: 1.0 }
28 }
29
30 pub fn with_mass(mut self, mass: f32) -> Self {
33 self.set_mass(mass);
34 self
35 }
36
37 pub fn set_mass(&mut self, mass: f32) {
39 self.mass = if mass.is_finite() { mass.max(0.01) } else { 1.0 };
40 }
41
42 pub fn critical(position: f32, speed: f32) -> Self {
44 let k = speed * speed;
45 let d = 2.0 * speed;
46 Self::new(position, k, d)
47 }
48
49 pub fn bouncy(position: f32, frequency: f32, damping_ratio: f32) -> Self {
51 let k = frequency * frequency;
52 let d = 2.0 * damping_ratio * frequency;
53 Self::new(position, k, d)
54 }
55
56 pub fn tick(&mut self, dt: f32) {
63 if !dt.is_finite() || dt <= 0.0 {
64 return;
65 }
66 let m = self.mass.max(0.01);
67 let omega = (self.stiffness.max(0.0) / m).sqrt();
70 let rate = (omega * dt / 0.5).max(self.damping / m * dt / 0.5);
71 let steps = (rate.ceil().max(1.0) as u32).min(32);
72 let h = dt / steps as f32;
73 for _ in 0..steps {
74 let force =
75 -self.stiffness * (self.position - self.target) - self.damping * self.velocity;
76 self.velocity += force / m * h;
78 self.position += self.velocity * h;
79 }
80 }
81
82 pub fn tick_get(&mut self, dt: f32) -> f32 {
84 self.tick(dt);
85 self.position
86 }
87
88 pub fn set_target(&mut self, target: f32) {
89 self.target = target;
90 }
91
92 pub fn teleport(&mut self, position: f32) {
93 self.position = position;
94 self.velocity = 0.0;
95 self.target = position;
96 }
97
98 pub fn is_settled(&self, threshold: f32) -> bool {
99 (self.position - self.target).abs() < threshold && self.velocity.abs() < threshold
100 }
101}
102
103#[derive(Debug, Clone)]
105pub struct Spring2D {
106 pub x: SpringDamper,
107 pub y: SpringDamper,
108}
109
110impl Spring2D {
111 pub fn new(px: f32, py: f32, stiffness: f32, damping: f32) -> Self {
112 Self {
113 x: SpringDamper::new(px, stiffness, damping),
114 y: SpringDamper::new(py, stiffness, damping),
115 }
116 }
117
118 pub fn critical(px: f32, py: f32, speed: f32) -> Self {
119 Self {
120 x: SpringDamper::critical(px, speed),
121 y: SpringDamper::critical(py, speed),
122 }
123 }
124
125 pub fn tick(&mut self, dt: f32) {
126 self.x.tick(dt);
127 self.y.tick(dt);
128 }
129
130 pub fn set_target(&mut self, tx: f32, ty: f32) {
131 self.x.set_target(tx);
132 self.y.set_target(ty);
133 }
134
135 pub fn position(&self) -> (f32, f32) {
136 (self.x.position, self.y.position)
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct Spring3D {
143 pub x: SpringDamper,
144 pub y: SpringDamper,
145 pub z: SpringDamper,
146}
147
148pub type SpringDamper3 = Spring3D;
150
151impl Spring3D {
152 pub fn new(px: f32, py: f32, pz: f32, stiffness: f32, damping: f32) -> Self {
154 Self {
155 x: SpringDamper::new(px, stiffness, damping),
156 y: SpringDamper::new(py, stiffness, damping),
157 z: SpringDamper::new(pz, stiffness, damping),
158 }
159 }
160
161 pub fn from_vec3(pos: glam::Vec3, stiffness: f32, damping: f32) -> Self {
163 Self::new(pos.x, pos.y, pos.z, stiffness, damping)
164 }
165
166 pub fn critical(px: f32, py: f32, pz: f32, speed: f32) -> Self {
167 Self {
168 x: SpringDamper::critical(px, speed),
169 y: SpringDamper::critical(py, speed),
170 z: SpringDamper::critical(pz, speed),
171 }
172 }
173
174 pub fn set_mass(&mut self, mass: f32) {
176 self.x.set_mass(mass);
177 self.y.set_mass(mass);
178 self.z.set_mass(mass);
179 }
180
181 pub fn mass(&self) -> f32 {
183 self.x.mass
184 }
185
186 pub fn tick(&mut self, dt: f32) -> glam::Vec3 {
188 self.x.tick(dt);
189 self.y.tick(dt);
190 self.z.tick(dt);
191 self.position()
192 }
193
194 pub fn set_target(&mut self, t: glam::Vec3) {
196 self.x.set_target(t.x);
197 self.y.set_target(t.y);
198 self.z.set_target(t.z);
199 }
200
201 pub fn set_target_xyz(&mut self, tx: f32, ty: f32, tz: f32) {
202 self.x.set_target(tx);
203 self.y.set_target(ty);
204 self.z.set_target(tz);
205 }
206
207 pub fn position(&self) -> glam::Vec3 {
208 glam::Vec3::new(self.x.position, self.y.position, self.z.position)
209 }
210}
211
212#[derive(Debug, Clone)]
216pub struct ConstrainedSpring {
217 pub inner: SpringDamper,
218 pub min_pos: Option<f32>,
219 pub max_pos: Option<f32>,
220 pub min_vel: Option<f32>,
221 pub max_vel: Option<f32>,
222}
223
224impl ConstrainedSpring {
225 pub fn new(position: f32, stiffness: f32, damping: f32) -> Self {
226 Self {
227 inner: SpringDamper::new(position, stiffness, damping),
228 min_pos: None, max_pos: None,
229 min_vel: None, max_vel: None,
230 }
231 }
232
233 pub fn with_pos_limits(mut self, min: f32, max: f32) -> Self {
234 self.min_pos = Some(min);
235 self.max_pos = Some(max);
236 self
237 }
238
239 pub fn with_vel_limits(mut self, min: f32, max: f32) -> Self {
240 self.min_vel = Some(min);
241 self.max_vel = Some(max);
242 self
243 }
244
245 pub fn tick(&mut self, dt: f32) -> f32 {
246 self.inner.tick(dt);
247 if let Some(lo) = self.min_pos {
248 if self.inner.position < lo {
249 self.inner.position = lo;
250 self.inner.velocity = self.inner.velocity.max(0.0);
251 }
252 }
253 if let Some(hi) = self.max_pos {
254 if self.inner.position > hi {
255 self.inner.position = hi;
256 self.inner.velocity = self.inner.velocity.min(0.0);
257 }
258 }
259 if let Some(lo) = self.min_vel {
260 self.inner.velocity = self.inner.velocity.max(lo);
261 }
262 if let Some(hi) = self.max_vel {
263 self.inner.velocity = self.inner.velocity.min(hi);
264 }
265 self.inner.position
266 }
267
268 pub fn set_target(&mut self, t: f32) { self.inner.set_target(t); }
269 pub fn position(&self) -> f32 { self.inner.position }
270 pub fn velocity(&self) -> f32 { self.inner.velocity }
271 pub fn is_settled(&self, threshold: f32) -> bool { self.inner.is_settled(threshold) }
272}
273
274#[derive(Debug, Clone)]
280pub struct DistanceConstraint {
281 pub rest_length: f32,
283 pub stiffness: f32,
285 pub damping: f32,
287 pub allow_compression: bool,
289 pub allow_extension: bool,
291}
292
293impl DistanceConstraint {
294 pub fn new(rest_length: f32, stiffness: f32) -> Self {
295 Self {
296 rest_length,
297 stiffness,
298 damping: 0.3,
299 allow_compression: true,
300 allow_extension: true,
301 }
302 }
303
304 pub fn rod(rest_length: f32) -> Self {
306 Self { rest_length, stiffness: 1.0, damping: 0.5, allow_compression: true, allow_extension: true }
307 }
308
309 pub fn rope(rest_length: f32) -> Self {
311 Self { rest_length, stiffness: 0.9, damping: 0.4, allow_compression: false, allow_extension: true }
312 }
313
314 pub fn strut(rest_length: f32) -> Self {
316 Self { rest_length, stiffness: 0.9, damping: 0.4, allow_compression: true, allow_extension: false }
317 }
318
319 pub fn solve(
325 &self,
326 pa: glam::Vec3, pb: glam::Vec3,
327 mass_a: f32, mass_b: f32,
328 ) -> (glam::Vec3, glam::Vec3) {
329 let delta = pb - pa;
330 let dist = delta.length();
331 if dist < 1e-6 { return (glam::Vec3::ZERO, glam::Vec3::ZERO); }
332
333 let error = dist - self.rest_length;
334 let stretch = error > 0.0;
335 let compress = error < 0.0;
336
337 if stretch && !self.allow_extension { return (glam::Vec3::ZERO, glam::Vec3::ZERO); }
339 if compress && !self.allow_compression { return (glam::Vec3::ZERO, glam::Vec3::ZERO); }
340
341 let dir = delta / dist;
342 let total_mass = (mass_a + mass_b).max(1e-6);
343 let ratio_a = mass_b / total_mass;
344 let ratio_b = mass_a / total_mass;
345 let correction = dir * error * self.stiffness;
346
347 (correction * ratio_a, -correction * ratio_b)
348 }
349}
350
351#[derive(Debug, Clone)]
357pub struct PinConstraint {
358 pub anchor: glam::Vec3,
359 pub stiffness: f32,
361 pub dead_zone: f32,
363}
364
365impl PinConstraint {
366 pub fn new(anchor: glam::Vec3) -> Self {
367 Self { anchor, stiffness: 1.0, dead_zone: 0.0 }
368 }
369
370 pub fn soft(anchor: glam::Vec3, stiffness: f32) -> Self {
371 Self { anchor, stiffness, dead_zone: 0.0 }
372 }
373
374 pub fn with_dead_zone(mut self, zone: f32) -> Self {
375 self.dead_zone = zone;
376 self
377 }
378
379 pub fn solve(&self, pos: glam::Vec3) -> glam::Vec3 {
381 let delta = self.anchor - pos;
382 let dist = delta.length();
383 if dist <= self.dead_zone { return glam::Vec3::ZERO; }
384 let excess = dist - self.dead_zone;
385 let dir = delta / dist;
386 dir * excess * self.stiffness
387 }
388
389 pub fn move_anchor(&mut self, new_anchor: glam::Vec3) {
390 self.anchor = new_anchor;
391 }
392}
393
394#[derive(Debug, Clone)]
401pub struct SpringChain {
402 pub positions: Vec<glam::Vec3>,
404 pub velocities: Vec<glam::Vec3>,
406 pub rest_lengths: Vec<f32>,
408 pub stiffness: f32,
410 pub damping: f32,
412 pub masses: Vec<f32>,
414 pub pin_head: bool,
416 pub gravity: glam::Vec3,
418 pub iterations: usize,
420}
421
422impl SpringChain {
423 pub fn new(anchor: glam::Vec3, count: usize, segment_length: f32) -> Self {
428 let count = count.max(2);
429 let positions: Vec<glam::Vec3> = (0..count)
430 .map(|i| anchor + glam::Vec3::NEG_Y * (i as f32 * segment_length))
431 .collect();
432 let velocities = vec![glam::Vec3::ZERO; count];
433 let rest_lengths = vec![segment_length; count - 1];
434 let mut masses = vec![1.0_f32; count];
435 masses[0] = f32::INFINITY; Self {
437 positions, velocities, rest_lengths,
438 stiffness: 0.8, damping: 0.98,
439 masses, pin_head: true,
440 gravity: glam::Vec3::NEG_Y * 9.8,
441 iterations: 4,
442 }
443 }
444
445 pub fn horizontal(anchor: glam::Vec3, count: usize, segment_length: f32) -> Self {
447 let count = count.max(2);
448 let positions: Vec<glam::Vec3> = (0..count)
449 .map(|i| anchor + glam::Vec3::X * (i as f32 * segment_length))
450 .collect();
451 let velocities = vec![glam::Vec3::ZERO; count];
452 let rest_lengths = vec![segment_length; count - 1];
453 let mut masses = vec![1.0_f32; count];
454 masses[0] = f32::INFINITY;
455 Self {
456 positions, velocities, rest_lengths,
457 stiffness: 0.7, damping: 0.97,
458 masses, pin_head: true,
459 gravity: glam::Vec3::NEG_Y * 9.8,
460 iterations: 5,
461 }
462 }
463
464 pub fn set_anchor(&mut self, pos: glam::Vec3) {
466 self.positions[0] = pos;
467 }
468
469 pub fn tick(&mut self, dt: f32) {
473 let n = self.positions.len();
474
475 for i in 0..n {
477 if self.masses[i].is_infinite() { continue; }
478 self.velocities[i] += self.gravity * dt;
479 self.velocities[i] *= self.damping;
480 self.positions[i] += self.velocities[i] * dt;
481 }
482
483 for _ in 0..self.iterations {
485 for seg in 0..(n - 1) {
486 let pa = self.positions[seg];
487 let pb = self.positions[seg + 1];
488 let rest = self.rest_lengths[seg];
489 let ma = self.masses[seg];
490 let mb = self.masses[seg + 1];
491
492 let delta = pb - pa;
493 let dist = delta.length();
494 if dist < 1e-6 { continue; }
495
496 let error = dist - rest;
497 let dir = delta / dist;
498 let total_w = (1.0 / ma + 1.0 / mb).max(1e-6);
499 let correction = dir * error * self.stiffness / total_w;
500
501 if !ma.is_infinite() {
502 self.positions[seg] += correction / ma;
503 self.velocities[seg] += correction / ma / dt;
504 }
505 if !mb.is_infinite() {
506 self.positions[seg + 1] -= correction / mb;
507 self.velocities[seg + 1] -= correction / mb / dt;
508 }
509 }
510 }
511
512 if self.pin_head && n > 0 {
514 self.velocities[0] = glam::Vec3::ZERO;
516 }
517 }
518
519 pub fn apply_impulse(&mut self, index: usize, impulse: glam::Vec3) {
521 if index < self.velocities.len() && !self.masses[index].is_infinite() {
522 self.velocities[index] += impulse / self.masses[index];
523 }
524 }
525
526 pub fn apply_wind(&mut self, wind: glam::Vec3, dt: f32) {
528 for i in 0..self.velocities.len() {
529 if !self.masses[i].is_infinite() {
530 self.velocities[i] += wind * dt;
531 }
532 }
533 }
534
535 pub fn tip(&self) -> glam::Vec3 {
537 *self.positions.last().unwrap()
538 }
539
540 pub fn total_length(&self) -> f32 {
542 self.rest_lengths.iter().sum()
543 }
544
545 pub fn extension_ratio(&self) -> f32 {
547 let actual: f32 = self.positions.windows(2)
548 .map(|w| (w[1] - w[0]).length())
549 .sum();
550 let rest = self.total_length();
551 if rest < 1e-6 { 1.0 } else { actual / rest }
552 }
553}
554
555#[derive(Debug, Clone)]
559pub struct VerletPoint {
560 pub pos: glam::Vec3,
561 pub prev_pos: glam::Vec3,
562 pub pinned: bool,
563 pub mass: f32,
564}
565
566impl VerletPoint {
567 pub fn new(pos: glam::Vec3) -> Self {
568 Self { pos, prev_pos: pos, pinned: false, mass: 1.0 }
569 }
570
571 pub fn pinned(mut self) -> Self {
572 self.pinned = true;
573 self
574 }
575
576 pub fn with_mass(mut self, mass: f32) -> Self {
577 self.mass = mass;
578 self
579 }
580
581 pub fn integrate(&mut self, acceleration: glam::Vec3, dt: f32) {
583 if self.pinned { return; }
584 let vel = self.pos - self.prev_pos;
585 let next = self.pos + vel + acceleration * dt * dt;
586 self.prev_pos = self.pos;
587 self.pos = next;
588 }
589
590 pub fn velocity(&self, dt: f32) -> glam::Vec3 {
591 (self.pos - self.prev_pos) / dt.max(1e-6)
592 }
593}
594
595#[derive(Debug, Clone)]
602pub struct VerletCloth {
603 pub points: Vec<VerletPoint>,
604 pub cols: usize,
605 pub rows: usize,
606 pub rest_len: f32,
607 pub stiffness: f32,
608 pub iterations: usize,
609 pub gravity: glam::Vec3,
610 pub damping: f32,
611}
612
613impl VerletCloth {
614 pub fn new(origin: glam::Vec3, cols: usize, rows: usize, spacing: f32) -> Self {
616 let mut points = Vec::with_capacity(cols * rows);
617 for r in 0..rows {
618 for c in 0..cols {
619 let pos = origin + glam::Vec3::new(
620 c as f32 * spacing,
621 -(r as f32 * spacing),
622 0.0,
623 );
624 let mut pt = VerletPoint::new(pos);
625 if r == 0 { pt.pinned = true; }
627 points.push(pt);
628 }
629 }
630 Self {
631 points, cols, rows,
632 rest_len: spacing,
633 stiffness: 0.8,
634 iterations: 5,
635 gravity: glam::Vec3::NEG_Y * 9.8,
636 damping: 0.99,
637 }
638 }
639
640 fn idx(&self, r: usize, c: usize) -> usize { r * self.cols + c }
641
642 pub fn tick(&mut self, dt: f32) {
644 for pt in &mut self.points {
646 if pt.pinned { continue; }
647 let vel = (pt.pos - pt.prev_pos) * self.damping;
648 let next = pt.pos + vel + self.gravity * dt * dt;
649 pt.prev_pos = pt.pos;
650 pt.pos = next;
651 }
652
653 for _ in 0..self.iterations {
655 for r in 0..self.rows {
657 for c in 0..(self.cols - 1) {
658 let ia = self.idx(r, c);
659 let ib = self.idx(r, c + 1);
660 self.solve_constraint(ia, ib);
661 }
662 }
663 for r in 0..(self.rows - 1) {
665 for c in 0..self.cols {
666 let ia = self.idx(r, c);
667 let ib = self.idx(r + 1, c);
668 self.solve_constraint(ia, ib);
669 }
670 }
671 for r in 0..(self.rows - 1) {
673 for c in 0..(self.cols - 1) {
674 let ia = self.idx(r, c);
675 let ib = self.idx(r + 1, c + 1);
676 self.solve_constraint_len(ia, ib, self.rest_len * std::f32::consts::SQRT_2);
677 let ic = self.idx(r, c + 1);
678 let id = self.idx(r + 1, c);
679 self.solve_constraint_len(ic, id, self.rest_len * std::f32::consts::SQRT_2);
680 }
681 }
682 }
683 }
684
685 fn solve_constraint(&mut self, ia: usize, ib: usize) {
686 self.solve_constraint_len(ia, ib, self.rest_len);
687 }
688
689 fn solve_constraint_len(&mut self, ia: usize, ib: usize, rest: f32) {
690 let pa = self.points[ia].pos;
691 let pb = self.points[ib].pos;
692 let delta = pb - pa;
693 let dist = delta.length();
694 if dist < 1e-6 { return; }
695 let error = (dist - rest) / dist;
696 let correction = delta * error * self.stiffness * 0.5;
697 if !self.points[ia].pinned { self.points[ia].pos += correction; }
698 if !self.points[ib].pinned { self.points[ib].pos -= correction; }
699 }
700
701 pub fn apply_wind_gust(&mut self, center: glam::Vec3, strength: f32, radius: f32) {
703 for pt in &mut self.points {
704 if pt.pinned { continue; }
705 let d = pt.pos - center;
706 let dist = d.length();
707 if dist < radius && dist > 1e-6 {
708 let factor = (1.0 - dist / radius) * strength;
709 pt.pos += d / dist * factor;
710 }
711 }
712 }
713
714 pub fn tear(&mut self, center: glam::Vec3, radius: f32) {
716 for pt in &mut self.points {
717 let dist = (pt.pos - center).length();
718 if dist < radius {
719 pt.pinned = false;
720 let dir = (pt.pos - center).normalize_or_zero();
722 pt.pos += dir * 0.05;
723 }
724 }
725 }
726
727 pub fn point(&self, row: usize, col: usize) -> glam::Vec3 {
728 self.points[self.idx(row, col)].pos
729 }
730
731 pub fn pin(&mut self, row: usize, col: usize) {
732 let i = self.idx(row, col);
733 self.points[i].pinned = true;
734 }
735
736 pub fn unpin(&mut self, row: usize, col: usize) {
737 let i = self.idx(row, col);
738 self.points[i].pinned = false;
739 }
740}
741
742#[derive(Debug, Clone)]
749pub struct SpringNetwork {
750 pub positions: Vec<glam::Vec3>,
751 pub velocities: Vec<glam::Vec3>,
752 pub masses: Vec<f32>,
753 pub edges: Vec<(usize, usize, f32, f32)>,
755 pub gravity: glam::Vec3,
756 pub damping: f32,
757 pub iterations: usize,
758}
759
760impl SpringNetwork {
761 pub fn new() -> Self {
762 Self {
763 positions: Vec::new(),
764 velocities: Vec::new(),
765 masses: Vec::new(),
766 edges: Vec::new(),
767 gravity: glam::Vec3::NEG_Y * 9.8,
768 damping: 0.98,
769 iterations: 4,
770 }
771 }
772
773 pub fn add_node(&mut self, pos: glam::Vec3, mass: f32) -> usize {
774 let i = self.positions.len();
775 self.positions.push(pos);
776 self.velocities.push(glam::Vec3::ZERO);
777 self.masses.push(mass);
778 i
779 }
780
781 pub fn add_edge(&mut self, a: usize, b: usize, stiffness: f32) {
783 let rest = (self.positions[b] - self.positions[a]).length();
784 self.edges.push((a, b, rest, stiffness));
785 }
786
787 pub fn add_edge_with_length(&mut self, a: usize, b: usize, rest_length: f32, stiffness: f32) {
788 self.edges.push((a, b, rest_length, stiffness));
789 }
790
791 pub fn tick(&mut self, dt: f32) {
792 let n = self.positions.len();
793
794 for i in 0..n {
796 if self.masses[i].is_infinite() { continue; }
797 self.velocities[i] += self.gravity * dt;
798 self.velocities[i] *= self.damping;
799 self.positions[i] += self.velocities[i] * dt;
800 }
801
802 for _ in 0..self.iterations {
804 for &(a, b, rest, stiffness) in &self.edges {
805 let pa = self.positions[a];
806 let pb = self.positions[b];
807 let delta = pb - pa;
808 let dist = delta.length();
809 if dist < 1e-6 { continue; }
810 let error = dist - rest;
811 let dir = delta / dist;
812 let ma = self.masses[a];
813 let mb = self.masses[b];
814 let total_w = (1.0 / ma.min(1e6) + 1.0 / mb.min(1e6)).max(1e-6);
815 let correction = dir * error * stiffness / total_w;
816 if !ma.is_infinite() { self.positions[a] += correction / ma.min(1e6); }
817 if !mb.is_infinite() { self.positions[b] -= correction / mb.min(1e6); }
818 }
819 }
820 }
821
822 pub fn apply_impulse(&mut self, node: usize, impulse: glam::Vec3) {
823 if node < self.velocities.len() && !self.masses[node].is_infinite() {
824 self.velocities[node] += impulse / self.masses[node];
825 }
826 }
827
828 pub fn explode(&mut self, center: glam::Vec3, strength: f32, radius: f32) {
830 for i in 0..self.positions.len() {
831 if self.masses[i].is_infinite() { continue; }
832 let d = self.positions[i] - center;
833 let dist = d.length();
834 if dist < radius && dist > 1e-6 {
835 let factor = (1.0 - dist / radius) * strength;
836 self.velocities[i] += d / dist * factor / self.masses[i];
837 }
838 }
839 }
840
841 pub fn node_count(&self) -> usize { self.positions.len() }
842 pub fn edge_count(&self) -> usize { self.edges.len() }
843}
844
845impl Default for SpringNetwork {
846 fn default() -> Self { Self::new() }
847}
848
849#[derive(Debug, Clone)]
856pub struct CoupledOscillators {
857 pub phases: Vec<f32>,
859 pub frequencies: Vec<f32>,
861 pub coupling: f32,
863 pub edges: Vec<(usize, usize)>,
865}
866
867impl CoupledOscillators {
868 pub fn ring(n: usize, frequency: f32, coupling: f32) -> Self {
870 let phases: Vec<f32> = (0..n).map(|i| {
871 let base = i as f32 / n as f32 * std::f32::consts::TAU;
875 base + 0.1 * ((i as f32 + 1.0) * 1.618).sin()
876 }).collect();
877 let frequencies = vec![frequency; n];
878 let edges: Vec<(usize, usize)> = (0..n).map(|i| (i, (i + 1) % n)).collect();
879 Self { phases, frequencies, coupling, edges }
880 }
881
882 pub fn chain(n: usize, base_freq: f32, freq_spread: f32, coupling: f32) -> Self {
884 let phases: Vec<f32> = (0..n).map(|i| i as f32 * 0.3).collect();
885 let frequencies: Vec<f32> = (0..n).map(|i| {
886 let t = if n > 1 { i as f32 / (n - 1) as f32 } else { 0.0 };
887 base_freq + (t - 0.5) * freq_spread
888 }).collect();
889 let edges: Vec<(usize, usize)> = (0..(n - 1)).map(|i| (i, i + 1)).collect();
890 Self { phases, frequencies, coupling, edges }
891 }
892
893 pub fn tick(&mut self, dt: f32) {
895 let n = self.phases.len();
896 let mut dphi = vec![0.0_f32; n];
897
898 for i in 0..n {
899 dphi[i] += self.frequencies[i] * std::f32::consts::TAU;
900 }
901
902 for &(a, b) in &self.edges {
903 let diff = self.phases[b] - self.phases[a];
904 let coupling_term = self.coupling * diff.sin();
905 dphi[a] += coupling_term;
906 dphi[b] -= coupling_term;
907 }
908
909 for i in 0..n {
910 self.phases[i] += dphi[i] * dt;
911 self.phases[i] %= std::f32::consts::TAU;
913 if self.phases[i] < 0.0 { self.phases[i] += std::f32::consts::TAU; }
914 }
915 }
916
917 pub fn value(&self, i: usize) -> f32 {
919 self.phases.get(i).map(|&p| p.sin()).unwrap_or(0.0)
920 }
921
922 pub fn synchrony(&self) -> f32 {
924 if self.phases.is_empty() { return 0.0; }
925 let sx: f32 = self.phases.iter().map(|p| p.cos()).sum();
926 let sy: f32 = self.phases.iter().map(|p| p.sin()).sum();
927 let n = self.phases.len() as f32;
928 (sx * sx + sy * sy).sqrt() / n
929 }
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935
936 #[test]
937 fn spring_converges() {
938 let mut s = SpringDamper::critical(0.0, 5.0);
939 s.set_target(1.0);
940 for _ in 0..500 {
941 s.tick(0.016);
942 }
943 assert!((s.position - 1.0).abs() < 0.01, "spring did not converge: {}", s.position);
944 }
945
946 #[test]
947 fn underdamped_overshoots() {
948 let mut s = SpringDamper::bouncy(0.0, 8.0, 0.3);
949 s.set_target(1.0);
950 let mut max = 0.0f32;
951 for _ in 0..200 {
952 s.tick(0.016);
953 max = max.max(s.position);
954 }
955 assert!(max > 1.0, "underdamped spring should overshoot, max={}", max);
956 }
957
958 #[test]
959 fn constrained_spring_clamps_position() {
960 let mut cs = ConstrainedSpring::new(0.5, 5.0, 2.0)
961 .with_pos_limits(0.0, 1.0);
962 cs.set_target(2.0); for _ in 0..200 {
964 cs.tick(0.016);
965 }
966 assert!(cs.position() <= 1.001, "position should be clamped: {}", cs.position());
967 }
968
969 #[test]
970 fn spring_chain_falls() {
971 let mut chain = SpringChain::new(glam::Vec3::ZERO, 4, 0.5);
972 let initial_tip = chain.tip();
973 for _ in 0..60 {
974 chain.tick(0.016);
975 }
976 let new_tip = chain.tip();
977 assert!(new_tip.y < initial_tip.y, "chain tip should fall under gravity");
979 }
980
981 #[test]
982 fn spring_chain_anchor_stays_put() {
983 let anchor = glam::Vec3::new(0.0, 5.0, 0.0);
984 let mut chain = SpringChain::new(anchor, 4, 0.5);
985 for _ in 0..100 {
986 chain.tick(0.016);
987 }
988 let head = chain.positions[0];
989 assert!((head - anchor).length() < 0.001, "anchor should stay fixed");
990 }
991
992 #[test]
993 fn distance_constraint_rope_ignores_compression() {
994 let rope = DistanceConstraint::rope(1.0);
995 let (da, db) = rope.solve(
996 glam::Vec3::ZERO,
997 glam::Vec3::new(0.5, 0.0, 0.0), 1.0, 1.0,
999 );
1000 assert!(da.length() < 1e-5, "rope should not correct compression");
1002 assert!(db.length() < 1e-5, "rope should not correct compression");
1003 }
1004
1005 #[test]
1006 fn spring_network_explode() {
1007 let mut net = SpringNetwork::new();
1008 let a = net.add_node(glam::Vec3::new(-1.0, 0.0, 0.0), 1.0);
1009 let b = net.add_node(glam::Vec3::new( 1.0, 0.0, 0.0), 1.0);
1010 net.add_edge(a, b, 0.5);
1011 let initial_dist = (net.positions[b] - net.positions[a]).length();
1012 net.explode(glam::Vec3::ZERO, 10.0, 5.0);
1013 net.tick(0.016);
1014 let new_dist = (net.positions[b] - net.positions[a]).length();
1015 assert!(new_dist > initial_dist, "explosion should push nodes apart");
1016 }
1017
1018 #[test]
1019 fn coupled_oscillators_ring_synchrony() {
1020 let mut osc = CoupledOscillators::ring(6, 1.0, 2.0);
1021 for _ in 0..1000 {
1023 osc.tick(0.016);
1024 }
1025 let r = osc.synchrony();
1026 assert!(r > 0.5, "ring oscillators should show some synchrony: r={:.3}", r);
1027 }
1028
1029 #[test]
1030 fn verlet_cloth_top_row_stays_pinned() {
1031 let mut cloth = VerletCloth::new(glam::Vec3::ZERO, 4, 3, 0.5);
1032 let initial_y = cloth.point(0, 0).y;
1033 for _ in 0..100 {
1034 cloth.tick(0.016);
1035 }
1036 let final_y = cloth.point(0, 0).y;
1037 assert!((final_y - initial_y).abs() < 0.001, "pinned top row should not move");
1038 }
1039
1040 #[test]
1041 fn verlet_cloth_bottom_falls() {
1042 let mut cloth = VerletCloth::new(glam::Vec3::ZERO, 2, 3, 0.5);
1043 let init = cloth.point(2, 0).y;
1044 for _ in 0..100 {
1045 cloth.tick(0.016);
1046 }
1047 let after = cloth.point(2, 0).y;
1048 assert!(after < init, "bottom row should fall under gravity");
1049 }
1050}