1use super::types::*;
4use burn::prelude::*;
5use serde_json::json;
6use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct RayAnimationConfig {
11 pub duration_ms: u32,
13 pub fps: u32,
15 pub ray_colors: Vec<String>,
17 pub camera_position: [f32; 3],
19 pub camera_target: [f32; 3],
21 pub background_color: String,
23 pub ray_width: f32,
25 pub hit_dot_size: f32,
27 pub show_hits_as_dots: bool,
29 pub max_rays: Option<usize>,
31 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(), "#4ecdc4".to_string(), "#45b7d1".to_string(), "#96ceb4".to_string(), "#feca57".to_string(), "#ff9ff3".to_string(), "#54a0ff".to_string(), ],
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), failed_ray_distance: 2.0, }
58 }
59}
60
61pub 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 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 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 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 let angle = (i as f32) * 0.618034; [
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 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 let color = if is_hit {
138 config.ray_colors[region_idx % config.ray_colors.len()].clone()
139 } else {
140 "#666666".to_string() };
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 let html_content = generate_threejs_html(&rays_data, config)?;
155
156 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
508pub 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}