Skip to main content

oxihuman_export/
cloth_sim_state_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// A snapshot of a cloth simulation particle.
6#[allow(dead_code)]
7#[derive(Clone)]
8pub struct ClothParticleState {
9    pub position: [f32; 3],
10    pub velocity: [f32; 3],
11    pub pinned: bool,
12}
13
14/// A cloth simulation state export.
15#[allow(dead_code)]
16pub struct ClothSimStateExport {
17    pub frame: usize,
18    pub time: f32,
19    pub particles: Vec<ClothParticleState>,
20}
21
22/// Create a new cloth sim state.
23#[allow(dead_code)]
24pub fn new_cloth_sim_state(frame: usize, time: f32) -> ClothSimStateExport {
25    ClothSimStateExport {
26        frame,
27        time,
28        particles: Vec::new(),
29    }
30}
31
32/// Add a particle.
33#[allow(dead_code)]
34pub fn add_cloth_particle_state(
35    export: &mut ClothSimStateExport,
36    pos: [f32; 3],
37    vel: [f32; 3],
38    pinned: bool,
39) {
40    export.particles.push(ClothParticleState {
41        position: pos,
42        velocity: vel,
43        pinned,
44    });
45}
46
47/// Count particles.
48#[allow(dead_code)]
49pub fn particle_count_css(export: &ClothSimStateExport) -> usize {
50    export.particles.len()
51}
52
53/// Count pinned particles.
54#[allow(dead_code)]
55pub fn pinned_count_css(export: &ClothSimStateExport) -> usize {
56    export.particles.iter().filter(|p| p.pinned).count()
57}
58
59/// Average speed of particles.
60#[allow(dead_code)]
61pub fn avg_speed_css(export: &ClothSimStateExport) -> f32 {
62    if export.particles.is_empty() {
63        return 0.0;
64    }
65    let sum: f32 = export
66        .particles
67        .iter()
68        .map(|p| {
69            let v = p.velocity;
70            (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
71        })
72        .sum();
73    sum / export.particles.len() as f32
74}
75
76/// Max speed.
77#[allow(dead_code)]
78pub fn max_speed_css(export: &ClothSimStateExport) -> f32 {
79    export
80        .particles
81        .iter()
82        .map(|p| {
83            let v = p.velocity;
84            (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
85        })
86        .fold(0.0_f32, f32::max)
87}
88
89/// Flatten positions to flat array.
90#[allow(dead_code)]
91pub fn flat_positions_css(export: &ClothSimStateExport) -> Vec<f32> {
92    export.particles.iter().flat_map(|p| p.position).collect()
93}
94
95/// Serialize to JSON.
96#[allow(dead_code)]
97pub fn cloth_sim_state_to_json(export: &ClothSimStateExport) -> String {
98    format!(
99        r#"{{"frame":{},"time":{:.4},"particles":{},"pinned":{}}}"#,
100        export.frame,
101        export.time,
102        export.particles.len(),
103        pinned_count_css(export)
104    )
105}
106
107/// Validate particle count matches expected.
108#[allow(dead_code)]
109pub fn validate_cloth_sim_state(export: &ClothSimStateExport, expected: usize) -> bool {
110    export.particles.len() == expected
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn add_and_count() {
119        let mut e = new_cloth_sim_state(0, 0.0);
120        add_cloth_particle_state(&mut e, [0.0; 3], [0.0; 3], false);
121        assert_eq!(particle_count_css(&e), 1);
122    }
123
124    #[test]
125    fn pinned_count() {
126        let mut e = new_cloth_sim_state(0, 0.0);
127        add_cloth_particle_state(&mut e, [0.0; 3], [0.0; 3], true);
128        add_cloth_particle_state(&mut e, [0.0; 3], [0.0; 3], false);
129        assert_eq!(pinned_count_css(&e), 1);
130    }
131
132    #[test]
133    fn avg_speed_zero() {
134        let mut e = new_cloth_sim_state(0, 0.0);
135        add_cloth_particle_state(&mut e, [0.0; 3], [0.0; 3], false);
136        assert!((avg_speed_css(&e) - 0.0).abs() < 1e-6);
137    }
138
139    #[test]
140    fn avg_speed_known() {
141        let mut e = new_cloth_sim_state(0, 0.0);
142        add_cloth_particle_state(&mut e, [0.0; 3], [3.0, 4.0, 0.0], false);
143        assert!((avg_speed_css(&e) - 5.0).abs() < 1e-5);
144    }
145
146    #[test]
147    fn max_speed() {
148        let mut e = new_cloth_sim_state(0, 0.0);
149        add_cloth_particle_state(&mut e, [0.0; 3], [1.0, 0.0, 0.0], false);
150        add_cloth_particle_state(&mut e, [0.0; 3], [3.0, 4.0, 0.0], false);
151        assert!((max_speed_css(&e) - 5.0).abs() < 1e-5);
152    }
153
154    #[test]
155    fn flat_positions_size() {
156        let mut e = new_cloth_sim_state(0, 0.0);
157        add_cloth_particle_state(&mut e, [1.0, 2.0, 3.0], [0.0; 3], false);
158        assert_eq!(flat_positions_css(&e).len(), 3);
159    }
160
161    #[test]
162    fn json_has_frame() {
163        let e = new_cloth_sim_state(5, 0.1);
164        let j = cloth_sim_state_to_json(&e);
165        assert!(j.contains("\"frame\":5"));
166    }
167
168    #[test]
169    fn validate_count() {
170        let mut e = new_cloth_sim_state(0, 0.0);
171        add_cloth_particle_state(&mut e, [0.0; 3], [0.0; 3], false);
172        assert!(validate_cloth_sim_state(&e, 1));
173    }
174
175    #[test]
176    fn empty_avg_speed() {
177        let e = new_cloth_sim_state(0, 0.0);
178        assert!((avg_speed_css(&e) - 0.0).abs() < 1e-6);
179    }
180
181    #[test]
182    fn time_stored() {
183        let e = new_cloth_sim_state(0, 1.5);
184        assert!((e.time - 1.5).abs() < 1e-4);
185    }
186}