Skip to main content

oxiphysics_softbody/constraint/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use crate::particle::SoftParticle;
7use oxiphysics_core::math::{Real, Vec3};
8
9/// Collision constraint that prevents a particle from penetrating a plane.
10#[derive(Debug, Clone)]
11pub struct CollisionConstraint {
12    /// Index of the particle.
13    pub particle_index: usize,
14    /// Plane normal (pointing away from the solid side).
15    pub normal: Vec3,
16    /// A point on the plane.
17    pub point_on_plane: Vec3,
18}
19impl CollisionConstraint {
20    /// Create a new plane-collision constraint.
21    pub fn new(particle_index: usize, normal: Vec3, point_on_plane: Vec3) -> Self {
22        Self {
23            particle_index,
24            normal,
25            point_on_plane,
26        }
27    }
28}
29/// Long Range Attachment constraint (Kim et al. 2012).
30///
31/// Prevents cloth from stretching beyond a maximum length from a fixed anchor,
32/// while allowing free motion within the rest length. Only corrects when the
33/// particle is too far from the anchor.
34#[derive(Debug, Clone)]
35pub struct LraConstraint {
36    /// Index of the particle to constrain.
37    pub particle: usize,
38    /// World-space anchor position.
39    pub anchor: Vec3,
40    /// Maximum allowed distance from the anchor.
41    pub max_distance: Real,
42    /// Compliance (XPBD).
43    pub compliance: Real,
44    /// Accumulated Lagrange multiplier.
45    pub(super) lambda: Real,
46}
47impl LraConstraint {
48    /// Create a new LRA constraint.
49    pub fn new(particle: usize, anchor: Vec3, max_distance: Real, compliance: Real) -> Self {
50        Self {
51            particle,
52            anchor,
53            max_distance,
54            compliance,
55            lambda: 0.0,
56        }
57    }
58    /// Build from current particle position (uses current distance as max).
59    pub fn from_particle(
60        particle: usize,
61        particles: &[SoftParticle],
62        anchor: Vec3,
63        compliance: Real,
64    ) -> Self {
65        let dist = (particles[particle].position - anchor).norm();
66        Self::new(particle, anchor, dist, compliance)
67    }
68    /// Reset Lagrange multiplier.
69    pub fn reset_lambda(&mut self) {
70        self.lambda = 0.0;
71    }
72    /// Evaluate constraint violation (positive when too far).
73    pub fn constraint_value(&self, particles: &[SoftParticle]) -> Real {
74        let dist = (particles[self.particle].position - self.anchor).norm();
75        (dist - self.max_distance).max(0.0)
76    }
77}
78/// Surface tension constraint for cloth simulation.
79///
80/// Minimises local area to simulate surface tension effects. Models cloth
81/// wrinkling resistance and thin-film surface energy.
82#[derive(Debug, Clone)]
83pub struct SurfaceTensionConstraint {
84    /// Triangle vertex indices.
85    pub indices: [usize; 3],
86    /// Rest area of this triangle.
87    pub rest_area: Real,
88    /// Surface tension coefficient γ (N/m).
89    pub gamma: Real,
90    /// Compliance.
91    pub compliance: Real,
92    /// Accumulated Lagrange multiplier.
93    pub(super) lambda: Real,
94}
95impl SurfaceTensionConstraint {
96    /// Create from current particle positions.
97    pub fn from_particles(
98        indices: [usize; 3],
99        particles: &[SoftParticle],
100        gamma: Real,
101        compliance: Real,
102    ) -> Self {
103        let p0 = particles[indices[0]].position;
104        let p1 = particles[indices[1]].position;
105        let p2 = particles[indices[2]].position;
106        let rest_area = 0.5 * (p1 - p0).cross(&(p2 - p0)).norm();
107        Self {
108            indices,
109            rest_area,
110            gamma,
111            compliance,
112            lambda: 0.0,
113        }
114    }
115    /// Reset Lagrange multiplier.
116    pub fn reset_lambda(&mut self) {
117        self.lambda = 0.0;
118    }
119    /// Current triangle area.
120    pub fn current_area(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Real {
121        0.5 * (p1 - p0).cross(&(p2 - p0)).norm()
122    }
123    /// Surface energy: E = γ * (A - A0).
124    pub fn surface_energy(&self, particles: &[SoftParticle]) -> Real {
125        let [i0, i1, i2] = self.indices;
126        let area = Self::current_area(
127            &particles[i0].position,
128            &particles[i1].position,
129            &particles[i2].position,
130        );
131        self.gamma * (area - self.rest_area)
132    }
133}
134/// Shape matching constraint (Müller et al. 2005).
135///
136/// Preserves the shape of a group of particles by computing the optimal
137/// rotation from the rest shape to the current configuration.
138#[derive(Debug, Clone)]
139pub struct ShapeMatchingConstraint {
140    /// Indices of the particles in this shape.
141    pub indices: Vec<usize>,
142    /// Rest positions relative to center of mass.
143    pub rest_relative: Vec<Vec3>,
144    /// Rest center of mass.
145    pub rest_com: Vec3,
146    /// Stiffness (0-1, where 1 is rigid).
147    pub stiffness: Real,
148}
149impl ShapeMatchingConstraint {
150    /// Create a shape matching constraint from the current configuration.
151    pub fn from_particles(
152        indices: Vec<usize>,
153        particles: &[SoftParticle],
154        stiffness: Real,
155    ) -> Self {
156        let n = indices.len();
157        if n == 0 {
158            return Self {
159                indices,
160                rest_relative: Vec::new(),
161                rest_com: Vec3::zeros(),
162                stiffness,
163            };
164        }
165        let total_inv_mass: Real = indices
166            .iter()
167            .map(|&i| {
168                if particles[i].inverse_mass > 0.0 {
169                    1.0 / particles[i].inverse_mass
170                } else {
171                    1e6
172                }
173            })
174            .sum();
175        let mut com = Vec3::zeros();
176        for &i in &indices {
177            let mass = if particles[i].inverse_mass > 0.0 {
178                1.0 / particles[i].inverse_mass
179            } else {
180                1e6
181            };
182            com += particles[i].position * mass;
183        }
184        com /= total_inv_mass;
185        let rest_relative: Vec<Vec3> = indices
186            .iter()
187            .map(|&i| particles[i].position - com)
188            .collect();
189        Self {
190            indices,
191            rest_relative,
192            rest_com: com,
193            stiffness,
194        }
195    }
196    /// Compute current center of mass.
197    pub(super) fn current_com(&self, particles: &[SoftParticle]) -> Vec3 {
198        let n = self.indices.len();
199        if n == 0 {
200            return Vec3::zeros();
201        }
202        let mut total_mass = 0.0_f64;
203        let mut com = Vec3::zeros();
204        for &i in &self.indices {
205            let mass = if particles[i].inverse_mass > 0.0 {
206                1.0 / particles[i].inverse_mass
207            } else {
208                1e6
209            };
210            com += particles[i].position * mass;
211            total_mass += mass;
212        }
213        if total_mass > 1e-14 {
214            com / total_mass
215        } else {
216            Vec3::zeros()
217        }
218    }
219    /// Compute the optimal rotation using the polar decomposition
220    /// via cross-covariance matrix.
221    ///
222    /// Returns the rotation matrix as a 3x3 array (row-major).
223    pub(super) fn compute_rotation(&self, particles: &[SoftParticle]) -> [Real; 9] {
224        let com = self.current_com(particles);
225        let mut a = [0.0_f64; 9];
226        for (k, &idx) in self.indices.iter().enumerate() {
227            let mass = if particles[idx].inverse_mass > 0.0 {
228                1.0 / particles[idx].inverse_mass
229            } else {
230                1e6
231            };
232            let p = particles[idx].position - com;
233            let q = &self.rest_relative[k];
234            a[0] += mass * p.x * q.x;
235            a[1] += mass * p.x * q.y;
236            a[2] += mass * p.x * q.z;
237            a[3] += mass * p.y * q.x;
238            a[4] += mass * p.y * q.y;
239            a[5] += mass * p.y * q.z;
240            a[6] += mass * p.z * q.x;
241            a[7] += mass * p.z * q.y;
242            a[8] += mass * p.z * q.z;
243        }
244        let col0 = Vec3::new(a[0], a[3], a[6]);
245        let col1_raw = Vec3::new(a[1], a[4], a[7]);
246        let col2_raw = Vec3::new(a[2], a[5], a[8]);
247        let len0 = col0.norm();
248        if len0 < 1e-14 {
249            return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
250        }
251        let e0 = col0 / len0;
252        let proj1 = col1_raw - e0 * e0.dot(&col1_raw);
253        let len1 = proj1.norm();
254        if len1 < 1e-14 {
255            return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
256        }
257        let e1 = proj1 / len1;
258        let e2_raw = col2_raw - e0 * e0.dot(&col2_raw) - e1 * e1.dot(&col2_raw);
259        let len2 = e2_raw.norm();
260        if len2 < 1e-14 {
261            return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
262        }
263        let e2 = e2_raw / len2;
264        let det_sign = e0.dot(&e1.cross(&e2));
265        let e2 = if det_sign < 0.0 { -e2 } else { e2 };
266        [e0.x, e1.x, e2.x, e0.y, e1.y, e2.y, e0.z, e1.z, e2.z]
267    }
268}
269/// Strain limiting constraint for a triangle element.
270///
271/// Limits the maximum stretch and compression of a triangle to prevent
272/// excessive deformation. Based on principal strain analysis.
273#[derive(Debug, Clone)]
274pub struct StrainLimitingConstraint {
275    /// Indices of the three vertices.
276    pub indices: [usize; 3],
277    /// Maximum allowed stretch ratio (e.g. 1.1 for 10% stretch).
278    pub max_stretch: Real,
279    /// Minimum allowed compression ratio (e.g. 0.9 for 10% compression).
280    pub min_compression: Real,
281    /// Inverse rest transform (2x2 stored as \[a, b, c, d\]).
282    pub(super) inv_rest: [Real; 4],
283    /// Compliance (XPBD).
284    pub compliance: Real,
285    /// Accumulated Lagrange multiplier.
286    pub(super) lambda: Real,
287}
288impl StrainLimitingConstraint {
289    /// Create a strain limiting constraint from the current configuration.
290    pub fn from_particles(
291        indices: [usize; 3],
292        particles: &[SoftParticle],
293        max_stretch: Real,
294        min_compression: Real,
295        compliance: Real,
296    ) -> Self {
297        let p0 = particles[indices[0]].position;
298        let p1 = particles[indices[1]].position;
299        let p2 = particles[indices[2]].position;
300        let inv_rest = Self::compute_inv_rest(&p0, &p1, &p2);
301        Self {
302            indices,
303            max_stretch,
304            min_compression,
305            inv_rest,
306            compliance,
307            lambda: 0.0,
308        }
309    }
310    /// Reset the Lagrange multiplier.
311    pub fn reset_lambda(&mut self) {
312        self.lambda = 0.0;
313    }
314    /// Compute the inverse of the 2D rest matrix.
315    fn compute_inv_rest(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> [Real; 4] {
316        let d1 = p1 - p0;
317        let d2 = p2 - p0;
318        let e1 = d1;
319        let e1_len = e1.norm();
320        if e1_len < 1e-14 {
321            return [1.0, 0.0, 0.0, 1.0];
322        }
323        let u = e1 / e1_len;
324        let normal = d1.cross(&d2);
325        let n_len = normal.norm();
326        if n_len < 1e-14 {
327            return [1.0, 0.0, 0.0, 1.0];
328        }
329        let v = normal.cross(&d1).normalize();
330        let a = d1.dot(&u);
331        let b = d1.dot(&v);
332        let c = d2.dot(&u);
333        let d = d2.dot(&v);
334        let det = a * d - b * c;
335        if det.abs() < 1e-14 {
336            return [1.0, 0.0, 0.0, 1.0];
337        }
338        [d / det, -c / det, -b / det, a / det]
339    }
340}
341/// Neo-Hookean strain energy constraint for a tetrahedron (FEM-based XPBD).
342///
343/// Uses the linearised Neo-Hookean model:
344///   Ψ = (mu/2)(I1 - 3) + (lambda/2)(J-1)^2
345/// where I1 = tr(F^T F), J = det(F), and F is the deformation gradient.
346///
347/// Indices: \[i0, i1, i2, i3\] — tet vertices.
348#[derive(Debug, Clone)]
349pub struct NeoHookeanConstraint {
350    /// Indices of the four tet vertices.
351    pub indices: [usize; 4],
352    /// Shear modulus (mu).
353    pub mu: Real,
354    /// First Lamé parameter (lambda).
355    pub lambda_lame: Real,
356    /// Compliance for volume part.
357    pub compliance: Real,
358    /// Inverse rest-shape matrix Dm^-1 (stored as 9 entries, column-major 3x3).
359    pub(super) dm_inv: [Real; 9],
360    /// Rest volume of tetrahedron.
361    pub rest_volume: Real,
362    /// Accumulated Lagrange multiplier (deviatoric).
363    pub(super) lambda_dev: Real,
364    /// Accumulated Lagrange multiplier (hydrostatic).
365    pub(super) lambda_vol: Real,
366}
367impl NeoHookeanConstraint {
368    /// Build constraint from current particle positions.
369    pub fn from_particles(
370        indices: [usize; 4],
371        particles: &[SoftParticle],
372        mu: Real,
373        lambda_lame: Real,
374        compliance: Real,
375    ) -> Self {
376        let p0 = particles[indices[0]].position;
377        let p1 = particles[indices[1]].position;
378        let p2 = particles[indices[2]].position;
379        let p3 = particles[indices[3]].position;
380        let dm = Self::compute_shape_matrix(&p0, &p1, &p2, &p3);
381        let dm_inv = mat3_inverse(dm).unwrap_or([0.0; 9]);
382        let rest_volume = VolumeConstraint::compute_tet_volume(&p0, &p1, &p2, &p3).abs();
383        Self {
384            indices,
385            mu,
386            lambda_lame,
387            compliance,
388            dm_inv,
389            rest_volume,
390            lambda_dev: 0.0,
391            lambda_vol: 0.0,
392        }
393    }
394    /// Reset Lagrange multipliers.
395    pub fn reset_lambda(&mut self) {
396        self.lambda_dev = 0.0;
397        self.lambda_vol = 0.0;
398    }
399    /// Compute deformation gradient F = Ds * Dm_inv.
400    pub fn deformation_gradient(&self, particles: &[SoftParticle]) -> [Real; 9] {
401        let [i0, i1, i2, i3] = self.indices;
402        let p0 = particles[i0].position;
403        let p1 = particles[i1].position;
404        let p2 = particles[i2].position;
405        let p3 = particles[i3].position;
406        let ds = Self::compute_shape_matrix(&p0, &p1, &p2, &p3);
407        mat3_mul(ds, self.dm_inv)
408    }
409    /// Compute I1 = tr(F^T F).
410    pub fn compute_i1(f: [Real; 9]) -> Real {
411        f.iter().map(|&x| x * x).sum()
412    }
413    /// Compute J = det(F).
414    pub fn compute_j(f: [Real; 9]) -> Real {
415        mat3_det(f)
416    }
417    fn compute_shape_matrix(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> [Real; 9] {
418        let c0 = *p1 - *p0;
419        let c1 = *p2 - *p0;
420        let c2 = *p3 - *p0;
421        [c0.x, c0.y, c0.z, c1.x, c1.y, c1.z, c2.x, c2.y, c2.z]
422    }
423}
424/// Area conservation constraint for a single triangle.
425///
426/// Preserves the area of a triangle formed by three particles.
427#[derive(Debug, Clone)]
428pub struct AreaConstraint {
429    /// Indices of the three vertices.
430    pub indices: [usize; 3],
431    /// Rest area.
432    pub rest_area: Real,
433    /// Compliance (XPBD).
434    pub compliance: Real,
435    /// Accumulated Lagrange multiplier.
436    pub(super) lambda: Real,
437}
438impl AreaConstraint {
439    /// Create an area constraint with an explicit rest area.
440    pub fn new(indices: [usize; 3], rest_area: Real, compliance: Real) -> Self {
441        Self {
442            indices,
443            rest_area,
444            compliance,
445            lambda: 0.0,
446        }
447    }
448    /// Build an area constraint from the current configuration.
449    pub fn from_particles(
450        indices: [usize; 3],
451        particles: &[SoftParticle],
452        compliance: Real,
453    ) -> Self {
454        let area = Self::compute_triangle_area(
455            &particles[indices[0]].position,
456            &particles[indices[1]].position,
457            &particles[indices[2]].position,
458        );
459        Self::new(indices, area, compliance)
460    }
461    /// Reset the Lagrange multiplier.
462    pub fn reset_lambda(&mut self) {
463        self.lambda = 0.0;
464    }
465    /// Compute the area of a triangle.
466    pub(super) fn compute_triangle_area(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Real {
467        let e1 = p1 - p0;
468        let e2 = p2 - p0;
469        0.5 * e1.cross(&e2).norm()
470    }
471}
472/// Binding between a soft-body particle and a rigid-body anchor point.
473///
474/// Pulls the particle towards a world-space point that may be updated each
475/// frame (e.g. driven by a rigid body transform).
476#[derive(Debug, Clone)]
477pub struct RigidBindingConstraint {
478    /// Particle index.
479    pub particle: usize,
480    /// World-space target position (updated externally).
481    pub target: Vec3,
482    /// Compliance (0 = hard attachment).
483    pub compliance: Real,
484    /// Accumulated Lagrange multiplier.
485    pub(super) lambda: Real,
486}
487impl RigidBindingConstraint {
488    /// Create a new rigid binding constraint.
489    pub fn new(particle: usize, target: Vec3, compliance: Real) -> Self {
490        Self {
491            particle,
492            target,
493            compliance,
494            lambda: 0.0,
495        }
496    }
497    /// Reset Lagrange multiplier.
498    pub fn reset_lambda(&mut self) {
499        self.lambda = 0.0;
500    }
501    /// Update the target position (call before each solver iteration).
502    pub fn set_target(&mut self, target: Vec3) {
503        self.target = target;
504    }
505    /// Residual (distance to target).
506    pub fn residual(&self, particles: &[SoftParticle]) -> Real {
507        (particles[self.particle].position - self.target).norm()
508    }
509}
510/// Motor constraint for soft actuators.
511///
512/// Drives a pair of particles towards a target length (like a muscle fibre).
513/// Uses XPBD with a target rest length that can be set dynamically.
514#[derive(Debug, Clone)]
515pub struct MotorConstraint {
516    /// First particle index.
517    pub i: usize,
518    /// Second particle index.
519    pub j: usize,
520    /// Current target length (driven by actuation signal).
521    pub target_length: Real,
522    /// Natural rest length (fully relaxed).
523    pub rest_length: Real,
524    /// Maximum contraction ratio (e.g. 0.6 means 60% of rest).
525    pub min_ratio: Real,
526    /// Compliance.
527    pub compliance: Real,
528    /// Current actuation in \[0, 1\]: 0 = relaxed, 1 = fully contracted.
529    pub actuation: Real,
530    /// Accumulated Lagrange multiplier.
531    pub(super) lambda: Real,
532}
533impl MotorConstraint {
534    /// Create a new motor constraint.
535    pub fn new(i: usize, j: usize, rest_length: Real, min_ratio: Real, compliance: Real) -> Self {
536        Self {
537            i,
538            j,
539            target_length: rest_length,
540            rest_length,
541            min_ratio,
542            compliance,
543            actuation: 0.0,
544            lambda: 0.0,
545        }
546    }
547    /// Build from current particle distance.
548    pub fn from_particles(
549        i: usize,
550        j: usize,
551        particles: &[SoftParticle],
552        min_ratio: Real,
553        compliance: Real,
554    ) -> Self {
555        let rest = (particles[i].position - particles[j].position).norm();
556        Self::new(i, j, rest, min_ratio, compliance)
557    }
558    /// Reset Lagrange multiplier.
559    pub fn reset_lambda(&mut self) {
560        self.lambda = 0.0;
561    }
562    /// Set the actuation signal \[0, 1\] and update target length.
563    pub fn set_actuation(&mut self, actuation: Real) {
564        self.actuation = actuation.clamp(0.0, 1.0);
565        let min_len = self.rest_length * self.min_ratio;
566        self.target_length = self.rest_length - (self.rest_length - min_len) * self.actuation;
567    }
568    /// Current contraction ratio.
569    pub fn contraction_ratio(&self) -> Real {
570        if self.rest_length < 1e-12 {
571            return 1.0;
572        }
573        self.target_length / self.rest_length
574    }
575}
576/// Volume-preservation constraint for a tetrahedral mesh.
577///
578/// Operates over all tetrahedra whose indices are stored in the constraint.
579#[derive(Debug, Clone)]
580pub struct VolumeConstraint {
581    /// Indices of the four vertices forming the tetrahedron.
582    pub indices: [usize; 4],
583    /// Rest volume (positive).
584    pub rest_volume: Real,
585    /// Compliance (XPBD).
586    pub compliance: Real,
587    /// Accumulated Lagrange multiplier.
588    pub(super) lambda: Real,
589}
590impl VolumeConstraint {
591    /// Create a volume constraint with an explicit rest volume.
592    pub fn new(indices: [usize; 4], rest_volume: Real, compliance: Real) -> Self {
593        Self {
594            indices,
595            rest_volume,
596            compliance,
597            lambda: 0.0,
598        }
599    }
600    /// Build a volume constraint whose rest volume matches the current
601    /// configuration.
602    pub fn from_particles(
603        indices: [usize; 4],
604        particles: &[SoftParticle],
605        compliance: Real,
606    ) -> Self {
607        let vol = Self::compute_tet_volume(
608            &particles[indices[0]].position,
609            &particles[indices[1]].position,
610            &particles[indices[2]].position,
611            &particles[indices[3]].position,
612        );
613        Self::new(indices, vol, compliance)
614    }
615    /// Reset the Lagrange multiplier.
616    pub fn reset_lambda(&mut self) {
617        self.lambda = 0.0;
618    }
619    /// Signed volume of a tetrahedron.
620    pub fn compute_tet_volume(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> Real {
621        let a = p1 - p0;
622        let b = p2 - p0;
623        let c = p3 - p0;
624        a.dot(&b.cross(&c)) / 6.0
625    }
626}
627/// XPBD distance constraint between two particles.
628///
629/// Maintains a rest length with optional compliance (softness).
630#[derive(Debug, Clone)]
631pub struct DistanceConstraint {
632    /// Index of the first particle.
633    pub i: usize,
634    /// Index of the second particle.
635    pub j: usize,
636    /// Rest length.
637    pub rest_length: Real,
638    /// Compliance (inverse stiffness). 0 = perfectly rigid.
639    pub compliance: Real,
640    /// Accumulated Lagrange multiplier (reset each sub-step sequence).
641    pub(super) lambda: Real,
642}
643impl DistanceConstraint {
644    /// Create a new distance constraint.
645    pub fn new(i: usize, j: usize, rest_length: Real, compliance: Real) -> Self {
646        Self {
647            i,
648            j,
649            rest_length,
650            compliance,
651            lambda: 0.0,
652        }
653    }
654    /// Build a constraint whose rest length is the current distance between the
655    /// two particles.
656    pub fn from_particles(
657        i: usize,
658        j: usize,
659        particles: &[SoftParticle],
660        compliance: Real,
661    ) -> Self {
662        let rest = (particles[i].position - particles[j].position).norm();
663        Self::new(i, j, rest, compliance)
664    }
665    /// Reset the Lagrange multiplier (call before a new solver iteration
666    /// sequence).
667    pub fn reset_lambda(&mut self) {
668        self.lambda = 0.0;
669    }
670}
671/// Isometric bending constraint (Bergou et al. 2006).
672///
673/// Uses a quadratic energy based on the Laplacian of the surface positions,
674/// which is more robust than dihedral-angle bending for small angles.
675///
676/// For two triangles sharing edge (p0,p1) with opposite vertices p2,p3:
677/// `C = (p0*Q[0] + p1*Q[1] + p2*Q[2] + p3*Q[3])`
678/// where Q are cotangent-weighted coefficients.
679#[derive(Debug, Clone)]
680pub struct IsometricBendingConstraint {
681    /// Indices of the four particles: shared edge (i0, i1), wing vertices (i2, i3).
682    pub indices: [usize; 4],
683    /// Stiffness coefficient for bending.
684    pub stiffness: Real,
685    /// Compliance (XPBD).
686    pub compliance: Real,
687    /// Rest state matrix Q (4x4 symmetric, stored as flat array).
688    pub(super) q_matrix: [Real; 16],
689    /// Accumulated Lagrange multiplier.
690    pub(super) lambda: Real,
691}
692impl IsometricBendingConstraint {
693    /// Create an isometric bending constraint from the current configuration.
694    pub fn from_particles(
695        indices: [usize; 4],
696        particles: &[SoftParticle],
697        stiffness: Real,
698        compliance: Real,
699    ) -> Self {
700        let p0 = particles[indices[0]].position;
701        let p1 = particles[indices[1]].position;
702        let p2 = particles[indices[2]].position;
703        let p3 = particles[indices[3]].position;
704        let q_matrix = Self::compute_q_matrix(&p0, &p1, &p2, &p3);
705        Self {
706            indices,
707            stiffness,
708            compliance,
709            q_matrix,
710            lambda: 0.0,
711        }
712    }
713    /// Reset the Lagrange multiplier.
714    pub fn reset_lambda(&mut self) {
715        self.lambda = 0.0;
716    }
717    /// Compute the Q matrix from cotangent weights.
718    ///
719    /// Uses the Bergou et al. formulation where the constraint is based on
720    /// the discrete Laplacian. The key idea is that the constraint vector
721    /// K = \[c03+c06, c02+c05, -c01, -c04\] satisfies K.p = 0 at rest
722    /// (when the surface is flat), and Q = K^T K / (4A) encodes the energy.
723    fn compute_q_matrix(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> [Real; 16] {
724        let e = p1 - p0;
725        let e0_2 = p2 - p0;
726        let e1_2 = p2 - p1;
727        let e0_3 = p3 - p0;
728        let e1_3 = p3 - p1;
729        let cot = |a: &Vec3, b: &Vec3| -> Real {
730            let cross_norm = a.cross(b).norm();
731            if cross_norm < 1e-14 {
732                return 0.0;
733            }
734            a.dot(b) / cross_norm
735        };
736        let c01 = cot(&e0_2, &e1_2);
737        let c02 = cot(&e, &e0_2);
738        let c03 = cot(&(-e), &e1_2);
739        let c04 = cot(&e0_3, &e1_3);
740        let c05 = cot(&e, &e0_3);
741        let c06 = cot(&(-e), &e1_3);
742        let a1 = 0.5 * e0_2.cross(&e).norm();
743        let a2 = 0.5 * e0_3.cross(&e).norm();
744        let a_total = a1 + a2;
745        if a_total < 1e-14 {
746            return [0.0; 16];
747        }
748        let kk = [c03 + c06, c02 + c05, -c01, -c04];
749        let mut q = [0.0_f64; 16];
750        for row in 0..4 {
751            for col in 0..4 {
752                q[row * 4 + col] = kk[row] * kk[col] / (4.0 * a_total);
753            }
754        }
755        q
756    }
757}
758/// Improved isometric bending constraint that applies gradients to all four
759/// vertices (shared edge + two wing vertices), not just the wings.
760///
761/// Uses the quadratic form from Bergou et al. 2006 for better convergence.
762#[derive(Debug, Clone)]
763pub struct IsometricBendingConstraintV2 {
764    /// \[i0, i1, i2, i3\]: shared edge (i0,i1), wing vertices (i2,i3).
765    pub indices: [usize; 4],
766    /// Bending stiffness.
767    pub stiffness: Real,
768    /// Compliance.
769    pub compliance: Real,
770    /// 4×4 Q matrix (flat, row-major).
771    pub(super) q_matrix: [Real; 16],
772    /// Accumulated Lagrange multiplier.
773    pub(super) lambda: Real,
774}
775impl IsometricBendingConstraintV2 {
776    /// Build from current positions.
777    pub fn from_particles(
778        indices: [usize; 4],
779        particles: &[SoftParticle],
780        stiffness: Real,
781        compliance: Real,
782    ) -> Self {
783        let p = [
784            particles[indices[0]].position,
785            particles[indices[1]].position,
786            particles[indices[2]].position,
787            particles[indices[3]].position,
788        ];
789        let q_matrix = Self::build_q(&p[0], &p[1], &p[2], &p[3]);
790        Self {
791            indices,
792            stiffness,
793            compliance,
794            q_matrix,
795            lambda: 0.0,
796        }
797    }
798    /// Reset Lagrange multiplier.
799    pub fn reset_lambda(&mut self) {
800        self.lambda = 0.0;
801    }
802    /// Quadratic bending energy: E = (stiffness/2) * sum_i p_i^T Q p_i.
803    pub fn bending_energy(&self, particles: &[SoftParticle]) -> Real {
804        let ps: Vec<Vec3> = self
805            .indices
806            .iter()
807            .map(|&i| particles[i].position)
808            .collect();
809        let mut energy = 0.0;
810        for axis in 0..3 {
811            let coords: [Real; 4] = std::array::from_fn(|k| match axis {
812                0 => ps[k].x,
813                1 => ps[k].y,
814                _ => ps[k].z,
815            });
816            for i in 0..4 {
817                for j in 0..4 {
818                    energy += self.q_matrix[i * 4 + j] * coords[i] * coords[j];
819                }
820            }
821        }
822        0.5 * self.stiffness * energy
823    }
824    fn cot_angle(a: &Vec3, b: &Vec3) -> Real {
825        let cross = a.cross(b).norm();
826        if cross < 1e-14 {
827            return 0.0;
828        }
829        a.dot(b) / cross
830    }
831    fn build_q(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> [Real; 16] {
832        let e0 = *p2 - *p0;
833        let e1 = *p2 - *p1;
834        let e2 = *p3 - *p0;
835        let e3 = *p3 - *p1;
836        let c01 = Self::cot_angle(&e0, &e1);
837        let c02 = Self::cot_angle(&e2, &e3);
838        let k = [c01 + c02, -(c01 + c02), c01, -c01 + c02 - c02, c02, -c02];
839        let coeff = [k[0], k[1], k[2], k[3]];
840        let mut q = [0.0f64; 16];
841        for i in 0..4 {
842            for j in 0..4 {
843                q[i * 4 + j] = coeff[i] * coeff[j];
844            }
845        }
846        q
847    }
848}
849/// Balloon pressure constraint for inflatable objects.
850///
851/// Enforces a target enclosed volume by applying outward pressure on the
852/// surface triangles. Indices specify a surface triangle (i0, i1, i2).
853#[derive(Debug, Clone)]
854pub struct BalloonPressureConstraint {
855    /// Surface triangle indices.
856    pub indices: [usize; 3],
857    /// Target pressure (multiplied by current volume deviation).
858    pub pressure: Real,
859    /// Rest volume reference (of the entire balloon).
860    pub rest_volume: Real,
861    /// Compliance.
862    pub compliance: Real,
863    /// Accumulated Lagrange multiplier.
864    pub(super) lambda: Real,
865}
866impl BalloonPressureConstraint {
867    /// Create a new balloon pressure constraint.
868    pub fn new(indices: [usize; 3], pressure: Real, rest_volume: Real, compliance: Real) -> Self {
869        Self {
870            indices,
871            pressure,
872            rest_volume,
873            compliance,
874            lambda: 0.0,
875        }
876    }
877    /// Reset Lagrange multiplier.
878    pub fn reset_lambda(&mut self) {
879        self.lambda = 0.0;
880    }
881    /// Compute signed triangle area contribution to enclosed volume.
882    /// Divergence theorem: V = (1/6) * sum over faces of (p0 · (p1 × p2)).
883    pub fn triangle_volume_contribution(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Real {
884        p0.dot(&p1.cross(p2)) / 6.0
885    }
886    /// Outward normal of the triangle (unnormalized, magnitude = area).
887    pub(super) fn triangle_normal_area(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Vec3 {
888        (p1 - p0).cross(&(p2 - p0))
889    }
890}
891/// Dihedral-angle bending constraint for two triangles sharing an edge.
892///
893/// Vertices (p0, p1) are the shared edge; p2 and p3 are the opposing vertices.
894#[derive(Debug, Clone)]
895pub struct BendingConstraint {
896    /// Indices of the four particles: shared edge (i0, i1), wing vertices (i2, i3).
897    pub indices: [usize; 4],
898    /// Rest dihedral angle.
899    pub rest_angle: Real,
900    /// Compliance (XPBD).
901    pub compliance: Real,
902    /// Accumulated Lagrange multiplier.
903    pub(super) lambda: Real,
904}
905impl BendingConstraint {
906    /// Create a bending constraint with the given rest angle.
907    pub fn new(indices: [usize; 4], rest_angle: Real, compliance: Real) -> Self {
908        Self {
909            indices,
910            rest_angle,
911            compliance,
912            lambda: 0.0,
913        }
914    }
915    /// Build a bending constraint whose rest angle matches the current
916    /// configuration.
917    pub fn from_particles(
918        indices: [usize; 4],
919        particles: &[SoftParticle],
920        compliance: Real,
921    ) -> Self {
922        let angle = Self::compute_dihedral(
923            &particles[indices[0]].position,
924            &particles[indices[1]].position,
925            &particles[indices[2]].position,
926            &particles[indices[3]].position,
927        );
928        Self::new(indices, angle, compliance)
929    }
930    /// Reset the Lagrange multiplier.
931    pub fn reset_lambda(&mut self) {
932        self.lambda = 0.0;
933    }
934    /// Compute the dihedral angle between two triangles sharing edge (p0,p1).
935    pub(super) fn compute_dihedral(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> Real {
936        let e = p1 - p0;
937        let n1 = (p2 - p0).cross(&e);
938        let n2 = (p3 - p0).cross(&e);
939        let n1_len = n1.norm();
940        let n2_len = n2.norm();
941        if n1_len < 1e-12 || n2_len < 1e-12 {
942            return 0.0;
943        }
944        let n1 = n1 / n1_len;
945        let n2 = n2 / n2_len;
946        let cos_a = n1.dot(&n2).clamp(-1.0, 1.0);
947        cos_a.acos()
948    }
949}