crater/analysis/rays/
animation.rs

1//! Ray animation functionality - requires serde-threejs feature
2
3use super::types::*;
4use burn::prelude::*;
5use serde_json::json;
6use std::path::Path;
7
8/// Configuration for ray animation generation
9#[derive(Debug, Clone)]
10pub struct RayAnimationConfig {
11    /// Duration of the animation in milliseconds
12    pub duration_ms: u32,
13    /// Frame rate for the animation
14    pub fps: u32,
15    /// Color scheme for rays (hex colors)
16    pub ray_colors: Vec<String>,
17    /// Camera position for the 3D view
18    pub camera_position: [f32; 3],
19    /// Camera target (look-at point)
20    pub camera_target: [f32; 3],
21    /// Background color (hex)
22    pub background_color: String,
23    /// Ray line width
24    pub ray_width: f32,
25    /// Size of hit point dots
26    pub hit_dot_size: f32,
27    /// Show successful hits as dots instead of lines
28    pub show_hits_as_dots: bool,
29    /// Maximum number of rays to animate (for performance)
30    pub max_rays: Option<usize>,
31    /// Distance to show failed rays (instead of infinite extension)
32    pub failed_ray_distance: f32,
33}
34
35impl Default for RayAnimationConfig {
36    fn default() -> Self {
37        Self {
38            duration_ms: 3000,
39            fps: 60,
40            ray_colors: vec![
41                "#ff6b6b".to_string(), // Red
42                "#4ecdc4".to_string(), // Teal
43                "#45b7d1".to_string(), // Blue
44                "#96ceb4".to_string(), // Green
45                "#feca57".to_string(), // Yellow
46                "#ff9ff3".to_string(), // Pink
47                "#54a0ff".to_string(), // Light blue
48            ],
49            camera_position: [10.0, 10.0, 10.0],
50            camera_target: [0.0, 0.0, 0.0],
51            background_color: "#1a1a1a".to_string(),
52            ray_width: 2.0,
53            hit_dot_size: 0.02,
54            show_hits_as_dots: true,
55            max_rays: Some(100_000),  // Limit for performance
56            failed_ray_distance: 2.0, // Show failed rays extending this distance
57        }
58    }
59}
60
61/// Generate a Three.js-based animation from ray cast results
62pub fn generate_ray_animation<B: Backend, const N: usize>(
63    result: &RayCastResult<B, N>,
64    config: &RayAnimationConfig,
65    output_path: &Path,
66) -> Result<(), Box<dyn std::error::Error>> {
67    // Extract ray data
68    let origins = result.rays().origins().to_data();
69    let extensions = result.extensions().to_data();
70    let distances = result.distances().to_data();
71    let region_indices = result.region_indices().to_data();
72
73    let ray_count = result.rays().ray_count();
74    let actual_ray_count = if let Some(max_rays) = config.max_rays {
75        ray_count.min(max_rays)
76    } else {
77        ray_count
78    };
79
80    // Convert tensor data to vectors
81    let origins_vec: Vec<f32> = origins.iter::<f32>().collect();
82    let extensions_vec: Vec<f32> = extensions.iter::<f32>().collect();
83    let distances_vec: Vec<f32> = distances.iter::<f32>().collect();
84    let region_indices_vec: Vec<f32> = region_indices.iter::<f32>().collect();
85
86    let mut rays_data = Vec::new();
87    for i in 0..actual_ray_count {
88        let origin = if N == 2 {
89            [origins_vec[i * N], origins_vec[i * N + 1], 0.0]
90        } else {
91            [
92                origins_vec[i * N],
93                origins_vec[i * N + 1],
94                origins_vec[i * N + 2],
95            ]
96        };
97
98        let raw_extension = if N == 2 {
99            [extensions_vec[i * N], extensions_vec[i * N + 1], 0.0]
100        } else {
101            [
102                extensions_vec[i * N],
103                extensions_vec[i * N + 1],
104                extensions_vec[i * N + 2],
105            ]
106        };
107
108        // Check if extension coordinates are reasonable (not RAY_FAIL_VALUE)
109        let extension_is_valid = raw_extension
110            .iter()
111            .all(|&x| x.is_finite() && x.abs() < 1000.0);
112
113        let extension = if extension_is_valid {
114            raw_extension
115        } else {
116            // Extension is invalid (probably RAY_FAIL_VALUE), generate a reasonable direction
117            let angle = (i as f32) * 0.618034; // Golden angle for good distribution
118            [
119                origin[0] + angle.cos() * config.failed_ray_distance,
120                origin[1] + angle.sin() * config.failed_ray_distance,
121                origin[2] + (angle * 0.5).sin() * config.failed_ray_distance,
122            ]
123        };
124
125        let distance = distances_vec[i];
126        let region_idx = region_indices_vec[i] as usize;
127
128        // Determine if this is a successful hit or failed ray
129        let is_hit = extension_is_valid && distance.is_finite() && distance < 1000.0;
130        let final_distance = if is_hit {
131            distance
132        } else {
133            config.failed_ray_distance
134        };
135
136        // Use a different color for failed rays
137        let color = if is_hit {
138            config.ray_colors[region_idx % config.ray_colors.len()].clone()
139        } else {
140            "#666666".to_string() // Gray for failed rays
141        };
142
143        rays_data.push(json!({
144            "origin": origin,
145            "extension": extension,
146            "distance": final_distance,
147            "region": region_idx,
148            "color": color,
149            "isHit": is_hit
150        }));
151    }
152
153    // Generate HTML with Three.js animation
154    let html_content = generate_threejs_html(&rays_data, config)?;
155
156    // Write to file
157    std::fs::write(output_path, html_content)?;
158
159    Ok(())
160}
161
162fn generate_threejs_html(
163    rays_data: &[serde_json::Value],
164    config: &RayAnimationConfig,
165) -> Result<String, Box<dyn std::error::Error>> {
166    let rays_json = serde_json::to_string(rays_data)?;
167
168    let html = format!(
169        r#"
170<!DOCTYPE html>
171<html lang="en">
172<head>
173    <meta charset="UTF-8">
174    <meta name="viewport" content="width=device-width, initial-scale=1.0">
175    <title>Ray Casting Animation</title>
176    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
177    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
178    <style>
179        body {{
180            margin: 0;
181            padding: 0;
182            background-color: {background_color};
183            font-family: Arial, sans-serif;
184            overflow: hidden;
185        }}
186        #container {{
187            width: 100vw;
188            height: 100vh;
189            position: relative;
190        }}
191        #controls {{
192            position: absolute;
193            top: 10px;
194            left: 10px;
195            z-index: 1000;
196            background: rgba(0, 0, 0, 0.7);
197            padding: 10px;
198            border-radius: 5px;
199            color: white;
200        }}
201        button {{
202            margin: 5px;
203            padding: 8px 16px;
204            background: #007acc;
205            color: white;
206            border: none;
207            border-radius: 3px;
208            cursor: pointer;
209        }}
210        button:hover {{
211            background: #005a99;
212        }}
213        button:disabled {{
214            background: #444;
215            cursor: not-allowed;
216        }}
217        #info {{
218            position: absolute;
219            bottom: 10px;
220            left: 10px;
221            color: white;
222            background: rgba(0, 0, 0, 0.7);
223            padding: 10px;
224            border-radius: 5px;
225            font-size: 12px;
226        }}
227    </style>
228</head>
229<body>
230    <div id="container">
231        <div id="controls">
232            <button id="playBtn" onclick="playAnimation()">Play</button>
233            <button id="pauseBtn" onclick="pauseAnimation()" disabled>Pause</button>
234            <button id="replayBtn" onclick="replayAnimation()" style="display:none;">Replay</button>
235            <button onclick="resetAnimation()">Reset</button>
236            <label>Speed: <input type="range" id="speedSlider" min="0.1" max="3" value="1" step="0.1" onchange="updateSpeed()"></label>
237        </div>
238        <div id="info">
239            <div>Rays: {ray_count}</div>
240            <div>Duration: {duration_ms}ms</div>
241            <div>Use mouse to orbit, zoom, and pan</div>
242        </div>
243    </div>
244
245    <script>
246        // Configuration
247        const config = {{
248            duration: {duration_ms},
249            fps: {fps},
250            cameraPosition: {camera_position:?},
251            cameraTarget: {camera_target:?},
252            rayWidth: {ray_width},
253            hitDotSize: {hit_dot_size},
254            showHitsAsDots: {show_hits_as_dots}
255        }};
256        
257        // Ray data
258        const raysData = {rays_json};
259        
260        // Three.js setup
261        let scene, camera, renderer, controls;
262        let rayObjects = []; // Can contain both lines and dots
263        let animationSpeed = 1.0;
264        let isPlaying = false;
265        let isCompleted = false;
266        let startTime = 0;
267        let pausedTime = 0;
268        let lastProgress = -1; // Track last progress to avoid unnecessary updates
269        
270        function init() {{
271            // Scene
272            scene = new THREE.Scene();
273            scene.background = new THREE.Color('{background_color}');
274            
275            // Camera
276            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
277            camera.position.set(config.cameraPosition[0], config.cameraPosition[1], config.cameraPosition[2]);
278            camera.lookAt(config.cameraTarget[0], config.cameraTarget[1], config.cameraTarget[2]);
279            
280            // Renderer
281            renderer = new THREE.WebGLRenderer({{ antialias: true }});
282            renderer.setSize(window.innerWidth, window.innerHeight);
283            document.getElementById('container').appendChild(renderer.domElement);
284            
285            // Controls
286            controls = new THREE.OrbitControls(camera, renderer.domElement);
287            controls.target.set(config.cameraTarget[0], config.cameraTarget[1], config.cameraTarget[2]);
288            controls.enableDamping = true;
289            controls.dampingFactor = 0.05;
290            
291            // Create ray objects (lines and/or dots)
292            createRayObjects();
293            
294            // Start animation loop
295            animate();
296        }}
297        
298        function createRayObjects() {{
299            // Group rays by color for instanced rendering
300            const raysByColor = new Map();
301            
302            raysData.forEach((rayData, index) => {{
303                // Validate coordinates to prevent extreme values
304                const validateCoord = (coord) => {{
305                    return isFinite(coord) && Math.abs(coord) < 1000 ? coord : 0;
306                }};
307                
308                const origin = new THREE.Vector3(
309                    validateCoord(rayData.origin[0]),
310                    validateCoord(rayData.origin[1]),
311                    validateCoord(rayData.origin[2])
312                );
313                const extension = new THREE.Vector3(
314                    validateCoord(rayData.extension[0]),
315                    validateCoord(rayData.extension[1]),
316                    validateCoord(rayData.extension[2])
317                );
318                
319                const color = rayData.color || '#ffffff';
320                if (!raysByColor.has(color)) {{
321                    raysByColor.set(color, []);
322                }}
323                raysByColor.get(color).push({{
324                    origin: origin,
325                    extension: extension,
326                    rayData: rayData
327                }});
328            }});
329            
330            // Create instanced meshes for each color group
331            raysByColor.forEach((rays, color) => {{
332                const count = rays.length;
333                const geometry = new THREE.SphereGeometry(config.hitDotSize, 4, 3); // Minimal segments for performance
334                const material = new THREE.MeshBasicMaterial({{ color: color }});
335                
336                const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
337                const matrix = new THREE.Matrix4();
338                
339                // Store ray data in userData for animation
340                instancedMesh.userData = {{
341                    rays: rays,
342                    type: 'instanced_dots'
343                }};
344                
345                // Initialize positions at origins
346                rays.forEach((ray, i) => {{
347                    matrix.setPosition(ray.origin);
348                    instancedMesh.setMatrixAt(i, matrix);
349                }});
350                instancedMesh.instanceMatrix.needsUpdate = true;
351                
352                scene.add(instancedMesh);
353                rayObjects.push(instancedMesh);
354            }});
355        }}
356        
357        function updateRayAnimation(progress) {{
358            // Clamp progress between 0 and 1
359            progress = Math.max(0, Math.min(1, progress));
360            
361            // Skip update if progress hasn't changed significantly
362            const progressDelta = Math.abs(progress - lastProgress);
363            if (progressDelta < 0.001 && progress !== 0 && progress !== 1) {{
364                return;
365            }}
366            lastProgress = progress;
367            
368            const matrix = new THREE.Matrix4();
369            const tempPos = new THREE.Vector3();
370            
371            rayObjects.forEach(obj => {{
372                if (obj.userData.type === 'instanced_dots') {{
373                    // Handle instanced mesh animation
374                    const rays = obj.userData.rays;
375                    rays.forEach((ray, i) => {{
376                        tempPos.lerpVectors(ray.origin, ray.extension, progress);
377                        matrix.setPosition(tempPos);
378                        obj.setMatrixAt(i, matrix);
379                    }});
380                    obj.instanceMatrix.needsUpdate = true;
381                }} else {{
382                    // Handle individual dots (fallback)
383                    const {{ origin, extension }} = obj.userData;
384                    tempPos.lerpVectors(origin, extension, progress);
385                    obj.position.copy(tempPos);
386                }}
387            }});
388        }}
389        
390        function animate() {{
391            requestAnimationFrame(animate);
392            
393            if (isPlaying && !isCompleted) {{
394                const currentTime = Date.now();
395                const elapsedTime = (currentTime - startTime + pausedTime) * animationSpeed;
396                const progress = elapsedTime / config.duration;
397                
398                if (progress >= 1.0) {{
399                    // Animation completed
400                    updateRayAnimation(1.0);
401                    isPlaying = false;
402                    isCompleted = true;
403                    updateControls();
404                }} else {{
405                    updateRayAnimation(progress);
406                }}
407            }}
408            
409            controls.update();
410            renderer.render(scene, camera);
411        }}
412        
413        function updateControls() {{
414            const playBtn = document.getElementById('playBtn');
415            const pauseBtn = document.getElementById('pauseBtn');
416            const replayBtn = document.getElementById('replayBtn');
417            
418            if (isCompleted) {{
419                playBtn.style.display = 'none';
420                pauseBtn.style.display = 'none';
421                replayBtn.style.display = 'inline-block';
422            }} else if (isPlaying) {{
423                playBtn.disabled = true;
424                pauseBtn.disabled = false;
425            }} else {{
426                playBtn.disabled = false;
427                pauseBtn.disabled = true;
428            }}
429        }}
430        
431        function playAnimation() {{
432            if (!isPlaying && !isCompleted) {{
433                isPlaying = true;
434                startTime = Date.now();
435                updateControls();
436            }}
437        }}
438        
439        function pauseAnimation() {{
440            if (isPlaying) {{
441                pausedTime += (Date.now() - startTime);
442                isPlaying = false;
443                updateControls();
444            }}
445        }}
446        
447        function replayAnimation() {{
448            isCompleted = false;
449            isPlaying = true;
450            startTime = Date.now();
451            pausedTime = 0;
452            updateRayAnimation(0);
453            updateControls();
454        }}
455        
456        function resetAnimation() {{
457            isPlaying = false;
458            isCompleted = false;
459            startTime = 0;
460            pausedTime = 0;
461            updateRayAnimation(0);
462            updateControls();
463        }}
464        
465        function updateSpeed() {{
466            const slider = document.getElementById('speedSlider');
467            animationSpeed = parseFloat(slider.value);
468        }}
469        
470        // Handle window resize
471        window.addEventListener('resize', () => {{
472            camera.aspect = window.innerWidth / window.innerHeight;
473            camera.updateProjectionMatrix();
474            renderer.setSize(window.innerWidth, window.innerHeight);
475        }});
476        
477        // Initialize
478        init();
479        updateControls();
480        
481        // Auto-play animation after a short delay
482        setTimeout(() => {{
483            playAnimation();
484        }}, 500);
485    </script>
486</body>
487</html>
488    "#,
489        background_color = config.background_color,
490        ray_count = rays_data.len(),
491        duration_ms = config.duration_ms,
492        fps = config.fps,
493        camera_position = config.camera_position,
494        camera_target = config.camera_target,
495        ray_width = config.ray_width,
496        hit_dot_size = config.hit_dot_size,
497        show_hits_as_dots = if config.show_hits_as_dots {
498            "true"
499        } else {
500            "false"
501        },
502        rays_json = rays_json
503    );
504
505    Ok(html)
506}
507
508/// Convenience function to create an animation with default settings
509pub fn create_default_animation<B: Backend, const N: usize>(
510    result: &RayCastResult<B, N>,
511    output_path: &Path,
512) -> Result<(), Box<dyn std::error::Error>> {
513    let config = RayAnimationConfig::default();
514    generate_ray_animation(result, &config, output_path)
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn test_animation_config_default() {
523        let config = RayAnimationConfig::default();
524        assert_eq!(config.duration_ms, 3000);
525        assert_eq!(config.fps, 60);
526        assert!(!config.ray_colors.is_empty());
527    }
528}