1use glam::Vec3;
9use crate::math::springs::SpringDamper3;
10
11pub struct GlyphCohesion {
15 pub spring: SpringDamper3,
17 pub temperature: f32,
19 pub thermal_vel: Vec3,
21 pub drift: f32,
23 pub phase: f32,
25}
26
27impl GlyphCohesion {
28 pub fn new(slot_position: Vec3, cohesion_strength: f32, phase: f32) -> Self {
30 let (stiffness, damping) = cohesion_to_spring(cohesion_strength);
31 Self {
32 spring: SpringDamper3::from_vec3(slot_position, stiffness, damping),
33 temperature: 0.0,
34 thermal_vel: Vec3::ZERO,
35 drift: 0.0,
36 phase,
37 }
38 }
39
40 pub fn tick(&mut self, dt: f32, cohesion: f32) -> Vec3 {
44 let (stiffness, damping) = cohesion_to_spring(cohesion);
46 self.spring.x.stiffness = stiffness;
47 self.spring.x.damping = damping;
48 self.spring.y.stiffness = stiffness;
49 self.spring.y.damping = damping;
50 self.spring.z.stiffness = stiffness;
51 self.spring.z.damping = damping;
52
53 self.thermal_vel *= (1.0 - 4.0 * dt).max(0.0);
55
56 let base_pos = self.spring.tick(dt);
58
59 let target = Vec3::new(
61 self.spring.x.target,
62 self.spring.y.target,
63 self.spring.z.target,
64 );
65 self.drift = (base_pos - target).length();
66
67 base_pos + self.thermal_vel * self.temperature
69 }
70
71 pub fn heat(&mut self, temperature: f32, seed: f32) {
73 self.temperature = temperature.clamp(0.0, 1.0);
74 let jitter_vel = thermal_direction(seed + self.phase) * temperature * 0.8;
76 self.thermal_vel += jitter_vel;
77 }
78
79 pub fn cool(&mut self, rate: f32, dt: f32) {
81 self.temperature = (self.temperature - rate * dt).max(0.0);
82 }
83
84 pub fn set_target(&mut self, new_slot: Vec3) {
86 self.spring.set_target(new_slot);
87 }
88
89 pub fn teleport(&mut self, pos: Vec3) {
91 self.spring.x.position = pos.x;
92 self.spring.y.position = pos.y;
93 self.spring.z.position = pos.z;
94 self.spring.x.velocity = 0.0;
95 self.spring.y.velocity = 0.0;
96 self.spring.z.velocity = 0.0;
97 }
98
99 pub fn position(&self) -> Vec3 {
101 Vec3::new(self.spring.x.position, self.spring.y.position, self.spring.z.position)
102 }
103
104 pub fn velocity(&self) -> Vec3 {
106 Vec3::new(self.spring.x.velocity, self.spring.y.velocity, self.spring.z.velocity)
107 }
108
109 pub fn apply_impulse(&mut self, impulse: Vec3) {
111 self.spring.x.velocity += impulse.x;
112 self.spring.y.velocity += impulse.y;
113 self.spring.z.velocity += impulse.z;
114 }
115}
116
117pub struct CohesionManager {
121 pub glyphs: Vec<GlyphCohesion>,
122 pub cohesion: f32,
124 pub dissolution: Option<f32>,
126 pub burst_velocities: Vec<Vec3>,
128}
129
130impl CohesionManager {
131 pub fn new(positions: &[Vec3], cohesion: f32) -> Self {
133 let glyphs = positions
134 .iter()
135 .enumerate()
136 .map(|(i, &pos)| {
137 let phase = i as f32 * 1.618033988; GlyphCohesion::new(pos, cohesion, phase)
139 })
140 .collect();
141 Self {
142 glyphs,
143 cohesion,
144 dissolution: None,
145 burst_velocities: Vec::new(),
146 }
147 }
148
149 pub fn tick(&mut self, dt: f32) -> Vec<Vec3> {
151 if let Some(ref mut t) = self.dissolution {
152 *t += dt;
153 return self.glyphs
155 .iter_mut()
156 .zip(self.burst_velocities.iter())
157 .map(|(g, &bv)| {
158 let pos = g.position() + bv * dt;
159 g.spring.x.position = pos.x;
161 g.spring.y.position = pos.y;
162 g.spring.z.position = pos.z;
163 pos
164 })
165 .collect();
166 }
167
168 self.glyphs
169 .iter_mut()
170 .map(|g| g.tick(dt, self.cohesion))
171 .collect()
172 }
173
174 pub fn damage_cohesion(&mut self, amount: f32) {
176 self.cohesion = (self.cohesion - amount).max(0.0);
177 if self.cohesion == 0.0 && self.dissolution.is_none() {
178 self.begin_dissolution();
179 }
180 }
181
182 pub fn restore_cohesion(&mut self, amount: f32) {
184 self.cohesion = (self.cohesion + amount).min(1.0);
185 }
186
187 pub fn heat_all(&mut self, temperature: f32, time: f32) {
189 for (i, g) in self.glyphs.iter_mut().enumerate() {
190 g.heat(temperature, time + i as f32 * 0.37);
191 }
192 }
193
194 pub fn cool_all(&mut self, rate: f32, dt: f32) {
196 for g in &mut self.glyphs {
197 g.cool(rate, dt);
198 }
199 }
200
201 pub fn apply_shockwave(&mut self, center: Vec3, strength: f32) {
203 for g in &mut self.glyphs {
204 let dir = (g.position() - center).normalize_or_zero();
205 g.apply_impulse(dir * strength);
206 }
207 }
208
209 pub fn apply_force(&mut self, force: Vec3) {
211 for g in &mut self.glyphs {
212 g.apply_impulse(force);
213 }
214 }
215
216 pub fn update_targets(&mut self, new_positions: &[Vec3]) {
218 for (g, &pos) in self.glyphs.iter_mut().zip(new_positions.iter()) {
219 g.set_target(pos);
220 }
221 }
222
223 pub fn teleport_all(&mut self, positions: &[Vec3]) {
225 for (g, &pos) in self.glyphs.iter_mut().zip(positions.iter()) {
226 g.teleport(pos);
227 }
228 }
229
230 pub fn is_dissolving(&self) -> bool { self.dissolution.is_some() }
232
233 pub fn is_dissolved(&self) -> bool {
235 self.dissolution.map(|t| t > 2.0).unwrap_or(false)
236 }
237
238 fn begin_dissolution(&mut self) {
240 self.dissolution = Some(0.0);
241 let center = self.centroid();
242 self.burst_velocities = dissolution_burst(
243 &self.glyphs.iter().map(|g| g.position()).collect::<Vec<_>>(),
244 center,
245 );
246 }
247
248 pub fn centroid(&self) -> Vec3 {
250 if self.glyphs.is_empty() { return Vec3::ZERO; }
251 let sum: Vec3 = self.glyphs.iter().map(|g| g.position()).sum();
252 sum / self.glyphs.len() as f32
253 }
254
255 pub fn max_drift(&self) -> f32 {
257 self.glyphs.iter().map(|g| g.drift).fold(0.0f32, f32::max)
258 }
259
260 pub fn avg_temperature(&self) -> f32 {
262 if self.glyphs.is_empty() { return 0.0; }
263 self.glyphs.iter().map(|g| g.temperature).sum::<f32>() / self.glyphs.len() as f32
264 }
265}
266
267pub fn cohesion_to_spring(cohesion: f32) -> (f32, f32) {
274 let c = cohesion.clamp(0.0, 1.0);
275 let stiffness = 0.5 + c * c * 39.5; let damping = 0.3 + c * 7.7;
277 (stiffness, damping)
278}
279
280pub fn cohesion_pull(actual: Vec3, target: Vec3, cohesion: f32, dt: f32) -> Vec3 {
283 let delta = target - actual;
284 let stiffness = cohesion_to_spring(cohesion).0;
285 delta * stiffness * dt
286}
287
288pub fn dissolution_burst(positions: &[Vec3], center: Vec3) -> Vec<Vec3> {
291 positions.iter().enumerate().map(|(i, pos)| {
292 let dir = (*pos - center).normalize_or_zero();
293 let speed = 2.0 + rand_f32_seeded(i as u64) * 3.0;
294 let up_bias = Vec3::new(0.0, rand_f32_seeded(i as u64 + 1000) * 2.0, 0.0);
296 dir * speed + up_bias
297 }).collect()
298}
299
300fn thermal_direction(seed: f32) -> Vec3 {
302 let h1 = (seed * 127.1 + 311.7) as u64;
303 let h1 = h1.wrapping_mul(0x9e3779b97f4a7c15);
304 let h2 = h1.wrapping_mul(0x6c62272e07bb0142);
305 let h3 = h2.wrapping_mul(0x9e3779b97f4a7c15);
306 let x = (h1 >> 32) as f32 / u32::MAX as f32 * 2.0 - 1.0;
307 let y = (h2 >> 32) as f32 / u32::MAX as f32 * 2.0 - 1.0;
308 let z = (h3 >> 32) as f32 / u32::MAX as f32 * 2.0 - 1.0;
309 Vec3::new(x, y, z).normalize_or_zero()
310}
311
312fn rand_f32_seeded(seed: u64) -> f32 {
313 let x = seed.wrapping_mul(0x9e3779b97f4a7c15).wrapping_add(0x6c62272e07bb0142);
314 (x >> 32) as f32 / u32::MAX as f32
315}
316
317#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn cohesion_spring_bounds() {
325 let (s0, d0) = cohesion_to_spring(0.0);
326 let (s1, d1) = cohesion_to_spring(1.0);
327 assert!(s1 > s0);
328 assert!(d1 > d0);
329 }
330
331 #[test]
332 fn manager_ticks_without_panic() {
333 let positions = vec![Vec3::ZERO, Vec3::X, Vec3::Y];
334 let mut mgr = CohesionManager::new(&positions, 0.8);
335 let result = mgr.tick(0.016);
336 assert_eq!(result.len(), 3);
337 }
338
339 #[test]
340 fn dissolution_triggers_at_zero_cohesion() {
341 let positions = vec![Vec3::X, Vec3::Y, Vec3::Z];
342 let mut mgr = CohesionManager::new(&positions, 0.1);
343 mgr.damage_cohesion(0.1);
344 assert!(mgr.is_dissolving());
345 }
346
347 #[test]
348 fn shockwave_imparts_velocity() {
349 let positions = vec![Vec3::new(1.0, 0.0, 0.0)];
350 let mut mgr = CohesionManager::new(&positions, 0.9);
351 let before = mgr.glyphs[0].velocity();
352 mgr.apply_shockwave(Vec3::ZERO, 5.0);
353 let after = mgr.glyphs[0].velocity();
354 assert!(after.length() > before.length());
355 }
356
357 #[test]
358 fn cohesion_pull_scales_with_cohesion() {
359 let a = Vec3::ZERO;
360 let b = Vec3::new(1.0, 0.0, 0.0);
361 let low = cohesion_pull(a, b, 0.1, 0.016).length();
362 let high = cohesion_pull(a, b, 0.9, 0.016).length();
363 assert!(high > low);
364 }
365}