1use super::functions::*;
6use crate::constraint::{DistanceConstraint, SoftConstraint};
7use crate::particle::{SoftBody, SoftParticle};
8use crate::solver::XpbdSolver;
9use oxiphysics_core::math::{Real, Vec3};
10
11#[derive(Debug, Clone)]
13pub struct Rope {
14 pub nodes: Vec<RopeNode>,
16 pub segment_length: f64,
18 pub stiffness: f64,
20 pub bending_stiffness: f64,
22 pub damping: f64,
24 pub radius: f64,
26}
27impl Rope {
28 pub fn new_straight(
33 n_nodes: usize,
34 start: [f64; 3],
35 direction: [f64; 3],
36 total_length: f64,
37 mass_per_node: f64,
38 ) -> Self {
39 assert!(n_nodes >= 2, "A rope needs at least 2 nodes");
40 let segment_length = total_length / (n_nodes - 1) as f64;
41 let dir = normalize3(direction);
42 let mut nodes = Vec::with_capacity(n_nodes);
43 for i in 0..n_nodes {
44 let pos = add3(start, scale3(dir, segment_length * i as f64));
45 nodes.push(RopeNode::new(pos, mass_per_node));
46 }
47 Self {
48 nodes,
49 segment_length,
50 stiffness: 1.0,
51 bending_stiffness: 0.3,
52 damping: 0.01,
53 radius: 0.01,
54 }
55 }
56 pub fn fix_end(&mut self, end: usize) {
61 match end {
62 0 => self.nodes[0].fixed = true,
63 1 => {
64 let last = self.nodes.len() - 1;
65 self.nodes[last].fixed = true;
66 }
67 _ => panic!("end must be 0 (start) or 1 (end)"),
68 }
69 }
70 pub fn apply_gravity(&mut self, dt: f64, gravity: [f64; 3]) {
76 let accel_disp = scale3(gravity, dt * dt);
77 for node in &mut self.nodes {
78 if node.fixed {
79 continue;
80 }
81 node.prev_position = sub3(node.prev_position, accel_disp);
82 }
83 }
84 pub fn step(&mut self, dt: f64, gravity: [f64; 3]) {
86 self.apply_gravity(dt, gravity);
87 let damping_factor = 1.0 - self.damping;
88 for node in &mut self.nodes {
89 if node.fixed {
90 continue;
91 }
92 let vel = scale3(sub3(node.position, node.prev_position), 1.0 / dt);
93 let vel_damped = scale3(vel, damping_factor);
94 let new_pos = add3(node.position, scale3(vel_damped, dt));
95 node.prev_position = node.position;
96 node.position = new_pos;
97 }
98 self.solve_distance_constraints();
99 self.solve_bending_constraints();
100 for node in &mut self.nodes {
101 if node.fixed {
102 continue;
103 }
104 node.velocity = scale3(sub3(node.position, node.prev_position), 1.0 / dt);
105 }
106 }
107 pub fn solve_distance_constraints(&mut self) {
109 let n = self.nodes.len();
110 for i in 0..n - 1 {
111 let pi = self.nodes[i].position;
112 let pj = self.nodes[i + 1].position;
113 let delta = sub3(pj, pi);
114 let dist = len3(delta);
115 if dist < 1e-15 {
116 continue;
117 }
118 let error = dist - self.segment_length;
119 let correction = scale3(normalize3(delta), error * self.stiffness * 0.5);
120 let wi = if self.nodes[i].fixed { 0.0 } else { 1.0 };
121 let wj = if self.nodes[i + 1].fixed { 0.0 } else { 1.0 };
122 let total_w = wi + wj;
123 if total_w < 1e-15 {
124 continue;
125 }
126 if !self.nodes[i].fixed {
127 self.nodes[i].position =
128 add3(self.nodes[i].position, scale3(correction, wi / total_w));
129 }
130 if !self.nodes[i + 1].fixed {
131 self.nodes[i + 1].position =
132 sub3(self.nodes[i + 1].position, scale3(correction, wj / total_w));
133 }
134 }
135 }
136 pub fn solve_bending_constraints(&mut self) {
138 let n = self.nodes.len();
139 if n < 3 {
140 return;
141 }
142 for i in 1..n - 1 {
143 let pa = self.nodes[i - 1].position;
144 let pb = self.nodes[i].position;
145 let pc = self.nodes[i + 1].position;
146 let mid = scale3(add3(pa, pc), 0.5);
147 let delta = sub3(pb, mid);
148 let correction = scale3(delta, self.bending_stiffness * 0.5);
149 if !self.nodes[i - 1].fixed {
150 self.nodes[i - 1].position =
151 add3(self.nodes[i - 1].position, scale3(correction, 0.25));
152 }
153 if !self.nodes[i].fixed {
154 self.nodes[i].position = sub3(self.nodes[i].position, scale3(correction, 0.5));
155 }
156 if !self.nodes[i + 1].fixed {
157 self.nodes[i + 1].position =
158 add3(self.nodes[i + 1].position, scale3(correction, 0.25));
159 }
160 }
161 }
162 pub fn total_length(&self) -> f64 {
164 self.nodes
165 .windows(2)
166 .map(|w| len3(sub3(w[1].position, w[0].position)))
167 .sum()
168 }
169 pub fn curvature_at(&self, i: usize) -> f64 {
176 let n = self.nodes.len();
177 if i == 0 || i >= n - 1 {
178 return 0.0;
179 }
180 let t1 = normalize3(sub3(self.nodes[i].position, self.nodes[i - 1].position));
181 let t2 = normalize3(sub3(self.nodes[i + 1].position, self.nodes[i].position));
182 let dt = sub3(t2, t1);
183 if self.segment_length < 1e-15 {
184 0.0
185 } else {
186 len3(dt) / self.segment_length
187 }
188 }
189}
190impl Rope {
191 pub fn collide_sphere(&mut self, center: [f64; 3], radius: f64) -> Vec<RopeCollision> {
196 let mut collisions = Vec::new();
197 for i in 0..self.nodes.len() {
198 if self.nodes[i].fixed {
199 continue;
200 }
201 let diff = sub3(self.nodes[i].position, center);
202 let dist = len3(diff);
203 if dist < radius && dist > 1e-15 {
204 let normal = normalize3(diff);
205 let penetration = radius - dist;
206 self.nodes[i].position = add3(center, scale3(normal, radius));
207 collisions.push(RopeCollision {
208 node_index: i,
209 penetration,
210 normal,
211 });
212 }
213 }
214 collisions
215 }
216 pub fn collide_plane(
221 &mut self,
222 plane_point: [f64; 3],
223 plane_normal: [f64; 3],
224 ) -> Vec<RopeCollision> {
225 let mut collisions = Vec::new();
226 for i in 0..self.nodes.len() {
227 if self.nodes[i].fixed {
228 continue;
229 }
230 let to_node = sub3(self.nodes[i].position, plane_point);
231 let dist = dot3(to_node, plane_normal);
232 if dist < 0.0 {
233 self.nodes[i].position = sub3(self.nodes[i].position, scale3(plane_normal, dist));
234 collisions.push(RopeCollision {
235 node_index: i,
236 penetration: -dist,
237 normal: plane_normal,
238 });
239 }
240 }
241 collisions
242 }
243 pub fn resolve_self_collision(&mut self, min_distance: f64, skip_adjacent: usize) {
249 let n = self.nodes.len();
250 for i in 0..n {
251 if self.nodes[i].fixed {
252 continue;
253 }
254 for j in (i + skip_adjacent + 1)..n {
255 if self.nodes[j].fixed {
256 continue;
257 }
258 let diff = sub3(self.nodes[j].position, self.nodes[i].position);
259 let dist = len3(diff);
260 if dist < min_distance && dist > 1e-15 {
261 let normal = normalize3(diff);
262 let overlap = min_distance - dist;
263 let half = overlap * 0.5;
264 self.nodes[i].position = sub3(self.nodes[i].position, scale3(normal, half));
265 self.nodes[j].position = add3(self.nodes[j].position, scale3(normal, half));
266 }
267 }
268 }
269 }
270 pub fn collide_cylinder(
275 &mut self,
276 axis_start: [f64; 3],
277 axis_end: [f64; 3],
278 radius: f64,
279 ) -> Vec<RopeCollision> {
280 let axis = sub3(axis_end, axis_start);
281 let axis_len = len3(axis);
282 if axis_len < 1e-15 {
283 return Vec::new();
284 }
285 let axis_dir = normalize3(axis);
286 let mut collisions = Vec::new();
287 for i in 0..self.nodes.len() {
288 if self.nodes[i].fixed {
289 continue;
290 }
291 let to_node = sub3(self.nodes[i].position, axis_start);
292 let proj = dot3(to_node, axis_dir);
293 if proj < 0.0 || proj > axis_len {
294 continue;
295 }
296 let closest_on_axis = add3(axis_start, scale3(axis_dir, proj));
297 let radial = sub3(self.nodes[i].position, closest_on_axis);
298 let radial_dist = len3(radial);
299 if radial_dist < radius && radial_dist > 1e-15 {
300 let normal = normalize3(radial);
301 let penetration = radius - radial_dist;
302 self.nodes[i].position = add3(closest_on_axis, scale3(normal, radius));
303 collisions.push(RopeCollision {
304 node_index: i,
305 penetration,
306 normal,
307 });
308 }
309 }
310 collisions
311 }
312 pub fn catenary_positions(
322 start: [f64; 3],
323 end: [f64; 3],
324 rope_length: f64,
325 n_nodes: usize,
326 ) -> Vec<[f64; 3]> {
327 assert!(n_nodes >= 2, "Need at least 2 nodes");
328 let dx = end[0] - start[0];
329 let dy = end[1] - start[1];
330 let dz = end[2] - start[2];
331 let horiz_dist = (dx * dx + dz * dz).sqrt();
332 let vert_diff = dy;
333 let span = (dx * dx + dy * dy + dz * dz).sqrt();
334 if rope_length <= span + 1e-12 {
335 let mut positions = Vec::with_capacity(n_nodes);
336 for i in 0..n_nodes {
337 let t = i as f64 / (n_nodes - 1) as f64;
338 positions.push([start[0] + dx * t, start[1] + dy * t, start[2] + dz * t]);
339 }
340 return positions;
341 }
342 let mut a = 1.0_f64;
343 for _ in 0..50 {
344 let rhs = (rope_length * rope_length - vert_diff * vert_diff).sqrt();
345 let arg = horiz_dist / (2.0 * a);
346 let f = 2.0 * a * arg.sinh() - rhs;
347 let df = 2.0 * arg.sinh() - horiz_dist * arg.cosh() / a;
348 if df.abs() < 1e-15 {
349 break;
350 }
351 a -= f / df;
352 if a < 1e-6 {
353 a = 1e-6;
354 }
355 }
356 let x0 = horiz_dist * 0.5 - a * ((vert_diff / rope_length).atanh()).clamp(-10.0, 10.0);
357 let y0 = start[1] - a * ((0.0 - x0) / a).cosh();
358 let horiz_dir = if horiz_dist > 1e-15 {
359 [dx / horiz_dist, 0.0, dz / horiz_dist]
360 } else {
361 [1.0, 0.0, 0.0]
362 };
363 let mut positions = Vec::with_capacity(n_nodes);
364 for i in 0..n_nodes {
365 let t = i as f64 / (n_nodes - 1) as f64;
366 let x_local = horiz_dist * t;
367 let y_cat = a * ((x_local - x0) / a).cosh() + y0;
368 positions.push([
369 start[0] + horiz_dir[0] * x_local,
370 y_cat,
371 start[2] + horiz_dir[2] * x_local,
372 ]);
373 }
374 positions
375 }
376 pub fn is_taut(&self, tolerance: f64) -> bool {
380 let rest = self.segment_length * (self.nodes.len() - 1) as f64;
381 self.total_length() >= rest * (1.0 - tolerance)
382 }
383 pub fn enforce_max_stretch(&mut self, max_stretch_ratio: f64) {
389 let max_len = self.segment_length * max_stretch_ratio;
390 let n = self.nodes.len();
391 let max_iters = n * 10;
392 for _iter in 0..max_iters {
393 let mut converged = true;
394 for i in 0..n - 1 {
395 let diff = sub3(self.nodes[i + 1].position, self.nodes[i].position);
396 let dist = len3(diff);
397 if dist > max_len && dist > 1e-15 {
398 converged = false;
399 let correction_dir = normalize3(diff);
400 let excess = dist - max_len;
401 let half = excess * 0.5;
402 if !self.nodes[i].fixed {
403 self.nodes[i].position =
404 add3(self.nodes[i].position, scale3(correction_dir, half));
405 }
406 if !self.nodes[i + 1].fixed {
407 self.nodes[i + 1].position =
408 sub3(self.nodes[i + 1].position, scale3(correction_dir, half));
409 }
410 }
411 }
412 if converged {
413 break;
414 }
415 }
416 }
417 pub fn segment_tensions(&self) -> Vec<f64> {
422 let n = self.nodes.len();
423 let mut tensions = Vec::with_capacity(n - 1);
424 for i in 0..n - 1 {
425 let diff = sub3(self.nodes[i + 1].position, self.nodes[i].position);
426 let dist = len3(diff);
427 let stretch = (dist - self.segment_length).max(0.0);
428 tensions.push(self.stiffness * stretch / self.segment_length.max(1e-15));
429 }
430 tensions
431 }
432 pub fn max_tension(&self) -> f64 {
434 self.segment_tensions().into_iter().fold(0.0_f64, f64::max)
435 }
436 pub fn kinetic_energy(&self) -> f64 {
438 self.nodes
439 .iter()
440 .filter(|n| !n.fixed)
441 .map(|n| 0.5 * n.mass * dot3(n.velocity, n.velocity))
442 .sum()
443 }
444 pub fn potential_energy(&self, gravity_y: f64, ref_y: f64) -> f64 {
446 self.nodes
447 .iter()
448 .filter(|n| !n.fixed)
449 .map(|n| n.mass * gravity_y.abs() * (n.position[1] - ref_y))
450 .sum()
451 }
452 pub fn compute_catenary_tension(&self, gravity_y: f64) -> Vec<f64> {
469 let n = self.nodes.len();
470 if n < 2 {
471 return vec![0.0; n];
472 }
473 let m_avg: f64 = self.nodes.iter().map(|nd| nd.mass).sum::<f64>() / n as f64;
474 let w = m_avg * gravity_y.abs() / self.segment_length.max(1e-15);
475 let x_start = self.nodes[0].position[0];
476 let x_end = self.nodes[n - 1].position[0];
477 let span = (x_end - x_start).abs();
478 let y_start = self.nodes[0].position[1];
479 let y_end = self.nodes[n - 1].position[1];
480 let y_min = self
481 .nodes
482 .iter()
483 .map(|nd| nd.position[1])
484 .fold(f64::INFINITY, f64::min);
485 let sag = ((y_start + y_end) / 2.0 - y_min).max(1e-6);
486 let a = span * span / (8.0 * sag).max(1e-15);
487 let h = w * a;
488 let x_mid = (x_start + x_end) / 2.0;
489 self.nodes
490 .iter()
491 .map(|nd| {
492 let s = (nd.position[0] - x_mid).abs();
493 (h * h + (w * s) * (w * s)).sqrt()
494 })
495 .collect()
496 }
497 pub fn compute_dynamic_stiffness(
514 &self,
515 youngs_modulus: f64,
516 radius: f64,
517 gravity_y: f64,
518 ) -> f64 {
519 let area_moment = std::f64::consts::PI * radius.powi(4) / 4.0;
520 let ei_material = youngs_modulus * area_moment;
521 let tensions = self.compute_catenary_tension(gravity_y);
522 let avg_tension = if tensions.is_empty() {
523 0.0
524 } else {
525 tensions.iter().sum::<f64>() / tensions.len() as f64
526 };
527 let l = self.segment_length;
528 ei_material + avg_tension * l * l
529 }
530 pub fn apply_twist_constraint(&mut self, max_twist: f64, compliance: f64, dt: f64) {
545 let n = self.nodes.len();
546 if n < 3 {
547 return;
548 }
549 for i in 0..n - 2 {
550 let t0 = normalize3(sub3(self.nodes[i + 1].position, self.nodes[i].position));
551 let t1 = normalize3(sub3(self.nodes[i + 2].position, self.nodes[i + 1].position));
552 let cross = [
553 t0[1] * t1[2] - t0[2] * t1[1],
554 t0[2] * t1[0] - t0[0] * t1[2],
555 t0[0] * t1[1] - t0[1] * t1[0],
556 ];
557 let sin_angle = len3(cross);
558 let cos_angle = dot3(t0, t1).clamp(-1.0, 1.0);
559 let angle = sin_angle.atan2(cos_angle);
560 if angle.abs() <= max_twist {
561 continue;
562 }
563 let excess = angle - angle.signum() * max_twist;
564 let w_sum = if !self.nodes[i + 1].fixed && !self.nodes[i + 2].fixed {
565 2.0
566 } else if !self.nodes[i + 1].fixed || !self.nodes[i + 2].fixed {
567 1.0
568 } else {
569 continue;
570 };
571 let alpha_tilde = compliance / (dt * dt).max(1e-30);
572 let delta = excess / (w_sum + alpha_tilde);
573 let perp = normalize3(cross);
574 let correction = scale3(perp, delta * 0.5);
575 if !self.nodes[i + 1].fixed {
576 self.nodes[i + 1].position = add3(self.nodes[i + 1].position, correction);
577 }
578 if !self.nodes[i + 2].fixed {
579 self.nodes[i + 2].position = sub3(self.nodes[i + 2].position, correction);
580 }
581 }
582 }
583}
584#[derive(Debug, Clone)]
586pub struct RopeSegment {
587 pub p0: [f64; 3],
589 pub p1: [f64; 3],
591 pub rest_length: f64,
593 pub mass: f64,
595 pub k_stretch: f64,
597}
598impl RopeSegment {
599 pub fn new(p0: [f64; 3], p1: [f64; 3], rest_length: f64, mass: f64, k_stretch: f64) -> Self {
601 Self {
602 p0,
603 p1,
604 rest_length,
605 mass,
606 k_stretch,
607 }
608 }
609 pub fn elastic_force(&self) -> ([f64; 3], [f64; 3]) {
614 let diff = sub3(self.p1, self.p0);
615 let d = len3(diff);
616 if d < 1e-15 {
617 return ([0.0; 3], [0.0; 3]);
618 }
619 let dir = normalize3(diff);
620 let stretch = d - self.rest_length;
621 let f_mag = self.k_stretch * stretch;
622 let force = scale3(dir, f_mag);
623 (force, scale3(force, -1.0))
624 }
625}
626#[derive(Debug, Clone)]
633pub struct ViscoelasticSegment {
634 pub node_a: usize,
636 pub node_b: usize,
638 pub rest_length: f64,
640 pub ea: f64,
642 pub eta_a: f64,
644 pub prev_strain: f64,
646}
647impl ViscoelasticSegment {
648 pub fn new(node_a: usize, node_b: usize, rest_length: f64, ea: f64, eta_a: f64) -> Self {
650 Self {
651 node_a,
652 node_b,
653 rest_length,
654 ea,
655 eta_a,
656 prev_strain: 0.0,
657 }
658 }
659 pub fn strain(&self, positions: &[[f64; 3]]) -> f64 {
661 let pa = positions[self.node_a];
662 let pb = positions[self.node_b];
663 let l = len3(sub3(pb, pa));
664 (l - self.rest_length) / self.rest_length.max(1e-15)
665 }
666 pub fn kelvin_voigt_force(&mut self, positions: &[[f64; 3]], dt: f64) -> ([f64; 3], [f64; 3]) {
671 let pa = positions[self.node_a];
672 let pb = positions[self.node_b];
673 let diff = sub3(pb, pa);
674 let l = len3(diff);
675 if l < 1e-15 {
676 return ([0.0; 3], [0.0; 3]);
677 }
678 let dir = normalize3(diff);
679 let eps = (l - self.rest_length) / self.rest_length.max(1e-15);
680 let eps_dot = (eps - self.prev_strain) / dt.max(1e-15);
681 self.prev_strain = eps;
682 let sigma = self.ea * eps + self.eta_a * eps_dot;
683 let fa = scale3(dir, sigma);
684 (fa, scale3(fa, -1.0))
685 }
686 pub fn elastic_energy(&self, positions: &[[f64; 3]]) -> f64 {
688 let eps = self.strain(positions);
689 0.5 * self.ea * eps * eps * self.rest_length
690 }
691}
692#[derive(Debug, Clone)]
694pub struct RopeNode {
695 pub position: [f64; 3],
697 pub prev_position: [f64; 3],
699 pub velocity: [f64; 3],
701 pub mass: f64,
703 pub fixed: bool,
705}
706impl RopeNode {
707 fn new(position: [f64; 3], mass: f64) -> Self {
708 Self {
709 position,
710 prev_position: position,
711 velocity: [0.0; 3],
712 mass,
713 fixed: false,
714 }
715 }
716}
717#[derive(Debug, Clone)]
719pub struct RopeCollision {
720 pub node_index: usize,
722 pub penetration: f64,
724 pub normal: [f64; 3],
726}
727#[derive(Debug, Clone)]
729pub struct ViscoelasticRope {
730 pub positions: Vec<[f64; 3]>,
732 pub velocities: Vec<[f64; 3]>,
734 pub masses: Vec<f64>,
736 pub fixed: Vec<bool>,
738 pub segments: Vec<ViscoelasticSegment>,
740}
741impl ViscoelasticRope {
742 pub fn new_straight(
744 n: usize,
745 start: [f64; 3],
746 direction: [f64; 3],
747 seg_len: f64,
748 mass_per_node: f64,
749 ea: f64,
750 eta_a: f64,
751 ) -> Self {
752 assert!(n >= 2);
753 let dir = normalize3(direction);
754 let positions: Vec<[f64; 3]> = (0..n)
755 .map(|i| add3(start, scale3(dir, seg_len * i as f64)))
756 .collect();
757 let velocities = vec![[0.0; 3]; n];
758 let masses = vec![mass_per_node; n];
759 let fixed = vec![false; n];
760 let segments: Vec<ViscoelasticSegment> = (0..n - 1)
761 .map(|i| ViscoelasticSegment::new(i, i + 1, seg_len, ea, eta_a))
762 .collect();
763 Self {
764 positions,
765 velocities,
766 masses,
767 fixed,
768 segments,
769 }
770 }
771 pub fn step(&mut self, dt: f64, gravity: [f64; 3]) {
773 let n = self.positions.len();
774 let mut forces = vec![[0.0_f64; 3]; n];
775 let positions_snap = self.positions.clone();
776 for seg in &mut self.segments {
777 let (fa, fb) = seg.kelvin_voigt_force(&positions_snap, dt);
778 forces[seg.node_a] = add3(forces[seg.node_a], fa);
779 forces[seg.node_b] = add3(forces[seg.node_b], fb);
780 }
781 for (i, f) in forces.iter().enumerate().take(n) {
782 if self.fixed[i] {
783 continue;
784 }
785 let inv_m = 1.0 / self.masses[i].max(1e-30);
786 let a = [
787 f[0] * inv_m + gravity[0],
788 f[1] * inv_m + gravity[1],
789 f[2] * inv_m + gravity[2],
790 ];
791 self.velocities[i] = add3(self.velocities[i], scale3(a, dt));
792 self.positions[i] = add3(self.positions[i], scale3(self.velocities[i], dt));
793 }
794 }
795 pub fn total_elastic_energy(&self) -> f64 {
797 self.segments
798 .iter()
799 .map(|s| s.elastic_energy(&self.positions))
800 .sum()
801 }
802 pub fn total_kinetic_energy(&self) -> f64 {
804 self.positions
805 .iter()
806 .enumerate()
807 .filter(|(i, _)| !self.fixed[*i])
808 .map(|(i, _)| 0.5 * self.masses[i] * dot3(self.velocities[i], self.velocities[i]))
809 .sum()
810 }
811}
812#[derive(Debug, Clone)]
818pub struct XpbdRope {
819 pub body: SoftBody,
821 pub constraints: Vec<DistanceConstraint>,
823}
824impl XpbdRope {
825 pub fn new(
832 start: Vec3,
833 end: Vec3,
834 num_segments: usize,
835 mass_per_segment: Real,
836 compliance: Real,
837 ) -> Self {
838 assert!(num_segments >= 1, "Rope needs at least one segment");
839 let num_particles = num_segments + 1;
840 let mut particles = Vec::with_capacity(num_particles);
841 for i in 0..num_particles {
842 let t = i as Real / num_segments as Real;
843 let pos = start + (end - start) * t;
844 if i == 0 {
845 particles.push(SoftParticle::new_static(pos));
846 } else {
847 particles.push(SoftParticle::new(pos, mass_per_segment));
848 }
849 }
850 let body = SoftBody::from_particles(particles);
851 let mut constraints = Vec::with_capacity(num_segments);
852 for i in 0..num_segments {
853 constraints.push(DistanceConstraint::from_particles(
854 i,
855 i + 1,
856 &body.particles,
857 compliance,
858 ));
859 }
860 Self { body, constraints }
861 }
862 pub fn num_particles(&self) -> usize {
864 self.body.particles.len()
865 }
866 pub fn num_segments(&self) -> usize {
868 self.constraints.len()
869 }
870}
871#[derive(Debug, Clone)]
873pub struct HairStrand {
874 pub rope: Rope,
876 pub color: [f64; 3],
878}
879impl HairStrand {
880 pub fn new(root: [f64; 3], length: f64, n_nodes: usize) -> Self {
884 let direction = [0.0, -1.0, 0.0];
885 let mut rope = Rope::new_straight(n_nodes, root, direction, length, 1.0 / n_nodes as f64);
886 rope.fix_end(0);
887 Self {
888 rope,
889 color: [0.6, 0.4, 0.2],
890 }
891 }
892}
893#[derive(Debug, Clone)]
899pub struct RopeVerlet {
900 pub particles: Vec<[f64; 3]>,
902 pub velocities: Vec<[f64; 3]>,
904 pub masses: Vec<f64>,
906 pub rest_lengths: Vec<f64>,
908 pub fixed: Vec<bool>,
910}
911impl RopeVerlet {
912 pub fn new(
917 n: usize,
918 start: [f64; 3],
919 direction: [f64; 3],
920 segment_length: f64,
921 mass_per_segment: f64,
922 ) -> Self {
923 assert!(n >= 2, "Rope needs at least 2 particles");
924 let dir = normalize3(direction);
925 let mut particles = Vec::with_capacity(n);
926 for i in 0..n {
927 particles.push(add3(start, scale3(dir, segment_length * i as f64)));
928 }
929 let velocities = vec![[0.0_f64; 3]; n];
930 let masses = vec![mass_per_segment; n];
931 let rest_lengths = vec![segment_length; n - 1];
932 let fixed = vec![false; n];
933 Self {
934 particles,
935 velocities,
936 masses,
937 rest_lengths,
938 fixed,
939 }
940 }
941 pub fn total_length(&self) -> f64 {
943 self.particles
944 .windows(2)
945 .map(|w| len3(sub3(w[1], w[0])))
946 .sum()
947 }
948 pub fn step(&mut self, dt: f64, gravity: [f64; 3], k_stretch: f64, damping: f64) {
953 let n = self.particles.len();
954 for i in 0..n {
955 if self.fixed[i] {
956 continue;
957 }
958 self.velocities[i] = add3(self.velocities[i], scale3(gravity, dt));
959 self.velocities[i] = scale3(self.velocities[i], 1.0 - damping);
960 self.particles[i] = add3(self.particles[i], scale3(self.velocities[i], dt));
961 }
962 self.apply_constraint_iterations(k_stretch, 1);
963 }
964 pub fn apply_constraint_iterations(&mut self, k: f64, n_iters: usize) {
966 let n = self.particles.len();
967 for _ in 0..n_iters {
968 for seg in 0..n - 1 {
969 let pi = self.particles[seg];
970 let pj = self.particles[seg + 1];
971 let diff = sub3(pj, pi);
972 let dist = len3(diff);
973 if dist < 1e-15 {
974 continue;
975 }
976 let rest = self.rest_lengths[seg];
977 let error = dist - rest;
978 let correction = scale3(normalize3(diff), error * k * 0.5);
979 let wi = if self.fixed[seg] { 0.0 } else { 1.0 };
980 let wj = if self.fixed[seg + 1] { 0.0 } else { 1.0 };
981 let total_w = wi + wj;
982 if total_w < 1e-15 {
983 continue;
984 }
985 if !self.fixed[seg] {
986 self.particles[seg] = add3(pi, scale3(correction, wi / total_w));
987 }
988 if !self.fixed[seg + 1] {
989 self.particles[seg + 1] = sub3(pj, scale3(correction, wj / total_w));
990 }
991 }
992 }
993 }
994 pub fn catenary_shape(&self) -> Vec<f64> {
1001 let n = self.particles.len();
1002 if n < 2 {
1003 return vec![0.0];
1004 }
1005 let start = self.particles[0];
1006 let end = self.particles[n - 1];
1007 let dx = end[0] - start[0];
1008 let dy = end[1] - start[1];
1009 let dz = end[2] - start[2];
1010 let horiz_dist = (dx * dx + dz * dz).sqrt();
1011 let vert_diff = dy;
1012 let rope_length = self.total_length();
1013 let span = (dx * dx + dy * dy + dz * dz).sqrt();
1014 if rope_length <= span + 1e-12 || horiz_dist < 1e-12 {
1015 return vec![0.0];
1016 }
1017 let mut a = horiz_dist.max(1e-6);
1018 for _ in 0..50 {
1019 let rhs = (rope_length * rope_length - vert_diff * vert_diff).sqrt();
1020 let arg = horiz_dist / (2.0 * a);
1021 let f = 2.0 * a * arg.sinh() - rhs;
1022 let df = 2.0 * arg.sinh() - horiz_dist * arg.cosh() / a;
1023 if df.abs() < 1e-15 {
1024 break;
1025 }
1026 a -= f / df;
1027 if a < 1e-6 {
1028 a = 1e-6;
1029 }
1030 }
1031 vec![a]
1032 }
1033}
1034#[derive(Debug, Clone)]
1036pub struct HairSystem {
1037 pub strands: Vec<HairStrand>,
1039}
1040impl HairSystem {
1041 pub fn new() -> Self {
1043 Self {
1044 strands: Vec::new(),
1045 }
1046 }
1047 pub fn add_strand(&mut self, strand: HairStrand) {
1049 self.strands.push(strand);
1050 }
1051 pub fn step(&mut self, dt: f64, gravity: [f64; 3]) {
1053 for strand in &mut self.strands {
1054 strand.rope.step(dt, gravity);
1055 }
1056 }
1057 pub fn strand_count(&self) -> usize {
1059 self.strands.len()
1060 }
1061}
1062#[derive(Debug, Clone, Copy)]
1069pub struct MaterialFrame {
1070 pub d1: [f64; 3],
1072 pub d2: [f64; 3],
1074 pub d3: [f64; 3],
1076}
1077impl MaterialFrame {
1078 pub fn from_tangent(tangent: [f64; 3]) -> Self {
1083 let d3 = normalize3(tangent);
1084 let d1 = Self::perpendicular_to(d3);
1085 let d2 = cross3(d3, d1);
1086 Self { d1, d2, d3 }
1087 }
1088 pub fn twist(&self, phi: f64) -> Self {
1090 let cos_phi = phi.cos();
1091 let sin_phi = phi.sin();
1092 let d1_new = [
1093 cos_phi * self.d1[0] + sin_phi * self.d2[0],
1094 cos_phi * self.d1[1] + sin_phi * self.d2[1],
1095 cos_phi * self.d1[2] + sin_phi * self.d2[2],
1096 ];
1097 let d2_new = cross3(self.d3, d1_new);
1098 Self {
1099 d1: d1_new,
1100 d2: d2_new,
1101 d3: self.d3,
1102 }
1103 }
1104 fn perpendicular_to(n: [f64; 3]) -> [f64; 3] {
1106 let ax: [f64; 3] = if n[0].abs() <= n[1].abs() && n[0].abs() <= n[2].abs() {
1107 [1.0, 0.0, 0.0]
1108 } else if n[1].abs() <= n[2].abs() {
1109 [0.0, 1.0, 0.0]
1110 } else {
1111 [0.0, 0.0, 1.0]
1112 };
1113 let d = dot3(ax, n);
1114 normalize3([ax[0] - d * n[0], ax[1] - d * n[1], ax[2] - d * n[2]])
1115 }
1116}
1117#[derive(Debug, Clone)]
1123pub struct KirchhoffRod {
1124 pub positions: Vec<[f64; 3]>,
1126 pub frames: Vec<MaterialFrame>,
1128 pub rest_length: f64,
1130 pub bending_stiffness: f64,
1132 pub twisting_stiffness: f64,
1134}
1135impl KirchhoffRod {
1136 pub fn new_straight(
1140 n: usize,
1141 start: [f64; 3],
1142 direction: [f64; 3],
1143 seg_len: f64,
1144 bending_stiffness: f64,
1145 twisting_stiffness: f64,
1146 ) -> Self {
1147 assert!(n >= 2, "rod needs at least 2 nodes");
1148 let dir = normalize3(direction);
1149 let positions: Vec<[f64; 3]> = (0..n)
1150 .map(|i| add3(start, scale3(dir, seg_len * i as f64)))
1151 .collect();
1152 let frames: Vec<MaterialFrame> = (0..n - 1)
1153 .map(|_| MaterialFrame::from_tangent(dir))
1154 .collect();
1155 Self {
1156 positions,
1157 frames,
1158 rest_length: seg_len,
1159 bending_stiffness,
1160 twisting_stiffness,
1161 }
1162 }
1163 pub fn darboux_vector(&self, i: usize) -> [f64; 3] {
1168 let n = self.positions.len();
1169 if i == 0 || i >= n - 1 {
1170 return [0.0; 3];
1171 }
1172 let f_prev = &self.frames[i - 1];
1173 let f_next = &self.frames[i];
1174 let inv_l = 1.0 / self.rest_length.max(1e-15);
1175 let kappa1 = dot3(f_prev.d2, f_next.d3) * inv_l;
1176 let kappa2 = dot3(f_prev.d3, f_next.d1) * inv_l;
1177 let tau = dot3(f_prev.d1, f_next.d2) * inv_l;
1178 [kappa1, kappa2, tau]
1179 }
1180 pub fn bending_energy(&self) -> f64 {
1184 let n = self.positions.len();
1185 let mut e = 0.0;
1186 for i in 1..n - 1 {
1187 let dv = self.darboux_vector(i);
1188 e += dv[0] * dv[0] + dv[1] * dv[1];
1189 }
1190 0.5 * self.bending_stiffness * e * self.rest_length
1191 }
1192 pub fn twisting_energy(&self) -> f64 {
1196 let n = self.positions.len();
1197 let mut e = 0.0;
1198 for i in 1..n - 1 {
1199 let dv = self.darboux_vector(i);
1200 e += dv[2] * dv[2];
1201 }
1202 0.5 * self.twisting_stiffness * e * self.rest_length
1203 }
1204 pub fn total_elastic_energy(&self) -> f64 {
1206 self.bending_energy() + self.twisting_energy()
1207 }
1208 pub fn update_frames(&mut self) {
1214 let n = self.positions.len();
1215 for s in 0..n - 1 {
1216 let tangent = normalize3(sub3(self.positions[s + 1], self.positions[s]));
1217 let d1_old = self.frames[s].d1;
1218 let proj = dot3(d1_old, tangent);
1219 let d1_proj = sub3(d1_old, scale3(tangent, proj));
1220 let d1_new = if len3(d1_proj) > 1e-14 {
1221 normalize3(d1_proj)
1222 } else {
1223 MaterialFrame::perpendicular_to(tangent)
1224 };
1225 let d2_new = cross3(tangent, d1_new);
1226 self.frames[s] = MaterialFrame {
1227 d1: d1_new,
1228 d2: d2_new,
1229 d3: tangent,
1230 };
1231 }
1232 }
1233}
1234#[derive(Debug)]
1236pub struct RopeSolver {
1237 pub solver: XpbdSolver,
1239}
1240impl RopeSolver {
1241 pub fn new(num_substeps: usize) -> Self {
1243 Self {
1244 solver: XpbdSolver::new(num_substeps),
1245 }
1246 }
1247 pub fn step(&mut self, rope: &mut XpbdRope, dt: Real) {
1249 let mut boxed: Vec<Box<dyn SoftConstraint>> = rope
1250 .constraints
1251 .iter()
1252 .cloned()
1253 .map(|c| Box::new(c) as Box<dyn SoftConstraint>)
1254 .collect();
1255 self.solver.solve(&mut rope.body, &mut boxed, dt);
1256 }
1257}