1#[allow(dead_code)]
4pub struct SecondaryBone {
5 pub id: u32,
6 pub name: String,
7 pub position: [f32; 3],
8 pub velocity: [f32; 3],
9 pub target_position: [f32; 3],
10 pub stiffness: f32,
11 pub damping: f32,
12 pub mass: f32,
13}
14
15#[allow(dead_code)]
16pub struct SecondaryChain {
17 pub name: String,
18 pub bones: Vec<SecondaryBone>,
19 pub gravity: [f32; 3],
20 pub wind: [f32; 3],
21}
22
23#[allow(dead_code)]
24pub struct SecondaryMotionConfig {
25 pub gravity: [f32; 3],
26 pub default_stiffness: f32,
27 pub default_damping: f32,
28 pub default_mass: f32,
29}
30
31#[allow(dead_code)]
32pub fn default_secondary_config() -> SecondaryMotionConfig {
33 SecondaryMotionConfig {
34 gravity: [0.0, -9.81, 0.0],
35 default_stiffness: 50.0,
36 default_damping: 5.0,
37 default_mass: 1.0,
38 }
39}
40
41#[allow(dead_code)]
42pub fn new_secondary_bone(
43 id: u32,
44 name: &str,
45 pos: [f32; 3],
46 stiffness: f32,
47 damping: f32,
48) -> SecondaryBone {
49 SecondaryBone {
50 id,
51 name: name.to_string(),
52 position: pos,
53 velocity: [0.0, 0.0, 0.0],
54 target_position: pos,
55 stiffness,
56 damping,
57 mass: 1.0,
58 }
59}
60
61#[allow(dead_code)]
62pub fn new_secondary_chain(name: &str, gravity: [f32; 3]) -> SecondaryChain {
63 SecondaryChain {
64 name: name.to_string(),
65 bones: Vec::new(),
66 gravity,
67 wind: [0.0, 0.0, 0.0],
68 }
69}
70
71#[allow(dead_code)]
72pub fn add_secondary_bone(chain: &mut SecondaryChain, bone: SecondaryBone) {
73 chain.bones.push(bone);
74}
75
76#[allow(dead_code)]
77pub fn update_secondary_bone(bone: &mut SecondaryBone, dt: f32, external_force: [f32; 3]) {
78 let spring = [
80 bone.stiffness * (bone.target_position[0] - bone.position[0]),
81 bone.stiffness * (bone.target_position[1] - bone.position[1]),
82 bone.stiffness * (bone.target_position[2] - bone.position[2]),
83 ];
84 let damp = [
85 bone.damping * bone.velocity[0],
86 bone.damping * bone.velocity[1],
87 bone.damping * bone.velocity[2],
88 ];
89 let force = [
90 spring[0] - damp[0] + external_force[0],
91 spring[1] - damp[1] + external_force[1],
92 spring[2] - damp[2] + external_force[2],
93 ];
94 let inv_mass = if bone.mass > 0.0 {
95 1.0 / bone.mass
96 } else {
97 1.0
98 };
99 bone.velocity[0] += force[0] * inv_mass * dt;
100 bone.velocity[1] += force[1] * inv_mass * dt;
101 bone.velocity[2] += force[2] * inv_mass * dt;
102 bone.position[0] += bone.velocity[0] * dt;
103 bone.position[1] += bone.velocity[1] * dt;
104 bone.position[2] += bone.velocity[2] * dt;
105}
106
107#[allow(dead_code)]
108pub fn update_secondary_chain(chain: &mut SecondaryChain, dt: f32, target_positions: &[[f32; 3]]) {
109 let gravity = chain.gravity;
110 let wind = chain.wind;
111 let external = [
112 gravity[0] + wind[0],
113 gravity[1] + wind[1],
114 gravity[2] + wind[2],
115 ];
116 for (i, bone) in chain.bones.iter_mut().enumerate() {
117 if i < target_positions.len() {
118 bone.target_position = target_positions[i];
119 }
120 update_secondary_bone(bone, dt, external);
121 }
122}
123
124#[allow(dead_code)]
125pub fn set_chain_wind(chain: &mut SecondaryChain, wind: [f32; 3]) {
126 chain.wind = wind;
127}
128
129#[allow(dead_code)]
130pub fn secondary_bone_count(chain: &SecondaryChain) -> usize {
131 chain.bones.len()
132}
133
134#[allow(dead_code)]
135pub fn chain_kinetic_energy(chain: &SecondaryChain) -> f32 {
136 chain.bones.iter().fold(0.0_f32, |acc, bone| {
137 let v2 = bone.velocity[0] * bone.velocity[0]
138 + bone.velocity[1] * bone.velocity[1]
139 + bone.velocity[2] * bone.velocity[2];
140 acc + 0.5 * bone.mass * v2
141 })
142}
143
144#[allow(dead_code)]
145pub fn reset_secondary_chain(chain: &mut SecondaryChain) {
146 for bone in chain.bones.iter_mut() {
147 bone.velocity = [0.0, 0.0, 0.0];
148 }
149}
150
151#[allow(dead_code)]
152pub fn secondary_chain_positions(chain: &SecondaryChain) -> Vec<[f32; 3]> {
153 chain.bones.iter().map(|b| b.position).collect()
154}
155
156#[allow(dead_code)]
157pub fn secondary_bone_lag(bone: &SecondaryBone) -> f32 {
158 let dx = bone.position[0] - bone.target_position[0];
159 let dy = bone.position[1] - bone.target_position[1];
160 let dz = bone.position[2] - bone.target_position[2];
161 (dx * dx + dy * dy + dz * dz).sqrt()
162}
163
164#[allow(dead_code)]
165pub fn blend_secondary_to_target(chain: &mut SecondaryChain, alpha: f32) {
166 let a = alpha.clamp(0.0, 1.0);
167 for bone in chain.bones.iter_mut() {
168 bone.position[0] = bone.position[0] + a * (bone.target_position[0] - bone.position[0]);
169 bone.position[1] = bone.position[1] + a * (bone.target_position[1] - bone.position[1]);
170 bone.position[2] = bone.position[2] + a * (bone.target_position[2] - bone.position[2]);
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_new_secondary_bone_fields() {
180 let bone = new_secondary_bone(1, "hair_tip", [1.0, 2.0, 3.0], 40.0, 4.0);
181 assert_eq!(bone.id, 1);
182 assert_eq!(bone.name, "hair_tip");
183 assert_eq!(bone.position, [1.0, 2.0, 3.0]);
184 assert_eq!(bone.velocity, [0.0, 0.0, 0.0]);
185 assert_eq!(bone.stiffness, 40.0);
186 assert_eq!(bone.damping, 4.0);
187 }
188
189 #[test]
190 fn test_new_secondary_chain_empty() {
191 let chain = new_secondary_chain("hair_chain", [0.0, -9.81, 0.0]);
192 assert_eq!(chain.name, "hair_chain");
193 assert!(chain.bones.is_empty());
194 }
195
196 #[test]
197 fn test_add_secondary_bone() {
198 let mut chain = new_secondary_chain("chain", [0.0, -9.81, 0.0]);
199 let bone = new_secondary_bone(0, "b0", [0.0, 0.0, 0.0], 50.0, 5.0);
200 add_secondary_bone(&mut chain, bone);
201 assert_eq!(chain.bones.len(), 1);
202 }
203
204 #[test]
205 fn test_secondary_bone_count() {
206 let mut chain = new_secondary_chain("c", [0.0, 0.0, 0.0]);
207 add_secondary_bone(&mut chain, new_secondary_bone(0, "b0", [0.0; 3], 1.0, 1.0));
208 add_secondary_bone(&mut chain, new_secondary_bone(1, "b1", [1.0; 3], 1.0, 1.0));
209 assert_eq!(secondary_bone_count(&chain), 2);
210 }
211
212 #[test]
213 fn test_update_secondary_bone_spring_force() {
214 let mut bone = new_secondary_bone(0, "b", [0.0, 0.0, 0.0], 100.0, 0.0);
215 bone.target_position = [1.0, 0.0, 0.0];
216 update_secondary_bone(&mut bone, 0.01, [0.0, 0.0, 0.0]);
221 assert!((bone.velocity[0] - 1.0).abs() < 1e-4);
222 assert!((bone.position[0] - 0.01).abs() < 1e-4);
223 }
224
225 #[test]
226 fn test_update_secondary_bone_external_force() {
227 let mut bone = new_secondary_bone(0, "b", [0.0; 3], 0.0, 0.0);
228 update_secondary_bone(&mut bone, 0.1, [0.0, 10.0, 0.0]);
230 assert!((bone.velocity[1] - 1.0).abs() < 1e-4);
231 }
232
233 #[test]
234 fn test_chain_kinetic_energy_zero_velocity() {
235 let mut chain = new_secondary_chain("c", [0.0; 3]);
236 add_secondary_bone(&mut chain, new_secondary_bone(0, "b0", [0.0; 3], 1.0, 1.0));
237 assert_eq!(chain_kinetic_energy(&chain), 0.0);
238 }
239
240 #[test]
241 fn test_chain_kinetic_energy_nonzero() {
242 let mut chain = new_secondary_chain("c", [0.0; 3]);
243 let mut bone = new_secondary_bone(0, "b0", [0.0; 3], 1.0, 1.0);
244 bone.velocity = [2.0, 0.0, 0.0];
245 bone.mass = 1.0;
246 add_secondary_bone(&mut chain, bone);
247 assert!((chain_kinetic_energy(&chain) - 2.0).abs() < 1e-5);
249 }
250
251 #[test]
252 fn test_reset_secondary_chain_zeros_velocity() {
253 let mut chain = new_secondary_chain("c", [0.0; 3]);
254 let mut bone = new_secondary_bone(0, "b0", [0.0; 3], 1.0, 1.0);
255 bone.velocity = [5.0, 3.0, 1.0];
256 add_secondary_bone(&mut chain, bone);
257 reset_secondary_chain(&mut chain);
258 assert_eq!(chain.bones[0].velocity, [0.0, 0.0, 0.0]);
259 }
260
261 #[test]
262 fn test_secondary_bone_lag() {
263 let mut bone = new_secondary_bone(0, "b", [0.0; 3], 1.0, 1.0);
264 bone.target_position = [3.0, 4.0, 0.0];
265 let lag = secondary_bone_lag(&bone);
266 assert!((lag - 5.0).abs() < 1e-5);
267 }
268
269 #[test]
270 fn test_secondary_bone_lag_zero() {
271 let bone = new_secondary_bone(0, "b", [1.0, 2.0, 3.0], 1.0, 1.0);
272 assert_eq!(secondary_bone_lag(&bone), 0.0);
273 }
274
275 #[test]
276 fn test_blend_secondary_to_target_full() {
277 let mut chain = new_secondary_chain("c", [0.0; 3]);
278 let mut bone = new_secondary_bone(0, "b0", [0.0; 3], 1.0, 1.0);
279 bone.target_position = [10.0, 0.0, 0.0];
280 add_secondary_bone(&mut chain, bone);
281 blend_secondary_to_target(&mut chain, 1.0);
282 assert!((chain.bones[0].position[0] - 10.0).abs() < 1e-5);
283 }
284
285 #[test]
286 fn test_blend_secondary_to_target_half() {
287 let mut chain = new_secondary_chain("c", [0.0; 3]);
288 let mut bone = new_secondary_bone(0, "b0", [0.0; 3], 1.0, 1.0);
289 bone.target_position = [10.0, 0.0, 0.0];
290 add_secondary_bone(&mut chain, bone);
291 blend_secondary_to_target(&mut chain, 0.5);
292 assert!((chain.bones[0].position[0] - 5.0).abs() < 1e-5);
293 }
294
295 #[test]
296 fn test_secondary_chain_positions() {
297 let mut chain = new_secondary_chain("c", [0.0; 3]);
298 add_secondary_bone(
299 &mut chain,
300 new_secondary_bone(0, "b0", [1.0, 2.0, 3.0], 1.0, 1.0),
301 );
302 add_secondary_bone(
303 &mut chain,
304 new_secondary_bone(1, "b1", [4.0, 5.0, 6.0], 1.0, 1.0),
305 );
306 let positions = secondary_chain_positions(&chain);
307 assert_eq!(positions.len(), 2);
308 assert_eq!(positions[0], [1.0, 2.0, 3.0]);
309 assert_eq!(positions[1], [4.0, 5.0, 6.0]);
310 }
311
312 #[test]
313 fn test_set_chain_wind() {
314 let mut chain = new_secondary_chain("c", [0.0; 3]);
315 set_chain_wind(&mut chain, [1.0, 0.0, 2.0]);
316 assert_eq!(chain.wind, [1.0, 0.0, 2.0]);
317 }
318
319 #[test]
320 fn test_update_secondary_chain_targets() {
321 let mut chain = new_secondary_chain("c", [0.0; 3]);
322 add_secondary_bone(&mut chain, new_secondary_bone(0, "b0", [0.0; 3], 10.0, 0.0));
323 let targets = [[1.0, 0.0, 0.0]];
324 update_secondary_chain(&mut chain, 0.016, &targets);
325 assert_eq!(chain.bones[0].target_position, [1.0, 0.0, 0.0]);
326 }
327
328 #[test]
329 fn test_default_secondary_config() {
330 let cfg = default_secondary_config();
331 assert!(cfg.default_stiffness > 0.0);
332 assert!(cfg.default_damping > 0.0);
333 assert!(cfg.default_mass > 0.0);
334 assert!(cfg.gravity[1] < 0.0);
335 }
336}
337
338pub enum SecondaryConstraint {
344 Pin { vertex_idx: usize, target: [f32; 3] },
346 Length { a: usize, b: usize, rest_len: f32 },
348 Volume { vertices: Vec<usize>, rest_vol: f32 },
350}
351
352pub struct XpbdParticle {
354 pub pos: [f32; 3],
355 pub prev_pos: [f32; 3],
356 pub inv_mass: f32,
358}
359
360pub struct SecondaryMotionSystem {
362 pub particles: Vec<XpbdParticle>,
363 pub constraints: Vec<SecondaryConstraint>,
364 pub gravity: [f32; 3],
365 pub xpbd_iterations: u32,
366}
367
368impl SecondaryMotionSystem {
369 pub fn new(gravity: [f32; 3]) -> Self {
371 SecondaryMotionSystem {
372 particles: Vec::new(),
373 constraints: Vec::new(),
374 gravity,
375 xpbd_iterations: 4,
376 }
377 }
378
379 pub fn add_particle(&mut self, pos: [f32; 3], inv_mass: f32) {
381 self.particles.push(XpbdParticle {
382 pos,
383 prev_pos: pos,
384 inv_mass,
385 });
386 }
387
388 pub fn add_constraint(&mut self, c: SecondaryConstraint) {
390 self.constraints.push(c);
391 }
392
393 pub fn update(&mut self, dt: f32) {
399 for p in self.particles.iter_mut() {
401 if p.inv_mass <= 0.0 {
402 continue;
403 }
404 let velocity = vec3_sub(p.pos, p.prev_pos);
405 p.prev_pos = p.pos;
406 let grav_dt2 = [
407 self.gravity[0] * dt * dt,
408 self.gravity[1] * dt * dt,
409 self.gravity[2] * dt * dt,
410 ];
411 p.pos = vec3_add(vec3_add(p.pos, velocity), grav_dt2);
412 }
413
414 for _ in 0..self.xpbd_iterations {
416 let n_constraints = self.constraints.len();
418 for ci in 0..n_constraints {
419 match &self.constraints[ci] {
423 SecondaryConstraint::Pin { vertex_idx, target } => {
424 let idx = *vertex_idx;
425 let tgt = *target;
426 if idx < self.particles.len() {
427 let p = &mut self.particles[idx];
428 if p.inv_mass > 0.0 {
429 p.pos = tgt;
430 }
431 }
432 }
433 SecondaryConstraint::Length { a, b, rest_len } => {
434 let (ia, ib, rl) = (*a, *b, *rest_len);
435 if ia >= self.particles.len() || ib >= self.particles.len() {
436 continue;
437 }
438 let pos_a = self.particles[ia].pos;
440 let pos_b = self.particles[ib].pos;
441 let inv_a = self.particles[ia].inv_mass;
442 let inv_b = self.particles[ib].inv_mass;
443
444 let delta = vec3_sub(pos_b, pos_a);
445 let dist = vec3_len(delta);
446 if dist < 1e-10 {
447 continue;
448 }
449 let w_sum = inv_a + inv_b;
450 if w_sum == 0.0 {
451 continue;
452 }
453 let scale = (dist - rl) / dist;
454 let correction = [delta[0] * scale, delta[1] * scale, delta[2] * scale];
455 self.particles[ia].pos = vec3_add(
456 self.particles[ia].pos,
457 vec3_scale(correction, inv_a / w_sum),
458 );
459 self.particles[ib].pos = vec3_sub(
460 self.particles[ib].pos,
461 vec3_scale(correction, inv_b / w_sum),
462 );
463 }
464 SecondaryConstraint::Volume { vertices, rest_vol } => {
465 let indices: Vec<usize> = vertices.clone();
466 let rv = *rest_vol;
467 let n = indices.len();
468 if n == 0 {
469 continue;
470 }
471
472 let mut centroid = [0.0_f32; 3];
474 let mut valid_count = 0usize;
475 for &vi in &indices {
476 if vi < self.particles.len() {
477 centroid = vec3_add(centroid, self.particles[vi].pos);
478 valid_count += 1;
479 }
480 }
481 if valid_count == 0 {
482 continue;
483 }
484 let inv_n = 1.0 / valid_count as f32;
485 centroid = vec3_scale(centroid, inv_n);
486
487 let mut mean_r2 = 0.0_f32;
489 for &vi in &indices {
490 if vi >= self.particles.len() {
491 continue;
492 }
493 let d = vec3_sub(self.particles[vi].pos, centroid);
494 mean_r2 += vec3_dot(d, d);
495 }
496 mean_r2 *= inv_n;
497
498 if mean_r2 < 1e-12 {
499 continue;
500 }
501
502 let ratio = (rv / mean_r2).sqrt();
504
505 for &vi in &indices {
506 if vi >= self.particles.len() {
507 continue;
508 }
509 if self.particles[vi].inv_mass <= 0.0 {
510 continue;
511 }
512 let offset = vec3_sub(self.particles[vi].pos, centroid);
513 let new_offset = vec3_scale(offset, ratio);
514 self.particles[vi].pos = vec3_add(centroid, new_offset);
515 }
516 }
517 }
518 }
519 }
520 }
521
522 pub fn particle_positions(&self) -> Vec<[f32; 3]> {
524 self.particles.iter().map(|p| p.pos).collect()
525 }
526
527 pub fn detect_self_collisions(&self, collision_radius: f32) -> Vec<(usize, usize)> {
531 let n = self.particles.len();
532 let threshold = 2.0 * collision_radius;
533
534 if n < 32 {
535 let mut pairs = Vec::new();
537 for i in 0..n {
538 for j in (i + 1)..n {
539 let d = vec3_sub(self.particles[i].pos, self.particles[j].pos);
540 if vec3_len(d) < threshold {
541 pairs.push((i, j));
542 }
543 }
544 }
545 pairs
546 } else {
547 spatial_hash_collisions(&self.particles, threshold)
549 }
550 }
551}
552
553fn spatial_hash_collisions(particles: &[XpbdParticle], threshold: f32) -> Vec<(usize, usize)> {
558 use std::collections::HashMap;
559
560 let cell_size = threshold.max(1e-6);
561 let inv_cell = 1.0 / cell_size;
562
563 let cell_key = |pos: [f32; 3]| -> (i64, i64, i64) {
564 (
565 (pos[0] * inv_cell).floor() as i64,
566 (pos[1] * inv_cell).floor() as i64,
567 (pos[2] * inv_cell).floor() as i64,
568 )
569 };
570
571 let mut grid: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
573 for (i, p) in particles.iter().enumerate() {
574 let key = cell_key(p.pos);
575 grid.entry(key).or_default().push(i);
576 }
577
578 let mut pairs: Vec<(usize, usize)> = Vec::new();
579 let offsets: &[(i64, i64, i64)] = &[
581 (-1, -1, -1),
582 (-1, -1, 0),
583 (-1, -1, 1),
584 (-1, 0, -1),
585 (-1, 0, 0),
586 (-1, 0, 1),
587 (-1, 1, -1),
588 (-1, 1, 0),
589 (-1, 1, 1),
590 (0, -1, -1),
591 (0, -1, 0),
592 (0, -1, 1),
593 (0, 0, -1),
594 (0, 0, 0),
595 (0, 0, 1),
596 (0, 1, -1),
597 (0, 1, 0),
598 (0, 1, 1),
599 (1, -1, -1),
600 (1, -1, 0),
601 (1, -1, 1),
602 (1, 0, -1),
603 (1, 0, 0),
604 (1, 0, 1),
605 (1, 1, -1),
606 (1, 1, 0),
607 (1, 1, 1),
608 ];
609
610 let mut seen: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
612
613 for (i, p) in particles.iter().enumerate() {
614 let (cx, cy, cz) = cell_key(p.pos);
615 for &(ox, oy, oz) in offsets {
616 let neighbour_key = (cx + ox, cy + oy, cz + oz);
617 if let Some(bucket) = grid.get(&neighbour_key) {
618 for &j in bucket {
619 if i == j {
620 continue;
621 }
622 let (lo, hi) = if i < j { (i, j) } else { (j, i) };
623 if seen.contains(&(lo, hi)) {
624 continue;
625 }
626 let d = vec3_sub(particles[i].pos, particles[j].pos);
627 if vec3_len(d) < threshold {
628 seen.insert((lo, hi));
629 pairs.push((lo, hi));
630 }
631 }
632 }
633 }
634 }
635
636 pairs.sort_unstable();
637 pairs
638}
639
640#[inline]
645fn vec3_add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
646 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
647}
648
649#[inline]
650fn vec3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
651 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
652}
653
654#[inline]
655fn vec3_scale(a: [f32; 3], s: f32) -> [f32; 3] {
656 [a[0] * s, a[1] * s, a[2] * s]
657}
658
659#[inline]
660fn vec3_len(a: [f32; 3]) -> f32 {
661 (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
662}
663
664#[inline]
665fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
666 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
667}
668
669#[cfg(test)]
674mod xpbd_tests {
675 use super::*;
676
677 #[test]
679 fn test_pin_holds_fixed() {
680 let mut sys = SecondaryMotionSystem::new([0.0, -9.8, 0.0]);
681 sys.add_particle([1.0, 2.0, 3.0], 1.0); sys.add_particle([0.0, 0.0, 0.0], 0.0); sys.add_constraint(SecondaryConstraint::Pin {
684 vertex_idx: 0,
685 target: [1.0, 2.0, 3.0],
686 });
687
688 for _ in 0..10 {
689 sys.update(0.016);
690 }
691
692 let pos = sys.particles[0].pos;
693 assert!(
694 (pos[0] - 1.0).abs() < 1e-5
695 && (pos[1] - 2.0).abs() < 1e-5
696 && (pos[2] - 3.0).abs() < 1e-5,
697 "Pinned particle drifted: {pos:?}"
698 );
699 }
700
701 #[test]
703 fn test_length_constraint_preserves_distance() {
704 let mut sys = SecondaryMotionSystem::new([0.0, 0.0, 0.0]);
705 sys.add_particle([0.0, 0.0, 0.0], 1.0);
706 sys.add_particle([2.0, 0.0, 0.0], 1.0); sys.add_constraint(SecondaryConstraint::Length {
708 a: 0,
709 b: 1,
710 rest_len: 1.0,
711 });
712
713 for _ in 0..30 {
714 sys.update(0.016);
715 }
716
717 let p0 = sys.particles[0].pos;
718 let p1 = sys.particles[1].pos;
719 let d = vec3_len(vec3_sub(p1, p0));
720 assert!(
721 (d - 1.0).abs() < 0.05,
722 "Length {d} deviates more than 5% from rest_len=1.0"
723 );
724 }
725
726 #[test]
728 fn test_no_stretch_under_gravity() {
729 let mut sys = SecondaryMotionSystem::new([0.0, -9.8, 0.0]);
730 sys.add_particle([0.0, 0.0, 0.0], 1.0);
731 sys.add_particle([0.0, 1.0, 0.0], 1.0);
732 sys.add_constraint(SecondaryConstraint::Length {
733 a: 0,
734 b: 1,
735 rest_len: 1.0,
736 });
737 sys.xpbd_iterations = 20;
739
740 for _ in 0..30 {
741 sys.update(0.016);
742 }
743
744 let p0 = sys.particles[0].pos;
745 let p1 = sys.particles[1].pos;
746 let d = vec3_len(vec3_sub(p1, p0));
747 assert!(
748 (d - 1.0).abs() < 0.05,
749 "Distance under gravity {d} deviates more than 5% from 1.0"
750 );
751 }
752
753 #[test]
755 fn test_self_collision_detection() {
756 let mut sys = SecondaryMotionSystem::new([0.0, 0.0, 0.0]);
757 sys.add_particle([0.0, 0.0, 0.0], 1.0);
758 sys.add_particle([0.1, 0.0, 0.0], 1.0);
759
760 let pairs = sys.detect_self_collisions(0.15);
761 assert_eq!(pairs.len(), 1, "Expected 1 collision pair, got {:?}", pairs);
762 assert_eq!(pairs[0], (0, 1));
763 }
764}