1use crate::math::{Fix128, Vec3Fix};
16#[cfg(feature = "std")]
17use crate::sdf_collider::SdfCollider;
18
19#[cfg(not(feature = "std"))]
20use alloc::vec;
21#[cfg(not(feature = "std"))]
22use alloc::vec::Vec;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct RopeConfig {
31 pub iterations: usize,
33 pub substeps: usize,
35 pub gravity: Vec3Fix,
37 pub damping: Fix128,
39 pub compliance: Fix128,
41 pub sdf_friction: Fix128,
45}
46
47impl Default for RopeConfig {
48 fn default() -> Self {
49 Self {
50 iterations: 8,
51 substeps: 4,
52 gravity: Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-10), Fix128::ZERO),
53 damping: Fix128::from_ratio(99, 100),
54 compliance: Fix128::ZERO,
55 sdf_friction: Fix128::from_ratio(3, 10),
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub struct PinConstraint {
63 pub particle_index: usize,
65 pub target: Vec3Fix,
67 pub body_index: Option<usize>,
69 pub local_offset: Vec3Fix,
71}
72
73pub struct Rope {
79 pub positions: Vec<Vec3Fix>,
81 pub prev_positions: Vec<Vec3Fix>,
83 pub velocities: Vec<Vec3Fix>,
85 pub inv_masses: Vec<Fix128>,
87 pub rest_lengths: Vec<Fix128>,
89 pub pins: Vec<PinConstraint>,
91 pub config: RopeConfig,
93 pub total_length: Fix128,
95}
96
97impl Rope {
98 #[must_use]
104 pub fn new(start: Vec3Fix, end: Vec3Fix, num_segments: usize, mass_per_unit: Fix128) -> Self {
105 assert!(num_segments > 0, "Rope requires at least one segment");
106 let n = num_segments + 1; let total_length = (end - start).length();
108 let segment_length = total_length / Fix128::from_int(num_segments as i64);
109 let particle_mass = mass_per_unit * segment_length;
114 let inv_mass = if particle_mass.is_zero() {
115 Fix128::ZERO
116 } else {
117 Fix128::ONE / particle_mass
118 };
119 let end_inv_mass = inv_mass.double();
120
121 let mut positions = Vec::with_capacity(n);
122 for i in 0..n {
123 let t = Fix128::from_ratio(i as i64, num_segments as i64);
124 let p = Vec3Fix::new(
125 start.x + (end.x - start.x) * t,
126 start.y + (end.y - start.y) * t,
127 start.z + (end.z - start.z) * t,
128 );
129 positions.push(p);
130 }
131
132 let rest_lengths = vec![segment_length; num_segments];
133 let mut inv_masses = vec![inv_mass; n];
134 inv_masses[0] = end_inv_mass;
135 inv_masses[n - 1] = end_inv_mass;
136
137 Self {
138 prev_positions: positions.clone(),
139 velocities: vec![Vec3Fix::ZERO; n],
140 positions,
141 inv_masses,
142 rest_lengths,
143 pins: Vec::new(),
144 config: RopeConfig::default(),
145 total_length,
146 }
147 }
148
149 #[inline]
151 #[must_use]
152 pub fn particle_count(&self) -> usize {
153 self.positions.len()
154 }
155
156 #[inline]
158 #[must_use]
159 pub fn segment_count(&self) -> usize {
160 self.rest_lengths.len()
161 }
162
163 pub fn add_pin(&mut self, pin: PinConstraint) {
165 self.inv_masses[pin.particle_index] = Fix128::ZERO;
166 self.pins.push(pin);
167 }
168
169 pub fn pin_start(&mut self) {
171 let pos = self.positions[0];
172 self.add_pin(PinConstraint {
173 particle_index: 0,
174 target: pos,
175 body_index: None,
176 local_offset: Vec3Fix::ZERO,
177 });
178 }
179
180 pub fn pin_end(&mut self) {
182 let last = self.particle_count() - 1;
183 let pos = self.positions[last];
184 self.add_pin(PinConstraint {
185 particle_index: last,
186 target: pos,
187 body_index: None,
188 local_offset: Vec3Fix::ZERO,
189 });
190 }
191
192 pub fn update_pin_targets(
196 &mut self,
197 body_positions: &[Vec3Fix],
198 body_rotations: &[crate::math::QuatFix],
199 ) {
200 for pin in &mut self.pins {
201 if let Some(idx) = pin.body_index {
202 if idx < body_positions.len() && idx < body_rotations.len() {
203 pin.target =
204 body_positions[idx] + body_rotations[idx].rotate_vec(pin.local_offset);
205 }
206 }
207 }
208 }
209
210 pub fn step(&mut self, dt: Fix128) {
212 let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
213
214 for _ in 0..self.config.substeps {
215 self.substep(substep_dt);
216 }
217 self.apply_frame_damping();
218 }
219
220 #[cfg(feature = "std")]
222 pub fn step_with_sdf(&mut self, dt: Fix128, sdf_colliders: &[SdfCollider]) {
223 let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
224
225 for _ in 0..self.config.substeps {
226 self.substep(substep_dt);
227 self.resolve_sdf_collisions(sdf_colliders);
228 }
229 self.apply_frame_damping();
230 }
231
232 fn apply_frame_damping(&mut self) {
238 let d = self.config.damping;
239 for v in &mut self.velocities {
240 *v = *v * d;
241 }
242 }
243
244 fn substep(&mut self, dt: Fix128) {
246 let n = self.particle_count();
247
248 for i in 0..n {
250 if self.inv_masses[i].is_zero() {
251 continue;
252 }
253
254 self.prev_positions[i] = self.positions[i];
255 self.velocities[i] = self.velocities[i] + self.config.gravity * dt;
256 self.positions[i] = self.positions[i] + self.velocities[i] * dt;
257 }
258
259 for pin in &self.pins {
261 self.positions[pin.particle_index] = pin.target;
262 }
263
264 for _ in 0..self.config.iterations {
266 self.solve_distance_constraints(dt);
267 }
268
269 let inv_dt = Fix128::ONE / dt;
271 for i in 0..n {
272 if self.inv_masses[i].is_zero() {
273 continue;
274 }
275 self.velocities[i] = (self.positions[i] - self.prev_positions[i]) * inv_dt;
276 }
277 }
278
279 fn solve_distance_constraints(&mut self, dt: Fix128) {
281 for i in 0..self.segment_count() {
282 let p0 = self.positions[i];
283 let p1 = self.positions[i + 1];
284 let w0 = self.inv_masses[i];
285 let w1 = self.inv_masses[i + 1];
286
287 let w_sum = w0 + w1;
288 if w_sum.is_zero() {
289 continue;
290 }
291
292 let delta = p1 - p0;
293 let dist = delta.length();
294 if dist.is_zero() {
295 continue;
296 }
297
298 let rest = self.rest_lengths[i];
299 let error = dist - rest;
300
301 let compliance_term = self.config.compliance / (dt * dt);
302 let lambda = error / (w_sum + compliance_term);
303 let correction = delta / dist * lambda;
304
305 if !w0.is_zero() {
306 self.positions[i] = self.positions[i] + correction * w0;
307 }
308 if !w1.is_zero() {
309 self.positions[i + 1] = self.positions[i + 1] - correction * w1;
310 }
311 }
312 }
313
314 #[cfg(feature = "std")]
316 fn resolve_sdf_collisions(&mut self, sdf_colliders: &[SdfCollider]) {
317 for i in 0..self.particle_count() {
318 if self.inv_masses[i].is_zero() {
319 continue;
320 }
321
322 for sdf in sdf_colliders {
323 let (lx, ly, lz) = sdf.world_to_local(self.positions[i]);
324 let dist = sdf.field.distance(lx, ly, lz) * sdf.scale_f32;
325
326 if dist < 0.0 {
327 let (nx, ny, nz) = sdf.field.normal(lx, ly, lz);
329 let normal = sdf.local_normal_to_world(nx, ny, nz);
330 let depth = Fix128::from_f32(-dist);
331
332 self.positions[i] = self.positions[i] + normal * depth;
333
334 let vel = self.velocities[i];
341 let vn_mag = vel.dot(normal);
342 let vt = vel - normal * vn_mag;
343 if vn_mag < Fix128::ZERO {
344 let restitution = Fix128::from_ratio(1, 10);
345 let dvn = -(Fix128::ONE + restitution) * vn_mag;
346 let vt_mag = vt.length();
347 let drop = self.config.sdf_friction * dvn;
348 let vt_new = if vt_mag <= drop || vt_mag.is_zero() {
349 Vec3Fix::ZERO
350 } else {
351 vt * ((vt_mag - drop) / vt_mag)
352 };
353 self.velocities[i] = normal * (-restitution * vn_mag) + vt_new;
354 }
355 }
356 }
357 }
358 }
359
360 #[must_use]
362 pub fn current_length(&self) -> Fix128 {
363 let mut length = Fix128::ZERO;
364 for i in 0..self.segment_count() {
365 length = length + (self.positions[i + 1] - self.positions[i]).length();
366 }
367 length
368 }
369}
370
371impl core::fmt::Debug for Rope {
372 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
373 f.debug_struct("Rope")
374 .field(
375 "positions",
376 &format_args!("[{} items]", self.positions.len()),
377 )
378 .field(
379 "prev_positions",
380 &format_args!("[{} items]", self.prev_positions.len()),
381 )
382 .field(
383 "velocities",
384 &format_args!("[{} items]", self.velocities.len()),
385 )
386 .field(
387 "inv_masses",
388 &format_args!("[{} items]", self.inv_masses.len()),
389 )
390 .field(
391 "rest_lengths",
392 &format_args!("[{} items]", self.rest_lengths.len()),
393 )
394 .field("pins", &format_args!("[{} items]", self.pins.len()))
395 .field("config", &self.config)
396 .field("total_length", &self.total_length)
397 .finish()
398 }
399}
400
401#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_rope_creation() {
411 let rope = Rope::new(
412 Vec3Fix::ZERO,
413 Vec3Fix::from_int(10, 0, 0),
414 20,
415 Fix128::from_ratio(1, 10),
416 );
417
418 assert_eq!(rope.particle_count(), 21);
419 assert_eq!(rope.segment_count(), 20);
420 }
421
422 #[test]
423 fn test_rope_pins() {
424 let mut rope = Rope::new(
425 Vec3Fix::ZERO,
426 Vec3Fix::from_int(5, 0, 0),
427 10,
428 Fix128::from_ratio(1, 10),
429 );
430
431 rope.pin_start();
432 rope.pin_end();
433
434 assert_eq!(rope.pins.len(), 2);
435 assert!(rope.inv_masses[0].is_zero(), "Start should be pinned");
436 assert!(rope.inv_masses[10].is_zero(), "End should be pinned");
437 }
438
439 #[test]
440 fn test_rope_gravity() {
441 let mut rope = Rope::new(
442 Vec3Fix::ZERO,
443 Vec3Fix::from_int(5, 0, 0),
444 10,
445 Fix128::from_ratio(1, 10),
446 );
447 rope.pin_start();
448 rope.pin_end();
449
450 let dt = Fix128::from_ratio(1, 60);
452 for _ in 0..60 {
453 rope.step(dt);
454 }
455
456 let mid = rope.positions[5];
458 assert!(mid.y < Fix128::ZERO, "Rope should sag under gravity");
459 }
460
461 #[test]
462 fn test_rope_length_preservation() {
463 let mut rope = Rope::new(
464 Vec3Fix::ZERO,
465 Vec3Fix::from_int(5, 0, 0),
466 10,
467 Fix128::from_ratio(1, 10),
468 );
469 rope.pin_start();
470 rope.pin_end();
471 rope.config.iterations = 16;
472
473 let initial_length = rope.total_length;
474
475 let dt = Fix128::from_ratio(1, 60);
476 for _ in 0..120 {
477 rope.step(dt);
478 }
479
480 let current_length = rope.current_length();
481 let error = (current_length - initial_length).abs();
482 let tolerance = initial_length * Fix128::from_ratio(1, 10); assert!(
484 error < tolerance,
485 "Rope length should be approximately preserved: initial={:?}, current={:?}",
486 initial_length.to_f32(),
487 current_length.to_f32()
488 );
489 }
490
491 #[cfg(feature = "std")]
493 fn unit_sphere_collider() -> crate::sdf_collider::SdfCollider {
494 use crate::sdf_collider::{ClosureSdf, SdfCollider};
495 let field = ClosureSdf::new(
496 |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
497 |x, y, z| {
498 let len = (x * x + y * y + z * z).sqrt();
499 if len > 1e-6 {
500 (x / len, y / len, z / len)
501 } else {
502 (0.0, 1.0, 0.0)
503 }
504 },
505 );
506 SdfCollider::new_static(
507 Box::new(field),
508 Vec3Fix::ZERO,
509 crate::math::QuatFix::IDENTITY,
510 )
511 }
512
513 #[cfg(feature = "std")]
515 fn sphere_dist(p: Vec3Fix) -> f32 {
516 let (x, y, z) = p.to_f32();
517 (x * x + y * y + z * z).sqrt() - 1.0
518 }
519
520 #[cfg(feature = "std")]
521 #[test]
522 fn step_with_sdf_keeps_free_rope_particles_outside_unit_sphere() {
523 let make = || {
525 Rope::new(
526 Vec3Fix::from_int(-1, 2, 0),
527 Vec3Fix::from_int(1, 2, 0),
528 8,
529 Fix128::ONE,
530 )
531 };
532 let sphere = [unit_sphere_collider()];
533 let mut rope = make();
534 let dt = Fix128::from_ratio(1, 60);
535 let mut min_dist = f32::MAX;
536 for frame in 0..60 {
537 rope.step_with_sdf(dt, &sphere);
538 for (i, p) in rope.positions.iter().enumerate() {
539 let d = sphere_dist(*p);
540 min_dist = min_dist.min(d);
541 assert!(
542 d >= -1e-3,
543 "frame {frame} particle {i} inside sphere: dist {d}"
544 );
545 }
546 }
547 assert!(min_dist < 0.05, "never touched: min dist {min_dist}");
548 let len = rope.current_length().to_f32();
550 assert!((len - 2.0).abs() < 0.2, "length {len}");
551
552 let mut free = make();
554 let mut free_min = f32::MAX;
555 for _ in 0..60 {
556 free.step(dt);
557 for p in &free.positions {
558 free_min = free_min.min(sphere_dist(*p));
559 }
560 }
561 assert!(
562 free_min < -0.1,
563 "without SDF the rope should pass through: {free_min}"
564 );
565
566 let mut pinned = Rope::new(
568 Vec3Fix::from_f32(0.0, 0.5, 0.0),
569 Vec3Fix::from_int(0, 3, 0),
570 5,
571 Fix128::ONE,
572 );
573 pinned.pin_start();
574 for _ in 0..10 {
575 pinned.step_with_sdf(dt, &sphere);
576 }
577 assert_eq!(pinned.positions[0], Vec3Fix::from_f32(0.0, 0.5, 0.0));
578
579 let mut a = make();
581 let mut b = make();
582 for _ in 0..10 {
583 a.step_with_sdf(dt, &[]);
584 b.step(dt);
585 }
586 assert_eq!(a.positions, b.positions);
587 assert_eq!(a.velocities, b.velocities);
588 }
589}