1use crate::csg::trajectory::Trajectories;
2use burn::prelude::*;
3use serde_json::json;
4use std::path::Path;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum AnimationType {
9 CustomColors,
11 RaycastResults,
13 Uniform,
15 Indexed { group_size: Option<usize> },
17 Gradient {
19 start_color: String,
20 end_color: String,
21 },
22}
23
24#[derive(Debug, Clone)]
26pub struct TrajectoryAnimationConfig {
27 pub duration_ms: u32,
29 pub fps: u32,
31 pub colors: Vec<String>,
33 pub camera_position: [f32; 3],
35 pub camera_target: [f32; 3],
37 pub background_color: String,
39 pub dot_size: f32,
41 pub max_trajectories: Option<usize>,
43 pub failed_opacity: f32,
45 pub animation_type: AnimationType,
47}
48
49#[derive(Clone)]
51pub enum ColorSource<'a, B: Backend, const N: usize> {
52 Custom(&'a [(String, f32)]),
54 Raycast(&'a crate::analysis::prelude::RayCastResult<B, N>),
56 Config,
58}
59
60impl Default for TrajectoryAnimationConfig {
61 fn default() -> Self {
62 Self {
63 duration_ms: 3000,
64 fps: 60,
65 colors: vec![
66 "#ff6b6b".to_string(), "#4ecdc4".to_string(), "#45b7d1".to_string(), "#96ceb4".to_string(), "#feca57".to_string(), "#ff9ff3".to_string(), "#54a0ff".to_string(), ],
74 camera_position: [10.0, 10.0, 10.0],
75 camera_target: [0.0, 0.0, 0.0],
76 background_color: "#1a1a1a".to_string(),
77 dot_size: 0.02,
78 max_trajectories: Some(100_000), failed_opacity: 0.3,
80 animation_type: AnimationType::Uniform,
81 }
82 }
83}
84
85impl AnimationType {
86 pub fn gradient_preset(preset: &str) -> Self {
88 match preset {
89 "fire" => Self::Gradient {
90 start_color: "#ff4757".to_string(),
91 end_color: "#ffa502".to_string(),
92 },
93 "ocean" => Self::Gradient {
94 start_color: "#3742fa".to_string(),
95 end_color: "#2ed573".to_string(),
96 },
97 "sunset" => Self::Gradient {
98 start_color: "#ff6b9d".to_string(),
99 end_color: "#ffa726".to_string(),
100 },
101 "forest" => Self::Gradient {
102 start_color: "#00d2d3".to_string(),
103 end_color: "#54a0ff".to_string(),
104 },
105 _ => Self::Gradient {
106 start_color: "#ff6b6b".to_string(),
107 end_color: "#4ecdc4".to_string(),
108 },
109 }
110 }
111}
112
113pub fn generate_trajectory_colors<B: Backend, const N: usize>(
115 trajectories: &Trajectories<B, N>,
116 color_source: ColorSource<'_, B, N>,
117 config: &TrajectoryAnimationConfig,
118) -> Result<Vec<(String, f32)>, Box<dyn std::error::Error>> {
119 if trajectories.is_empty() {
120 return Ok(Vec::new());
121 }
122
123 let first_step = &trajectories[0];
124 let trajectory_count = first_step.shape().dims::<2>()[0];
125
126 match (color_source, &config.animation_type) {
127 (ColorSource::Custom(colors), AnimationType::CustomColors) => {
128 if colors.len() != trajectory_count {
129 return Err(format!(
130 "Custom color count ({}) does not match trajectory count ({})",
131 colors.len(),
132 trajectory_count
133 )
134 .into());
135 }
136 Ok(colors.to_vec())
137 }
138 (ColorSource::Raycast(raycast_result), AnimationType::RaycastResults) => {
139 compute_raycast_colors(raycast_result, config)
140 }
141 (_, AnimationType::Uniform) => {
142 let mut colors = Vec::with_capacity(trajectory_count);
143 for i in 0..trajectory_count {
144 let color = config.colors[i % config.colors.len()].clone();
145 colors.push((color, 1.0));
146 }
147 Ok(colors)
148 }
149 (_, AnimationType::Indexed { group_size }) => {
150 let mut colors = Vec::with_capacity(trajectory_count);
151 let group_size = group_size.unwrap_or(config.colors.len());
152 for i in 0..trajectory_count {
153 let color_idx = (i / group_size) % config.colors.len();
154 let color = config.colors[color_idx].clone();
155 colors.push((color, 1.0));
156 }
157 Ok(colors)
158 }
159 (
160 _,
161 AnimationType::Gradient {
162 start_color,
163 end_color,
164 },
165 ) => generate_gradient_colors(trajectory_count, start_color, end_color),
166 _ => Err("Incompatible color source and animation type combination".into()),
167 }
168}
169
170fn generate_gradient_colors(
172 count: usize,
173 start_hex: &str,
174 end_hex: &str,
175) -> Result<Vec<(String, f32)>, Box<dyn std::error::Error>> {
176 let start_rgb = hex_to_rgb(start_hex)?;
177 let end_rgb = hex_to_rgb(end_hex)?;
178
179 let mut colors = Vec::with_capacity(count);
180 for i in 0..count {
181 let t = if count > 1 {
182 i as f32 / (count - 1) as f32
183 } else {
184 0.0
185 };
186 let r = (start_rgb[0] as f32 * (1.0 - t) + end_rgb[0] as f32 * t) as u8;
187 let g = (start_rgb[1] as f32 * (1.0 - t) + end_rgb[1] as f32 * t) as u8;
188 let b = (start_rgb[2] as f32 * (1.0 - t) + end_rgb[2] as f32 * t) as u8;
189 let hex = format!("#{:02x}{:02x}{:02x}", r, g, b);
190 colors.push((hex, 1.0));
191 }
192 Ok(colors)
193}
194
195fn hex_to_rgb(hex: &str) -> Result<[u8; 3], Box<dyn std::error::Error>> {
197 let hex = hex.trim_start_matches('#');
198 if hex.len() != 6 {
199 return Err("Invalid hex color format".into());
200 }
201
202 let r = u8::from_str_radix(&hex[0..2], 16)?;
203 let g = u8::from_str_radix(&hex[2..4], 16)?;
204 let b = u8::from_str_radix(&hex[4..6], 16)?;
205 Ok([r, g, b])
206}
207
208pub fn generate_trajectory_animation<B: Backend, const N: usize>(
210 trajectories: &Trajectories<B, N>,
211 color_source: ColorSource<'_, B, N>,
212 config: &TrajectoryAnimationConfig,
213 output_path: &Path,
214) -> Result<(), Box<dyn std::error::Error>> {
215 if trajectories.is_empty() {
216 return Err("No trajectories to animate".into());
217 }
218
219 let colors = generate_trajectory_colors(trajectories, color_source, config)?;
221
222 let trajectories_data = convert_trajectories_to_json::<B, N>(trajectories, &colors, config)?;
224
225 let html_content = generate_threejs_html(&trajectories_data, config)?;
227 std::fs::write(output_path, html_content)?;
228
229 Ok(())
230}
231
232fn convert_trajectories_to_json<B: Backend, const N: usize>(
234 trajectories: &Trajectories<B, N>,
235 colors: &[(String, f32)],
236 config: &TrajectoryAnimationConfig,
237) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
238 let first_step = &trajectories[0];
239 let trajectory_count = first_step.shape().dims::<2>()[0];
240 let actual_trajectory_count = config
241 .max_trajectories
242 .unwrap_or(trajectory_count)
243 .min(trajectory_count);
244
245 let step_data_vecs: Vec<Vec<f32>> = trajectories
247 .iter()
248 .map(|step| {
249 let step_data = step.to_data();
250 step_data.iter::<f32>().collect()
251 })
252 .collect();
253
254 let mut trajectories_data = Vec::with_capacity(actual_trajectory_count);
256
257 for traj_idx in 0..actual_trajectory_count {
258 let mut path = Vec::with_capacity(trajectories.len());
259
260 for step_vec in &step_data_vecs {
261 let position = if N == 2 {
262 [step_vec[traj_idx * N], step_vec[traj_idx * N + 1], 0.0]
263 } else {
264 [
265 step_vec[traj_idx * N],
266 step_vec[traj_idx * N + 1],
267 step_vec[traj_idx * N + 2],
268 ]
269 };
270
271 if position.iter().all(|&x| x.is_finite() && x.abs() < 1000.0) {
273 path.push(position);
274 }
275 }
276
277 if !path.is_empty() {
278 let (color, opacity) = &colors[traj_idx.min(colors.len() - 1)];
279
280 trajectories_data.push(json!({
281 "path": path,
282 "color": color,
283 "opacity": opacity
284 }));
285 }
286 }
287
288 Ok(trajectories_data)
289}
290
291pub fn generate_trajectory_animation_with_colors<B: Backend, const N: usize>(
293 trajectories: &Trajectories<B, N>,
294 colors: &[(String, f32)], config: &TrajectoryAnimationConfig,
296 output_path: &Path,
297) -> Result<(), Box<dyn std::error::Error>> {
298 let mut custom_config = config.clone();
299 custom_config.animation_type = AnimationType::CustomColors;
300
301 generate_trajectory_animation(
302 trajectories,
303 ColorSource::<B, N>::Custom(colors),
304 &custom_config,
305 output_path,
306 )
307}
308
309pub fn compute_raycast_colors<B: Backend, const N: usize>(
311 raycast_result: &crate::analysis::prelude::RayCastResult<B, N>,
312 config: &TrajectoryAnimationConfig,
313) -> Result<Vec<(String, f32)>, Box<dyn std::error::Error>> {
314 use crate::analysis::prelude::RAY_FAIL_VALUE;
315
316 let ray_count = raycast_result.rays().ray_count();
317 let distances = raycast_result.distances().to_data();
318 let region_indices = raycast_result.region_indices().to_data();
319 let extensions = raycast_result.extensions().to_data();
320
321 let distances_vec: Vec<f32> = distances.iter::<f32>().collect();
322 let region_indices_vec: Vec<f32> = region_indices.iter::<f32>().collect();
323 let extensions_vec: Vec<f32> = extensions.iter::<f32>().collect();
324
325 let mut colors = Vec::with_capacity(ray_count);
326
327 for ray_idx in 0..ray_count {
328 let distance = distances_vec[ray_idx];
329 let region_idx = region_indices_vec[ray_idx] as usize;
330
331 let extension_start = ray_idx * N;
333 let extension_is_valid = (0..N).all(|i| {
334 let coord = extensions_vec[extension_start + i];
335 coord.is_finite() && coord.abs() < 1000.0 && coord != RAY_FAIL_VALUE
336 });
337
338 let is_hit = extension_is_valid
339 && distance.is_finite()
340 && distance != RAY_FAIL_VALUE
341 && distance < 1000.0;
342
343 let (color, opacity) = if is_hit {
344 let color = config.colors[region_idx % config.colors.len()].clone();
346 (color, 1.0)
347 } else {
348 ("#666666".to_string(), config.failed_opacity)
350 };
351
352 colors.push((color, opacity));
353 }
354
355 Ok(colors)
356}
357
358pub fn generate_trajectory_animation_with_raycast<B: Backend, const N: usize>(
360 trajectories: &Trajectories<B, N>,
361 raycast_result: &crate::analysis::prelude::RayCastResult<B, N>,
362 config: &TrajectoryAnimationConfig,
363 output_path: &Path,
364) -> Result<(), Box<dyn std::error::Error>> {
365 let mut raycast_config = config.clone();
366 raycast_config.animation_type = AnimationType::RaycastResults;
367
368 generate_trajectory_animation(
369 trajectories,
370 ColorSource::<B, N>::Raycast(raycast_result),
371 &raycast_config,
372 output_path,
373 )
374}
375
376pub fn generate_trajectory_animation_uniform<B: Backend, const N: usize>(
378 trajectories: &Trajectories<B, N>,
379 config: &TrajectoryAnimationConfig,
380 output_path: &Path,
381) -> Result<(), Box<dyn std::error::Error>> {
382 let mut uniform_config = config.clone();
383 uniform_config.animation_type = AnimationType::Uniform;
384
385 generate_trajectory_animation(
386 trajectories,
387 ColorSource::<B, N>::Config,
388 &uniform_config,
389 output_path,
390 )
391}
392
393fn generate_threejs_html(
394 trajectories_data: &[serde_json::Value],
395 config: &TrajectoryAnimationConfig,
396) -> Result<String, Box<dyn std::error::Error>> {
397 let trajectories_json = serde_json::to_string(trajectories_data)?;
398
399 let template = include_str!("animation_template.html");
401 let html = template
402 .replace("BACKGROUND_COLOR_PLACEHOLDER", &config.background_color)
403 .replace(
404 "TRAJECTORY_COUNT_PLACEHOLDER",
405 &trajectories_data.len().to_string(),
406 )
407 .replace("DURATION_MS_PLACEHOLDER", &config.duration_ms.to_string())
408 .replace("FPS_PLACEHOLDER", &config.fps.to_string())
409 .replace(
410 "CAMERA_POSITION_PLACEHOLDER",
411 &format!("{:?}", config.camera_position),
412 )
413 .replace(
414 "CAMERA_TARGET_PLACEHOLDER",
415 &format!("{:?}", config.camera_target),
416 )
417 .replace("DOT_SIZE_PLACEHOLDER", &config.dot_size.to_string())
418 .replace("TRAJECTORIES_JSON_PLACEHOLDER", &trajectories_json);
419
420 Ok(html)
421}
422
423pub fn create_default_trajectory_animation<B: Backend, const N: usize>(
425 trajectories: &Trajectories<B, N>,
426 output_path: &Path,
427) -> Result<(), Box<dyn std::error::Error>> {
428 let config = TrajectoryAnimationConfig::default();
429 generate_trajectory_animation(
430 trajectories,
431 ColorSource::<B, N>::Config,
432 &config,
433 output_path,
434 )
435}
436
437pub fn create_gradient_animation<B: Backend, const N: usize>(
439 trajectories: &Trajectories<B, N>,
440 gradient_preset: &str,
441 output_path: &Path,
442) -> Result<(), Box<dyn std::error::Error>> {
443 let config = TrajectoryAnimationConfig {
444 animation_type: AnimationType::gradient_preset(gradient_preset),
445 ..Default::default()
446 };
447
448 generate_trajectory_animation(
449 trajectories,
450 ColorSource::<B, N>::Config,
451 &config,
452 output_path,
453 )
454}
455
456pub fn create_raycast_animation<B: Backend, const N: usize>(
458 trajectories: &Trajectories<B, N>,
459 raycast_result: &crate::analysis::prelude::RayCastResult<B, N>,
460 output_path: &Path,
461) -> Result<(), Box<dyn std::error::Error>> {
462 let config = TrajectoryAnimationConfig {
463 animation_type: AnimationType::RaycastResults,
464 ..Default::default()
465 };
466
467 generate_trajectory_animation(
468 trajectories,
469 ColorSource::<B, N>::Raycast(raycast_result),
470 &config,
471 output_path,
472 )
473}
474
475pub fn create_indexed_animation<B: Backend, const N: usize>(
477 trajectories: &Trajectories<B, N>,
478 group_size: Option<usize>,
479 output_path: &Path,
480) -> Result<(), Box<dyn std::error::Error>> {
481 let config = TrajectoryAnimationConfig {
482 animation_type: AnimationType::Indexed { group_size },
483 ..Default::default()
484 };
485
486 generate_trajectory_animation(
487 trajectories,
488 ColorSource::<B, N>::Config,
489 &config,
490 output_path,
491 )
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 #[test]
499 fn test_animation_config_default() {
500 let config = TrajectoryAnimationConfig::default();
501 assert_eq!(config.duration_ms, 3000);
502 assert_eq!(config.fps, 60);
503 assert!(!config.colors.is_empty());
504 assert_eq!(config.animation_type, AnimationType::Uniform);
505 }
506
507 #[test]
508 fn test_animation_type_gradient_presets() {
509 let fire = AnimationType::gradient_preset("fire");
510 match fire {
511 AnimationType::Gradient {
512 start_color,
513 end_color,
514 } => {
515 assert_eq!(start_color, "#ff4757");
516 assert_eq!(end_color, "#ffa502");
517 }
518 _ => panic!("Expected gradient type"),
519 }
520
521 let unknown = AnimationType::gradient_preset("unknown");
522 match unknown {
523 AnimationType::Gradient {
524 start_color,
525 end_color,
526 } => {
527 assert_eq!(start_color, "#ff6b6b");
528 assert_eq!(end_color, "#4ecdc4");
529 }
530 _ => panic!("Expected gradient type"),
531 }
532 }
533
534 #[test]
535 fn test_hex_to_rgb() {
536 assert_eq!(hex_to_rgb("#ff0000").unwrap(), [255, 0, 0]);
537 assert_eq!(hex_to_rgb("00ff00").unwrap(), [0, 255, 0]);
538 assert_eq!(hex_to_rgb("#0000ff").unwrap(), [0, 0, 255]);
539 assert!(hex_to_rgb("invalid").is_err());
540 }
541
542 #[test]
543 fn test_generate_gradient_colors() {
544 let colors = generate_gradient_colors(3, "#ff0000", "#0000ff").unwrap();
545 assert_eq!(colors.len(), 3);
546 assert_eq!(colors[0].0, "#ff0000");
547 assert_eq!(colors[2].0, "#0000ff");
548 assert_eq!(colors[0].1, 1.0); }
550}