1use glam::{Vec2, Vec3, Vec4};
5
6use super::glyph_mesh::{GlyphMesh, Vertex3D};
7use super::glyph_materials::{GlyphMaterial, lerp_material};
8
9#[derive(Clone, Debug)]
13pub struct GlyphFragment {
14 pub mesh: GlyphMesh,
15 pub position: Vec3,
16 pub velocity: Vec3,
17 pub angular_velocity: Vec3,
18 pub rotation: Vec3,
19 pub material: GlyphMaterial,
20 pub life: f32,
21 pub max_life: f32,
22}
23
24impl GlyphFragment {
25 pub fn is_alive(&self) -> bool { self.life > 0.0 }
26 pub fn progress(&self) -> f32 { 1.0 - (self.life / self.max_life).clamp(0.0, 1.0) }
27}
28
29#[derive(Clone, Debug)]
33pub struct DissolveState {
34 pub fragments: Vec<GlyphFragment>,
35 pub age: f32,
36 pub duration: f32,
37 pub done: bool,
38}
39
40impl DissolveState {
41 pub fn active_fragments(&self) -> usize {
42 self.fragments.iter().filter(|f| f.is_alive()).count()
43 }
44}
45
46pub fn split_mesh_by_plane(
50 mesh: &GlyphMesh,
51 plane_point: Vec3,
52 plane_normal: Vec3,
53) -> (GlyphMesh, GlyphMesh) {
54 let normal = plane_normal.normalize_or_zero();
55 let mut verts_a = Vec::new();
56 let mut verts_b = Vec::new();
57 let mut indices_a = Vec::new();
58 let mut indices_b = Vec::new();
59 let mut map_a: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
60 let mut map_b: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
61
62 let tri_count = mesh.indices.len() / 3;
64 for t in 0..tri_count {
65 let i0 = mesh.indices[t * 3] as usize;
66 let i1 = mesh.indices[t * 3 + 1] as usize;
67 let i2 = mesh.indices[t * 3 + 2] as usize;
68
69 let v0 = Vec3::from(mesh.vertices[i0].position);
70 let v1 = Vec3::from(mesh.vertices[i1].position);
71 let v2 = Vec3::from(mesh.vertices[i2].position);
72
73 let d0 = (v0 - plane_point).dot(normal);
74 let d1 = (v1 - plane_point).dot(normal);
75 let d2 = (v2 - plane_point).dot(normal);
76
77 let centroid_side = d0 + d1 + d2;
78
79 let original_indices = [mesh.indices[t * 3], mesh.indices[t * 3 + 1], mesh.indices[t * 3 + 2]];
81
82 if centroid_side >= 0.0 {
83 for &oi in &original_indices {
84 let new_idx = *map_a.entry(oi).or_insert_with(|| {
85 let idx = verts_a.len() as u32;
86 verts_a.push(mesh.vertices[oi as usize]);
87 idx
88 });
89 indices_a.push(new_idx);
90 }
91 } else {
92 for &oi in &original_indices {
93 let new_idx = *map_b.entry(oi).or_insert_with(|| {
94 let idx = verts_b.len() as u32;
95 verts_b.push(mesh.vertices[oi as usize]);
96 idx
97 });
98 indices_b.push(new_idx);
99 }
100 }
101 }
102
103 let compute_bounds = |verts: &[Vertex3D]| -> (Vec3, Vec3) {
104 let mut bmin = Vec3::splat(f32::MAX);
105 let mut bmax = Vec3::splat(f32::MIN);
106 for v in verts {
107 let p = Vec3::from(v.position);
108 bmin = bmin.min(p);
109 bmax = bmax.max(p);
110 }
111 if verts.is_empty() { (Vec3::ZERO, Vec3::ZERO) } else { (bmin, bmax) }
112 };
113
114 let (bmin_a, bmax_a) = compute_bounds(&verts_a);
115 let (bmin_b, bmax_b) = compute_bounds(&verts_b);
116
117 let mesh_a = GlyphMesh {
118 vertices: verts_a,
119 triangle_count: indices_a.len() as u32 / 3,
120 indices: indices_a,
121 character: mesh.character,
122 extrusion_depth: mesh.extrusion_depth,
123 bounds_min: bmin_a,
124 bounds_max: bmax_a,
125 };
126
127 let mesh_b = GlyphMesh {
128 vertices: verts_b,
129 triangle_count: indices_b.len() as u32 / 3,
130 indices: indices_b,
131 character: mesh.character,
132 extrusion_depth: mesh.extrusion_depth,
133 bounds_min: bmin_b,
134 bounds_max: bmax_b,
135 };
136
137 (mesh_a, mesh_b)
138}
139
140pub fn start_dissolve(
144 mesh: &GlyphMesh,
145 material: &GlyphMaterial,
146 impact_dir: Vec3,
147 impact_strength: f32,
148) -> DissolveState {
149 let mut fragments = Vec::new();
150
151 let num_splits = if mesh.triangle_count > 20 { 3 } else { 2 };
153 let center = (mesh.bounds_min + mesh.bounds_max) * 0.5;
154
155 let mut current_meshes = vec![mesh.clone()];
156
157 let planes: Vec<(Vec3, Vec3)> = (0..num_splits).map(|i| {
159 let angle = i as f32 * 1.2 + impact_dir.x * 0.5;
160 let normal = Vec3::new(angle.cos(), angle.sin(), 0.3).normalize();
161 let offset = (i as f32 - num_splits as f32 * 0.5) * 0.2;
162 let point = center + normal * offset;
163 (point, normal)
164 }).collect();
165
166 for (plane_point, plane_normal) in &planes {
167 let mut new_meshes = Vec::new();
168 for m in ¤t_meshes {
169 if m.triangle_count < 2 {
170 new_meshes.push(m.clone());
171 continue;
172 }
173 let (a, b) = split_mesh_by_plane(m, *plane_point, *plane_normal);
174 if !a.vertices.is_empty() { new_meshes.push(a); }
175 if !b.vertices.is_empty() { new_meshes.push(b); }
176 }
177 current_meshes = new_meshes;
178 }
179
180 for (i, frag_mesh) in current_meshes.into_iter().enumerate() {
182 if frag_mesh.vertices.is_empty() { continue; }
183
184 let frag_center = (frag_mesh.bounds_min + frag_mesh.bounds_max) * 0.5;
185 let scatter_dir = (frag_center - center).normalize_or_zero();
186
187 let base_speed = impact_strength * 2.0;
189 let velocity = (impact_dir.normalize_or_zero() * 0.5 + scatter_dir * 0.5).normalize_or_zero()
190 * base_speed * (1.0 + i as f32 * 0.3);
191
192 let angular = impact_dir.cross(Vec3::new(0.0, 0.0, 1.0)).normalize_or_zero()
194 * impact_strength * 5.0 * (if i % 2 == 0 { 1.0 } else { -1.0 });
195
196 let life = 1.0 + i as f32 * 0.2;
197
198 fragments.push(GlyphFragment {
199 mesh: frag_mesh,
200 position: frag_center,
201 velocity,
202 angular_velocity: angular,
203 rotation: Vec3::ZERO,
204 material: *material,
205 life,
206 max_life: life,
207 });
208 }
209
210 let max_life = fragments.iter().map(|f| f.max_life).fold(0.0f32, f32::max);
211
212 DissolveState {
213 fragments,
214 age: 0.0,
215 duration: max_life,
216 done: false,
217 }
218}
219
220pub fn tick_dissolve(state: &mut DissolveState, dt: f32, gravity: Vec3) {
224 state.age += dt;
225
226 for frag in &mut state.fragments {
227 if !frag.is_alive() { continue; }
228
229 frag.velocity += gravity * dt;
231 frag.position += frag.velocity * dt;
232 frag.rotation += frag.angular_velocity * dt;
233
234 frag.velocity *= 0.98;
236 frag.angular_velocity *= 0.97;
237
238 frag.life -= dt;
240
241 frag.material = dissolve_material_at(&frag.material, frag.progress());
243 }
244
245 state.done = state.fragments.iter().all(|f| !f.is_alive());
246}
247
248pub fn dissolve_material_at(base: &GlyphMaterial, progress: f32) -> GlyphMaterial {
250 let p = progress.clamp(0.0, 1.0);
251
252 let dead = GlyphMaterial {
253 base_color: [0.1, 0.1, 0.1, 0.0],
254 emission: 0.0,
255 metallic: 0.0,
256 roughness: 1.0,
257 subsurface: 0.0,
258 fresnel: 0.02,
259 _pad: [0.0; 3],
260 };
261
262 lerp_material(base, &dead, p)
263}
264
265pub struct DissolveRenderer;
269
270impl DissolveRenderer {
271 pub fn collect_instances(state: &DissolveState) -> Vec<(glam::Mat4, GlyphMaterial, char)> {
273 let mut instances = Vec::new();
274 for frag in &state.fragments {
275 if !frag.is_alive() { continue; }
276
277 let translation = glam::Mat4::from_translation(frag.position);
278 let rot_x = glam::Mat4::from_rotation_x(frag.rotation.x);
279 let rot_y = glam::Mat4::from_rotation_y(frag.rotation.y);
280 let rot_z = glam::Mat4::from_rotation_z(frag.rotation.z);
281 let scale = glam::Mat4::from_scale(Vec3::splat(1.0 - frag.progress() * 0.3));
282 let transform = translation * rot_z * rot_y * rot_x * scale;
283
284 instances.push((transform, frag.material, frag.mesh.character));
285 }
286 instances
287 }
288}
289
290#[cfg(test)]
293mod tests {
294 use super::*;
295 use super::super::glyph_mesh::create_box_mesh;
296
297 fn test_mesh() -> GlyphMesh {
298 create_box_mesh(Vec3::ZERO, Vec3::ONE, 'X', 0.5)
299 }
300
301 #[test]
302 fn split_produces_two_meshes() {
303 let mesh = test_mesh();
304 let (a, b) = split_mesh_by_plane(&mesh, Vec3::new(0.5, 0.5, 0.0), Vec3::X);
305 assert!(!a.vertices.is_empty() || !b.vertices.is_empty());
306 assert!(a.triangle_count + b.triangle_count <= mesh.triangle_count + 2);
308 }
309
310 #[test]
311 fn dissolve_creates_fragments() {
312 let mesh = test_mesh();
313 let mat = GlyphMaterial::enemy();
314 let state = start_dissolve(&mesh, &mat, Vec3::X, 1.0);
315 assert!(state.fragments.len() >= 2, "Should create at least 2 fragments, got {}", state.fragments.len());
316 }
317
318 #[test]
319 fn fragments_move_apart() {
320 let mesh = test_mesh();
321 let mat = GlyphMaterial::enemy();
322 let mut state = start_dissolve(&mesh, &mat, Vec3::X, 5.0);
323 let initial_positions: Vec<Vec3> = state.fragments.iter().map(|f| f.position).collect();
324
325 tick_dissolve(&mut state, 0.5, Vec3::new(0.0, -9.8, 0.0));
326
327 for (i, frag) in state.fragments.iter().enumerate() {
328 let moved = (frag.position - initial_positions[i]).length();
329 assert!(moved > 0.01, "Fragment {} should have moved", i);
330 }
331 }
332
333 #[test]
334 fn dissolve_material_darkens() {
335 let mat = GlyphMaterial::boss();
336 let dead_mat = dissolve_material_at(&mat, 1.0);
337 assert!(dead_mat.emission < mat.emission);
338 assert!(dead_mat.roughness > mat.roughness);
339 }
340
341 #[test]
342 fn dissolve_completes() {
343 let mesh = test_mesh();
344 let mat = GlyphMaterial::enemy();
345 let mut state = start_dissolve(&mesh, &mat, Vec3::X, 1.0);
346 for _ in 0..100 {
347 tick_dissolve(&mut state, 0.1, Vec3::ZERO);
348 }
349 assert!(state.done, "Should be done after enough time");
350 }
351
352 #[test]
353 fn collect_instances_only_alive() {
354 let mesh = test_mesh();
355 let mat = GlyphMaterial::enemy();
356 let mut state = start_dissolve(&mesh, &mat, Vec3::X, 1.0);
357 let initial = DissolveRenderer::collect_instances(&state).len();
358 for _ in 0..100 {
359 tick_dissolve(&mut state, 0.1, Vec3::ZERO);
360 }
361 let final_count = DissolveRenderer::collect_instances(&state).len();
362 assert!(final_count <= initial);
363 }
364}