helion_core/
scatter.rs

1use crate::data::{ChartData, Vertex};
2use crate::renderer::{Renderer, WindowRenderer, WebRenderer, RenderOptions};
3use crate::backend::GPUBackend;
4use crate::shaders::{SIMPLE_VERTEX_SHADER, SIMPLE_FRAGMENT_SHADER};
5use wgpu::util::DeviceExt;
6
7/// Scatter plot renderer - implements both WindowRenderer and WebRenderer traits
8/// 
9/// This dual implementation allows the same renderer to work in:
10/// - Native window contexts (Python bindings, desktop apps)
11/// - Web contexts (WASM, browser-based apps)
12/// 
13/// Design principles:
14/// - Trait composition: Implements multiple specialized interfaces
15/// - Context-agnostic core: Same rendering logic for all platforms
16/// - Resource encapsulation: Manages its own GPU resources
17pub struct ScatterRenderer {
18    render_pipeline: wgpu::RenderPipeline,
19    vertex_buffer: Option<wgpu::Buffer>,
20    vertex_count: u32,
21}
22
23// ============================================================================
24// Base Renderer Implementation - Common to all contexts
25// ============================================================================
26
27impl Renderer for ScatterRenderer {
28    fn render_to_pass<'rpass>(&'rpass mut self, render_pass: &mut wgpu::RenderPass<'rpass>) {
29        render_pass.set_pipeline(&self.render_pipeline);
30        
31        if let Some(ref buffer) = self.vertex_buffer {
32            render_pass.set_vertex_buffer(0, buffer.slice(..));
33            render_pass.draw(0..self.vertex_count, 0..1);
34        }
35    }
36}
37
38// ============================================================================
39// WindowRenderer Implementation - For native window contexts
40// ============================================================================
41
42impl WindowRenderer for ScatterRenderer {
43    /// Create a new scatter renderer for window context
44    fn new(
45        device: &wgpu::Device,
46        config: &wgpu::SurfaceConfiguration,
47        chart_data: ChartData,
48    ) -> Self {
49        // Create shader modules
50        let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
51            label: Some("Scatter Vertex Shader"),
52            source: wgpu::ShaderSource::Wgsl(SIMPLE_VERTEX_SHADER.into()),
53        });
54
55        let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
56            label: Some("Scatter Fragment Shader"),
57            source: wgpu::ShaderSource::Wgsl(SIMPLE_FRAGMENT_SHADER.into()),
58        });
59
60        // Create pipeline layout
61        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
62            label: Some("Scatter Pipeline Layout"),
63            bind_group_layouts: &[],
64            push_constant_ranges: &[],
65        });
66
67        // Create render pipeline with the surface's texture format
68        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
69            label: Some("Scatter Render Pipeline"),
70            layout: Some(&pipeline_layout),
71            vertex: wgpu::VertexState {
72                module: &vertex_shader,
73                entry_point: "vs_main",
74                buffers: &[Vertex::desc()],
75                compilation_options: Default::default(),
76            },
77            fragment: Some(wgpu::FragmentState {
78                module: &fragment_shader,
79                entry_point: "fs_main",
80                targets: &[Some(wgpu::ColorTargetState {
81                    format: config.format,  // Use surface format
82                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
83                    write_mask: wgpu::ColorWrites::ALL,
84                })],
85                compilation_options: Default::default(),
86            }),
87            primitive: wgpu::PrimitiveState {
88                topology: wgpu::PrimitiveTopology::PointList,
89                strip_index_format: None,
90                front_face: wgpu::FrontFace::Ccw,
91                cull_mode: None,
92                polygon_mode: wgpu::PolygonMode::Fill,
93                unclipped_depth: false,
94                conservative: false,
95            },
96            depth_stencil: None,
97            multisample: wgpu::MultisampleState {
98                count: 1,
99                mask: !0,
100                alpha_to_coverage_enabled: false,
101            },
102            multiview: None,
103            cache: None,
104        });
105
106        // Create vertex buffer with initial data
107        let vertices = &chart_data.vertices;
108        let vertex_buffer = if !vertices.is_empty() {
109            Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
110                label: Some("Scatter Vertex Buffer"),
111                contents: bytemuck::cast_slice(&vertices),
112                usage: wgpu::BufferUsages::VERTEX,
113            }))
114        } else {
115            None
116        };
117
118        ScatterRenderer {
119            render_pipeline,
120            vertex_buffer,
121            vertex_count: vertices.len() as u32,
122        }
123    }
124
125    /// Update the vertex data
126    fn update_data(&mut self, device: &wgpu::Device, chart_data: &ChartData) {
127        let vertices = &chart_data.vertices;
128        
129        if !vertices.is_empty() {
130            self.vertex_buffer = Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
131                label: Some("Scatter Vertex Buffer"),
132                contents: bytemuck::cast_slice(&vertices),
133                usage: wgpu::BufferUsages::VERTEX,
134            }));
135            self.vertex_count = vertices.len() as u32;
136        } else {
137            self.vertex_buffer = None;
138            self.vertex_count = 0;
139        }
140    }
141}
142
143// ============================================================================
144// WebRenderer Implementation - For web/WASM contexts
145// ============================================================================
146
147impl WebRenderer for ScatterRenderer {
148    fn new(backend: &GPUBackend) -> Result<Self, String> {
149        let device = backend.device()?;
150        let config = backend.config.as_ref().ok_or("Backend not configured")?;
151        
152        // Reuse the same initialization logic
153        let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
154            label: Some("Scatter Vertex Shader"),
155            source: wgpu::ShaderSource::Wgsl(SIMPLE_VERTEX_SHADER.into()),
156        });
157
158        let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
159            label: Some("Scatter Fragment Shader"),
160            source: wgpu::ShaderSource::Wgsl(SIMPLE_FRAGMENT_SHADER.into()),
161        });
162
163        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
164            label: Some("Scatter Pipeline Layout"),
165            bind_group_layouts: &[],
166            push_constant_ranges: &[],
167        });
168
169        let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
170            label: Some("Scatter Render Pipeline"),
171            layout: Some(&pipeline_layout),
172            vertex: wgpu::VertexState {
173                module: &vertex_shader,
174                entry_point: "vs_main",
175                buffers: &[Vertex::desc()],
176                compilation_options: Default::default(),
177            },
178            fragment: Some(wgpu::FragmentState {
179                module: &fragment_shader,
180                entry_point: "fs_main",
181                targets: &[Some(wgpu::ColorTargetState {
182                    format: config.format,
183                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
184                    write_mask: wgpu::ColorWrites::ALL,
185                })],
186                compilation_options: Default::default(),
187            }),
188            primitive: wgpu::PrimitiveState {
189                topology: wgpu::PrimitiveTopology::PointList,
190                strip_index_format: None,
191                front_face: wgpu::FrontFace::Ccw,
192                cull_mode: None,
193                polygon_mode: wgpu::PolygonMode::Fill,
194                unclipped_depth: false,
195                conservative: false,
196            },
197            depth_stencil: None,
198            multisample: wgpu::MultisampleState {
199                count: 1,
200                mask: !0,
201                alpha_to_coverage_enabled: false,
202            },
203            multiview: None,
204            cache: None,
205        });
206
207        Ok(ScatterRenderer {
208            render_pipeline,
209            vertex_buffer: None,
210            vertex_count: 0,
211        })
212    }
213
214    fn render_with_backend(
215        &mut self,
216        backend: &GPUBackend,
217        data: &ChartData,
218        options: &RenderOptions,
219    ) -> Result<(), String> {
220        // Update vertex buffer if data changed
221        <Self as WebRenderer>::update_data(self, backend, data)?;
222
223        let device = backend.device()?;
224        let queue = backend.queue()?;
225        let surface = backend.surface.as_ref().ok_or("Surface not configured")?;
226
227        // Get current texture
228        let frame = surface
229            .get_current_texture()
230            .map_err(|e| format!("Failed to get current texture: {}", e))?;
231
232        let view = frame
233            .texture
234            .create_view(&wgpu::TextureViewDescriptor::default());
235
236        // Create command encoder
237        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
238            label: Some("Render Encoder"),
239        });
240
241        // Render pass
242        {
243            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
244                label: Some("Render Pass"),
245                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
246                    view: &view,
247                    resolve_target: None,
248                    ops: wgpu::Operations {
249                        load: wgpu::LoadOp::Clear(options.clear_color),
250                        store: wgpu::StoreOp::Store,
251                    },
252                })],
253                depth_stencil_attachment: None,
254                timestamp_writes: None,
255                occlusion_query_set: None,
256            });
257
258            self.render_to_pass(&mut render_pass);
259        }
260
261        // Submit commands
262        queue.submit(std::iter::once(encoder.finish()));
263        frame.present();
264
265        Ok(())
266    }
267
268    fn update_data(&mut self, backend: &GPUBackend, data: &ChartData) -> Result<(), String> {
269        if data.vertices.is_empty() {
270            return Ok(());
271        }
272
273        let device = backend.device()?;
274
275        // Create or update vertex buffer
276        self.vertex_buffer = Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
277            label: Some("Vertex Buffer"),
278            contents: bytemuck::cast_slice(&data.vertices),
279            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
280        }));
281        self.vertex_count = data.vertices.len() as u32;
282
283        Ok(())
284    }
285}