xdl_viz3d/
lib.rs

1//! XDL 3D Visualization Library
2//!
3//! High-fidelity 3D visualization for scientific simulations using WebGPU.
4//! Supports volume rendering, isosurface extraction, and interactive visualization.
5
6pub mod camera;
7pub mod colormap;
8pub mod renderer;
9pub mod volume;
10
11pub use camera::Camera;
12pub use renderer::VolumeRenderer;
13pub use volume::{VolumeData, VolumeFormat};
14
15use anyhow::Result;
16use std::sync::Arc;
17use wgpu::{Device, Queue, Surface, SurfaceConfiguration};
18use winit::{
19    application::ApplicationHandler,
20    event::*,
21    event_loop::{ActiveEventLoop, EventLoop},
22    keyboard::{KeyCode, PhysicalKey},
23    window::{Window, WindowAttributes, WindowId},
24};
25
26/// 3D visualization application state
27pub struct Viz3DApp {
28    surface: Surface<'static>,
29    device: Device,
30    queue: Queue,
31    config: SurfaceConfiguration,
32    renderer: VolumeRenderer,
33    camera: Camera,
34    window: Arc<Window>,
35}
36
37impl Viz3DApp {
38    /// Create a new 3D visualization application with an existing window
39    pub async fn new_with_window(window: Arc<Window>) -> Result<Self> {
40        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
41            backends: wgpu::Backends::all(),
42            ..Default::default()
43        });
44
45        let surface = instance.create_surface(window.clone())?;
46
47        let adapter = instance
48            .request_adapter(&wgpu::RequestAdapterOptions {
49                power_preference: wgpu::PowerPreference::HighPerformance,
50                compatible_surface: Some(&surface),
51                force_fallback_adapter: false,
52            })
53            .await
54            .ok_or_else(|| anyhow::anyhow!("Failed to find suitable adapter"))?;
55
56        let (device, queue) = adapter
57            .request_device(
58                &wgpu::DeviceDescriptor {
59                    label: Some("XDL 3D Device"),
60                    required_features: wgpu::Features::empty(),
61                    required_limits: wgpu::Limits::default(),
62                    memory_hints: wgpu::MemoryHints::default(),
63                },
64                None,
65            )
66            .await?;
67
68        let surface_caps = surface.get_capabilities(&adapter);
69        let surface_format = surface_caps
70            .formats
71            .iter()
72            .copied()
73            .find(|f| f.is_srgb())
74            .unwrap_or(surface_caps.formats[0]);
75
76        let size = window.inner_size();
77        let config = SurfaceConfiguration {
78            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
79            format: surface_format,
80            width: size.width,
81            height: size.height,
82            present_mode: wgpu::PresentMode::Fifo,
83            alpha_mode: surface_caps.alpha_modes[0],
84            view_formats: vec![],
85            desired_maximum_frame_latency: 2,
86        };
87        surface.configure(&device, &config);
88
89        let mut renderer = VolumeRenderer::new(&device, &config)?;
90        renderer.init_colormap(&queue); // Initialize colormap texture
91
92        let camera = Camera::new(
93            glam::Vec3::new(0.0, 0.0, 3.0),
94            glam::Vec3::ZERO,
95            size.width as f32 / size.height as f32,
96        );
97
98        Ok(Self {
99            surface,
100            device,
101            queue,
102            config,
103            renderer,
104            camera,
105            window,
106        })
107    }
108
109    /// Load volume data for visualization
110    pub fn load_volume(&mut self, volume: VolumeData) -> Result<()> {
111        self.renderer.load_volume(&self.device, &self.queue, volume)
112    }
113
114    /// Resize the window
115    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
116        if new_size.width > 0 && new_size.height > 0 {
117            self.config.width = new_size.width;
118            self.config.height = new_size.height;
119            self.surface.configure(&self.device, &self.config);
120            self.camera
121                .set_aspect(new_size.width as f32 / new_size.height as f32);
122        }
123    }
124
125    /// Handle input events
126    pub fn input(&mut self, event: &WindowEvent) -> bool {
127        self.camera.handle_input(event)
128    }
129
130    /// Update the application state
131    pub fn update(&mut self) {
132        self.camera.update();
133    }
134
135    /// Render the current frame
136    pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
137        let output = self.surface.get_current_texture()?;
138        let view = output
139            .texture
140            .create_view(&wgpu::TextureViewDescriptor::default());
141
142        self.renderer
143            .render(&self.device, &self.queue, &view, &self.camera)?;
144
145        output.present();
146        Ok(())
147    }
148
149    /// Get device reference
150    pub fn device(&self) -> &Device {
151        &self.device
152    }
153
154    /// Get queue reference
155    pub fn queue(&self) -> &Queue {
156        &self.queue
157    }
158
159    /// Get window reference
160    pub fn window(&self) -> &Arc<Window> {
161        &self.window
162    }
163
164    /// Get mutable renderer reference
165    pub fn renderer_mut(&mut self) -> &mut VolumeRenderer {
166        &mut self.renderer
167    }
168
169    /// Set the colormap for volume rendering
170    pub fn set_colormap(&mut self, colormap: colormap::Colormap) {
171        self.renderer
172            .set_colormap(&self.device, &self.queue, colormap);
173    }
174}
175
176/// Application handler for winit 0.30
177struct Viz3DHandler {
178    app: Option<Viz3DApp>,
179    volume_data: Option<VolumeData>,
180    colormap: colormap::Colormap,
181    title: String,
182}
183
184impl ApplicationHandler for Viz3DHandler {
185    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
186        // Create window on first resume (winit 0.30 pattern)
187        if self.app.is_none() {
188            println!("Creating visualization window...");
189
190            let window_attrs = WindowAttributes::default()
191                .with_title(&self.title)
192                .with_inner_size(winit::dpi::PhysicalSize::new(1280, 720));
193
194            let window = match event_loop.create_window(window_attrs) {
195                Ok(w) => Arc::new(w),
196                Err(e) => {
197                    eprintln!("Failed to create window: {}", e);
198                    event_loop.exit();
199                    return;
200                }
201            };
202
203            // Create app with the window
204            let app = match pollster::block_on(Viz3DApp::new_with_window(window)) {
205                Ok(mut a) => {
206                    // Load volume if provided
207                    if let Some(volume) = self.volume_data.take() {
208                        if let Err(e) = a.load_volume(volume) {
209                            eprintln!("Failed to load volume: {}", e);
210                            event_loop.exit();
211                            return;
212                        }
213                    }
214                    a.set_colormap(self.colormap);
215                    a
216                }
217                Err(e) => {
218                    eprintln!("Failed to create app: {}", e);
219                    event_loop.exit();
220                    return;
221                }
222            };
223
224            app.window().request_redraw();
225            self.app = Some(app);
226            println!("Window created successfully");
227            println!(
228                "Window is visible: {}",
229                self.app
230                    .as_ref()
231                    .unwrap()
232                    .window()
233                    .is_visible()
234                    .unwrap_or(false)
235            );
236        } else if let Some(app) = &self.app {
237            app.window().request_redraw();
238        }
239    }
240
241    fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: winit::event::StartCause) {
242        // Handle new events cycle
243    }
244
245    fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ()) {
246        // Handle custom user events
247    }
248
249    fn device_event(
250        &mut self,
251        _event_loop: &ActiveEventLoop,
252        _device_id: winit::event::DeviceId,
253        _event: winit::event::DeviceEvent,
254    ) {
255        // Handle device events if needed
256    }
257
258    fn window_event(
259        &mut self,
260        event_loop: &ActiveEventLoop,
261        window_id: WindowId,
262        event: WindowEvent,
263    ) {
264        let Some(app) = self.app.as_mut() else { return };
265        if window_id != app.window().id() {
266            return;
267        }
268
269        if !app.input(&event) {
270            match event {
271                WindowEvent::CloseRequested => {
272                    println!("Close requested - exiting");
273                    event_loop.exit();
274                }
275                WindowEvent::KeyboardInput {
276                    event:
277                        KeyEvent {
278                            physical_key: PhysicalKey::Code(KeyCode::Escape),
279                            state: ElementState::Pressed,
280                            ..
281                        },
282                    ..
283                } => {
284                    event_loop.exit();
285                }
286                WindowEvent::Resized(physical_size) => {
287                    app.resize(physical_size);
288                }
289                WindowEvent::RedrawRequested => {
290                    app.update();
291                    match app.render() {
292                        Ok(_) => {}
293                        Err(wgpu::SurfaceError::Lost) => {
294                            println!("Surface lost, reconfiguring...");
295                            app.resize(app.window().inner_size());
296                        }
297                        Err(wgpu::SurfaceError::OutOfMemory) => {
298                            eprintln!("Out of memory!");
299                            event_loop.exit();
300                        }
301                        Err(e) => {
302                            eprintln!("Render error: {:?}", e);
303                            event_loop.exit();
304                        }
305                    }
306                    app.window().request_redraw();
307                }
308                _ => {}
309            }
310        }
311    }
312
313    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
314        if let Some(app) = &self.app {
315            app.window().request_redraw();
316        }
317    }
318}
319
320/// Run the 3D visualization event loop
321pub fn run(
322    volume_data: Option<VolumeData>,
323    colormap: colormap::Colormap,
324    title: String,
325    event_loop: EventLoop<()>,
326) -> Result<()> {
327    let mut handler = Viz3DHandler {
328        app: None,
329        volume_data,
330        colormap,
331        title,
332    };
333    event_loop.run_app(&mut handler)?;
334    Ok(())
335}
336
337use std::sync::Mutex;
338use std::sync::OnceLock;
339
340static EVENT_LOOP_CREATED: OnceLock<Mutex<bool>> = OnceLock::new();
341
342/// Launch a 3D visualization window with volume data
343///
344/// This is a high-level API for creating and running a visualization window.
345/// It blocks until the window is closed.
346///
347/// Note: Due to winit limitations, only one event loop can be created per process.
348/// After the first visualization window is closed, subsequent calls will fail.
349/// This is a fundamental limitation of the windowing system.
350pub fn launch_visualization(
351    volume_data: Vec<f32>,
352    dimensions: [usize; 3],
353    colormap_name: &str,
354    title: Option<&str>,
355) -> Result<()> {
356    use colormap::Colormap;
357
358    // Check if event loop was already created and used
359    let already_used = EVENT_LOOP_CREATED
360        .get_or_init(|| Mutex::new(false))
361        .lock()
362        .map(|mut guard| {
363            let used = *guard;
364            *guard = true;
365            used
366        })
367        .unwrap_or(false);
368
369    if already_used {
370        return Err(anyhow::anyhow!(
371            "Cannot create multiple visualization windows in the same process. \
372            This is a limitation of the windowing system (winit). \
373            Please run each visualization in a separate XDL script execution."
374        ));
375    }
376
377    let event_loop = EventLoop::new()?;
378
379    // Parse colormap from string
380    let colormap = match colormap_name.to_uppercase().as_str() {
381        "VIRIDIS" => Colormap::Viridis,
382        "RAINBOW" => Colormap::Rainbow,
383        "PLASMA" => Colormap::Plasma,
384        "INFERNO" => Colormap::Inferno,
385        "TURBO" => Colormap::Turbo,
386        "GRAYSCALE" | "GRAY" => Colormap::Grayscale,
387        _ => Colormap::Viridis, // Default
388    };
389
390    // Prepare volume data
391    let volume = VolumeData::new(volume_data, dimensions);
392
393    // Set title
394    let window_title = title.unwrap_or("XDL 3D Visualization").to_string();
395
396    // Run event loop (blocks until window closed)
397    // Window will be created in the resumed() callback
398    run(Some(volume), colormap, window_title, event_loop)
399}