Skip to main content

runmat_plot/core/
renderer.rs

1//! WGPU-based rendering backend for high-performance plotting
2//!
3//! This module provides GPU-accelerated rendering using WGPU, supporting
4//! both desktop and web targets for maximum compatibility.
5
6use bytemuck::{Pod, Zeroable};
7use glam::{Mat4, Vec3, Vec4};
8use std::sync::Arc;
9use wgpu::util::DeviceExt;
10
11use crate::core::DepthMode;
12use crate::{core::scene::GpuVertexBuffer, gpu::shaders};
13
14/// Uniforms for the procedural 3D grid plane.
15#[repr(C)]
16#[derive(Clone, Copy, Debug, Pod, Zeroable)]
17pub struct GridUniforms {
18    pub major_step: f32,
19    pub minor_step: f32,
20    pub fade_start: f32,
21    pub fade_end: f32,
22    pub camera_pos: [f32; 3],
23    pub _pad0: f32,
24    pub target_pos: [f32; 3],
25    pub _pad1: f32,
26    pub major_color: [f32; 4],
27    pub minor_color: [f32; 4],
28}
29
30impl Default for GridUniforms {
31    fn default() -> Self {
32        Self {
33            major_step: 1.0,
34            minor_step: 0.1,
35            fade_start: 10.0,
36            fade_end: 15.0,
37            camera_pos: [0.0, 0.0, 0.0],
38            _pad0: 0.0,
39            target_pos: [0.0, 0.0, 0.0],
40            _pad1: 0.0,
41            major_color: [0.90, 0.92, 0.96, 0.30],
42            minor_color: [0.82, 0.84, 0.88, 0.18],
43        }
44    }
45}
46
47/// Vertex data for rendering points, lines, and triangles
48#[repr(C)]
49#[derive(Clone, Copy, Debug, Pod, Zeroable)]
50pub struct Vertex {
51    pub position: [f32; 3],
52    pub color: [f32; 4],
53    pub normal: [f32; 3],
54    pub tex_coords: [f32; 2],
55}
56
57impl Vertex {
58    pub fn new(position: Vec3, color: Vec4) -> Self {
59        Self {
60            position: position.to_array(),
61            color: color.to_array(),
62            normal: [0.0, 0.0, 1.0], // Default normal
63            tex_coords: [0.0, 0.0],  // Default UV
64        }
65    }
66
67    pub fn desc() -> wgpu::VertexBufferLayout<'static> {
68        let stride = std::mem::size_of::<Vertex>() as wgpu::BufferAddress;
69        log::trace!(
70            target: "runmat_plot",
71            "vertex layout: size={}, stride={}",
72            std::mem::size_of::<Vertex>(),
73            stride
74        );
75        wgpu::VertexBufferLayout {
76            array_stride: stride,
77            step_mode: wgpu::VertexStepMode::Vertex,
78            attributes: &[
79                // Position
80                wgpu::VertexAttribute {
81                    offset: 0,
82                    shader_location: 0,
83                    format: wgpu::VertexFormat::Float32x3,
84                },
85                // Color
86                wgpu::VertexAttribute {
87                    offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
88                    shader_location: 1,
89                    format: wgpu::VertexFormat::Float32x4,
90                },
91                // Normal
92                wgpu::VertexAttribute {
93                    offset: std::mem::size_of::<[f32; 7]>() as wgpu::BufferAddress,
94                    shader_location: 2,
95                    format: wgpu::VertexFormat::Float32x3,
96                },
97                // Texture coordinates
98                wgpu::VertexAttribute {
99                    offset: std::mem::size_of::<[f32; 10]>() as wgpu::BufferAddress,
100                    shader_location: 3,
101                    format: wgpu::VertexFormat::Float32x2,
102                },
103            ],
104        }
105    }
106}
107
108/// Uniform buffer for camera and transformation matrices
109#[repr(C)]
110#[derive(Clone, Copy, Debug, Pod, Zeroable)]
111pub struct Uniforms {
112    pub view_proj: [[f32; 4]; 4],
113    pub model: [[f32; 4]; 4],
114    pub normal_matrix: [[f32; 4]; 3], // Use 4x3 for proper alignment instead of 3x3
115}
116
117/// Optimized uniform buffer for direct coordinate transformation rendering
118/// Enables precise viewport-constrained data visualization
119#[repr(C)]
120#[derive(Clone, Copy, Debug, Pod, Zeroable)]
121pub struct DirectUniforms {
122    pub data_min: [f32; 2],     // (x_min, y_min) in data space
123    pub data_max: [f32; 2],     // (x_max, y_max) in data space
124    pub viewport_min: [f32; 2], // NDC coordinates of viewport bottom-left
125    pub viewport_max: [f32; 2], // NDC coordinates of viewport top-right
126    pub viewport_px: [f32; 2],  // viewport size in pixels (width, height)
127}
128
129/// Style uniforms for direct point rendering (scatter markers)
130#[repr(C)]
131#[derive(Clone, Copy, Debug, Pod, Zeroable)]
132pub struct PointStyleUniforms {
133    pub face_color: [f32; 4],
134    pub edge_color: [f32; 4],
135    pub edge_thickness_px: f32,
136    pub marker_shape: u32,
137    pub _pad: [f32; 2],
138}
139
140impl Default for Uniforms {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl Uniforms {
147    pub fn new() -> Self {
148        Self {
149            view_proj: Mat4::IDENTITY.to_cols_array_2d(),
150            model: Mat4::IDENTITY.to_cols_array_2d(),
151            normal_matrix: [
152                [1.0, 0.0, 0.0, 0.0],
153                [0.0, 1.0, 0.0, 0.0],
154                [0.0, 0.0, 1.0, 0.0],
155            ],
156        }
157    }
158
159    pub fn update_view_proj(&mut self, view_proj: Mat4) {
160        self.view_proj = view_proj.to_cols_array_2d();
161    }
162
163    pub fn update_model(&mut self, model: Mat4) {
164        self.model = model.to_cols_array_2d();
165        // Update normal matrix (upper 3x3 of inverse transpose) with proper alignment
166        let normal_mat = model.inverse().transpose();
167        self.normal_matrix = [
168            [
169                normal_mat.x_axis.x,
170                normal_mat.x_axis.y,
171                normal_mat.x_axis.z,
172                0.0,
173            ],
174            [
175                normal_mat.y_axis.x,
176                normal_mat.y_axis.y,
177                normal_mat.y_axis.z,
178                0.0,
179            ],
180            [
181                normal_mat.z_axis.x,
182                normal_mat.z_axis.y,
183                normal_mat.z_axis.z,
184                0.0,
185            ],
186        ];
187    }
188}
189
190impl DirectUniforms {
191    pub fn new(
192        data_min: [f32; 2],
193        data_max: [f32; 2],
194        viewport_min: [f32; 2],
195        viewport_max: [f32; 2],
196        viewport_px: [f32; 2],
197    ) -> Self {
198        Self {
199            data_min,
200            data_max,
201            viewport_min,
202            viewport_max,
203            viewport_px,
204        }
205    }
206}
207
208pub fn marker_shape_code(style: crate::plots::scatter::MarkerStyle) -> u32 {
209    match style {
210        crate::plots::scatter::MarkerStyle::Circle => 0,
211        crate::plots::scatter::MarkerStyle::Square => 1,
212        crate::plots::scatter::MarkerStyle::Triangle => 2,
213        crate::plots::scatter::MarkerStyle::Diamond => 3,
214        crate::plots::scatter::MarkerStyle::Plus => 4,
215        crate::plots::scatter::MarkerStyle::Cross => 5,
216        crate::plots::scatter::MarkerStyle::Star => 6,
217        crate::plots::scatter::MarkerStyle::Hexagon => 7,
218    }
219}
220
221/// Rendering pipeline types
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum PipelineType {
224    Points,
225    Lines,
226    Triangles,
227    Scatter3,
228    Textured,
229}
230
231/// High-performance WGPU renderer for interactive plotting
232pub struct WgpuRenderer {
233    pub device: Arc<wgpu::Device>,
234    pub queue: Arc<wgpu::Queue>,
235    pub surface_config: wgpu::SurfaceConfiguration,
236
237    // Global MSAA sample count for pipelines/attachments
238    pub msaa_sample_count: u32,
239
240    // Rendering pipelines (traditional camera-based)
241    point_pipeline: Option<wgpu::RenderPipeline>,
242    line_pipeline: Option<wgpu::RenderPipeline>,
243    triangle_pipeline: Option<wgpu::RenderPipeline>,
244
245    // Direct rendering pipelines (optimized coordinate transformation)
246    pub direct_line_pipeline: Option<wgpu::RenderPipeline>,
247    pub direct_triangle_pipeline: Option<wgpu::RenderPipeline>,
248    pub direct_point_pipeline: Option<wgpu::RenderPipeline>,
249    image_pipeline: Option<wgpu::RenderPipeline>,
250    image_bind_group_layout: wgpu::BindGroupLayout,
251    image_sampler: wgpu::Sampler,
252    point_style_bind_group_layout: wgpu::BindGroupLayout,
253
254    // Grid helper uniforms/pipeline (3D only)
255    grid_uniform_buffer: wgpu::Buffer,
256    pub grid_uniform_bind_group: wgpu::BindGroup,
257    grid_uniform_bind_group_layout: wgpu::BindGroupLayout,
258    axes_grid_uniform_buffers: Vec<wgpu::Buffer>,
259    axes_grid_uniform_bind_groups: Vec<wgpu::BindGroup>,
260    grid_plane_pipeline: Option<wgpu::RenderPipeline>,
261
262    // Uniform resources (traditional)
263    uniform_buffer: wgpu::Buffer,
264    uniform_bind_group: wgpu::BindGroup,
265    uniform_bind_group_layout: wgpu::BindGroupLayout,
266    axes_uniform_buffers: Vec<wgpu::Buffer>,
267    axes_uniform_bind_groups: Vec<wgpu::BindGroup>,
268
269    // Direct uniform resources (optimized coordinate transformation)
270    direct_uniform_buffer: wgpu::Buffer,
271    pub direct_uniform_bind_group: wgpu::BindGroup,
272    direct_uniform_bind_group_layout: wgpu::BindGroupLayout,
273    axes_direct_uniform_buffers: Vec<wgpu::Buffer>,
274    axes_direct_uniform_bind_groups: Vec<wgpu::BindGroup>,
275
276    // Current uniforms
277    uniforms: Uniforms,
278    direct_uniforms: DirectUniforms,
279
280    // Depth resources (used by camera-based 3D rendering paths)
281    depth_texture: Option<wgpu::Texture>,
282    depth_view: Option<Arc<wgpu::TextureView>>,
283    depth_extent: (u32, u32, u32), // (w, h, sample_count)
284
285    // MSAA color resources for resolving into single-sampled targets.
286    msaa_color_texture: Option<wgpu::Texture>,
287    msaa_color_view: Option<Arc<wgpu::TextureView>>,
288    msaa_color_extent: (u32, u32, u32), // (w, h, sample_count)
289
290    /// Depth mapping mode for camera-based 3D pipelines.
291    pub depth_mode: DepthMode,
292}
293
294impl WgpuRenderer {
295    fn create_uniform_bind_group_for_buffer(
296        &self,
297        buffer: &wgpu::Buffer,
298        label: &str,
299    ) -> wgpu::BindGroup {
300        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
301            layout: &self.uniform_bind_group_layout,
302            entries: &[wgpu::BindGroupEntry {
303                binding: 0,
304                resource: buffer.as_entire_binding(),
305            }],
306            label: Some(label),
307        })
308    }
309
310    fn create_direct_uniform_bind_group_for_buffer(
311        &self,
312        buffer: &wgpu::Buffer,
313        label: &str,
314    ) -> wgpu::BindGroup {
315        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
316            layout: &self.direct_uniform_bind_group_layout,
317            entries: &[wgpu::BindGroupEntry {
318                binding: 0,
319                resource: buffer.as_entire_binding(),
320            }],
321            label: Some(label),
322        })
323    }
324
325    fn create_grid_uniform_bind_group_for_buffer(
326        &self,
327        buffer: &wgpu::Buffer,
328        label: &str,
329    ) -> wgpu::BindGroup {
330        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
331            label: Some(label),
332            layout: &self.grid_uniform_bind_group_layout,
333            entries: &[wgpu::BindGroupEntry {
334                binding: 0,
335                resource: buffer.as_entire_binding(),
336            }],
337        })
338    }
339
340    pub fn ensure_axes_uniform_capacity(&mut self, axes_count: usize) {
341        while self.axes_uniform_buffers.len() < axes_count {
342            let idx = self.axes_uniform_buffers.len();
343            let buffer = self
344                .device
345                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
346                    label: Some(&format!("Axes Uniform Buffer {idx}")),
347                    contents: bytemuck::cast_slice(&[Uniforms::new()]),
348                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
349                });
350            let bind_group = self.create_uniform_bind_group_for_buffer(
351                &buffer,
352                &format!("axes_uniform_bind_group_{idx}"),
353            );
354            self.axes_uniform_buffers.push(buffer);
355            self.axes_uniform_bind_groups.push(bind_group);
356        }
357        while self.axes_direct_uniform_buffers.len() < axes_count {
358            let idx = self.axes_direct_uniform_buffers.len();
359            let buffer = self
360                .device
361                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
362                    label: Some(&format!("Axes Direct Uniform Buffer {idx}")),
363                    contents: bytemuck::cast_slice(&[DirectUniforms::new(
364                        [0.0, 0.0],
365                        [1.0, 1.0],
366                        [-1.0, -1.0],
367                        [1.0, 1.0],
368                        [1.0, 1.0],
369                    )]),
370                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
371                });
372            let bind_group = self.create_direct_uniform_bind_group_for_buffer(
373                &buffer,
374                &format!("axes_direct_uniform_bind_group_{idx}"),
375            );
376            self.axes_direct_uniform_buffers.push(buffer);
377            self.axes_direct_uniform_bind_groups.push(bind_group);
378        }
379        while self.axes_grid_uniform_buffers.len() < axes_count {
380            let idx = self.axes_grid_uniform_buffers.len();
381            let buffer = self
382                .device
383                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
384                    label: Some(&format!("Axes Grid Uniform Buffer {idx}")),
385                    contents: bytemuck::cast_slice(&[GridUniforms::default()]),
386                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
387                });
388            let bind_group = self.create_grid_uniform_bind_group_for_buffer(
389                &buffer,
390                &format!("axes_grid_uniform_bind_group_{idx}"),
391            );
392            self.axes_grid_uniform_buffers.push(buffer);
393            self.axes_grid_uniform_bind_groups.push(bind_group);
394        }
395    }
396
397    /// Create a new WGPU renderer
398    pub async fn new(
399        device: Arc<wgpu::Device>,
400        queue: Arc<wgpu::Queue>,
401        surface_config: wgpu::SurfaceConfiguration,
402    ) -> Self {
403        // Create uniform buffer
404        let uniforms = Uniforms::new();
405        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
406            label: Some("Uniform Buffer"),
407            contents: bytemuck::cast_slice(&[uniforms]),
408            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
409        });
410
411        // Create bind group layout for uniforms
412        let uniform_bind_group_layout =
413            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
414                entries: &[wgpu::BindGroupLayoutEntry {
415                    binding: 0,
416                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
417                    ty: wgpu::BindingType::Buffer {
418                        ty: wgpu::BufferBindingType::Uniform,
419                        has_dynamic_offset: false,
420                        min_binding_size: None,
421                    },
422                    count: None,
423                }],
424                label: Some("uniform_bind_group_layout"),
425            });
426
427        // Create bind group
428        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
429            layout: &uniform_bind_group_layout,
430            entries: &[wgpu::BindGroupEntry {
431                binding: 0,
432                resource: uniform_buffer.as_entire_binding(),
433            }],
434            label: Some("uniform_bind_group"),
435        });
436
437        // Create direct rendering uniform buffer
438        let direct_uniforms = DirectUniforms::new(
439            [0.0, 0.0],   // data_min
440            [1.0, 1.0],   // data_max
441            [-1.0, -1.0], // viewport_min (full NDC)
442            [1.0, 1.0],   // viewport_max (full NDC)
443            [1.0, 1.0],   // viewport_px
444        );
445        let direct_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
446            label: Some("Direct Uniform Buffer"),
447            contents: bytemuck::cast_slice(&[direct_uniforms]),
448            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
449        });
450
451        // Create direct bind group layout for uniforms
452        let direct_uniform_bind_group_layout =
453            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
454                entries: &[wgpu::BindGroupLayoutEntry {
455                    binding: 0,
456                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
457                    ty: wgpu::BindingType::Buffer {
458                        ty: wgpu::BufferBindingType::Uniform,
459                        has_dynamic_offset: false,
460                        min_binding_size: None,
461                    },
462                    count: None,
463                }],
464                label: Some("direct_uniform_bind_group_layout"),
465            });
466
467        // Create direct bind group
468        let direct_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
469            layout: &direct_uniform_bind_group_layout,
470            entries: &[wgpu::BindGroupEntry {
471                binding: 0,
472                resource: direct_uniform_buffer.as_entire_binding(),
473            }],
474            label: Some("direct_uniform_bind_group"),
475        });
476
477        let image_bind_group_layout =
478            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
479                label: Some("Image Bind Group Layout"),
480                entries: &[
481                    // sampler
482                    wgpu::BindGroupLayoutEntry {
483                        binding: 0,
484                        visibility: wgpu::ShaderStages::FRAGMENT,
485                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
486                        count: None,
487                    },
488                    // texture view
489                    wgpu::BindGroupLayoutEntry {
490                        binding: 1,
491                        visibility: wgpu::ShaderStages::FRAGMENT,
492                        ty: wgpu::BindingType::Texture {
493                            multisampled: false,
494                            view_dimension: wgpu::TextureViewDimension::D2,
495                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
496                        },
497                        count: None,
498                    },
499                ],
500            });
501
502        let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
503            label: Some("Image Sampler"),
504            address_mode_u: wgpu::AddressMode::ClampToEdge,
505            address_mode_v: wgpu::AddressMode::ClampToEdge,
506            address_mode_w: wgpu::AddressMode::ClampToEdge,
507            mag_filter: wgpu::FilterMode::Linear,
508            min_filter: wgpu::FilterMode::Linear,
509            mipmap_filter: wgpu::FilterMode::Nearest,
510            ..Default::default()
511        });
512
513        // Point style bind group layout (face/edge colors, thickness, shape)
514        let point_style_bind_group_layout =
515            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
516                label: Some("Point Style Bind Group Layout"),
517                entries: &[wgpu::BindGroupLayoutEntry {
518                    binding: 0,
519                    visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX,
520                    ty: wgpu::BindingType::Buffer {
521                        ty: wgpu::BufferBindingType::Uniform,
522                        has_dynamic_offset: false,
523                        min_binding_size: None,
524                    },
525                    count: None,
526                }],
527            });
528
529        // Grid uniforms (3D helper plane)
530        let grid_uniforms = GridUniforms::default();
531        let grid_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
532            label: Some("Grid Uniform Buffer"),
533            contents: bytemuck::cast_slice(&[grid_uniforms]),
534            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
535        });
536        let grid_uniform_bind_group_layout =
537            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
538                entries: &[wgpu::BindGroupLayoutEntry {
539                    binding: 0,
540                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
541                    ty: wgpu::BindingType::Buffer {
542                        ty: wgpu::BufferBindingType::Uniform,
543                        has_dynamic_offset: false,
544                        min_binding_size: None,
545                    },
546                    count: None,
547                }],
548                label: Some("grid_uniform_bind_group_layout"),
549            });
550        let grid_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
551            label: Some("grid_uniform_bind_group"),
552            layout: &grid_uniform_bind_group_layout,
553            entries: &[wgpu::BindGroupEntry {
554                binding: 0,
555                resource: grid_uniform_buffer.as_entire_binding(),
556            }],
557        });
558
559        Self {
560            device,
561            queue,
562            surface_config,
563            msaa_sample_count: 1,
564            point_pipeline: None,
565            line_pipeline: None,
566            triangle_pipeline: None,
567            direct_line_pipeline: None,
568            direct_triangle_pipeline: None,
569            direct_point_pipeline: None,
570            image_pipeline: None,
571            image_bind_group_layout,
572            image_sampler,
573            point_style_bind_group_layout,
574            grid_uniform_buffer,
575            grid_uniform_bind_group,
576            grid_uniform_bind_group_layout,
577            axes_grid_uniform_buffers: Vec::new(),
578            axes_grid_uniform_bind_groups: Vec::new(),
579            grid_plane_pipeline: None,
580            uniform_buffer,
581            uniform_bind_group,
582            uniform_bind_group_layout,
583            axes_uniform_buffers: Vec::new(),
584            axes_uniform_bind_groups: Vec::new(),
585            direct_uniform_buffer,
586            direct_uniform_bind_group,
587            direct_uniform_bind_group_layout,
588            axes_direct_uniform_buffers: Vec::new(),
589            axes_direct_uniform_bind_groups: Vec::new(),
590            uniforms,
591            direct_uniforms,
592            depth_texture: None,
593            depth_view: None,
594            depth_extent: (0, 0, 0),
595            msaa_color_texture: None,
596            msaa_color_view: None,
597            msaa_color_extent: (0, 0, 0),
598            depth_mode: DepthMode::default(),
599        }
600    }
601
602    pub fn update_grid_uniforms(&mut self, uniforms: GridUniforms) {
603        self.queue.write_buffer(
604            &self.grid_uniform_buffer,
605            0,
606            bytemuck::cast_slice(&[uniforms]),
607        );
608        self.ensure_axes_uniform_capacity(1);
609        self.queue.write_buffer(
610            &self.axes_grid_uniform_buffers[0],
611            0,
612            bytemuck::cast_slice(&[uniforms]),
613        );
614    }
615
616    pub fn update_grid_uniforms_for_axes(&mut self, axes_index: usize, uniforms: GridUniforms) {
617        self.ensure_axes_uniform_capacity(axes_index + 1);
618        self.queue.write_buffer(
619            &self.axes_grid_uniform_buffers[axes_index],
620            0,
621            bytemuck::cast_slice(&[uniforms]),
622        );
623    }
624
625    pub fn get_grid_uniform_bind_group_for_axes(&self, axes_index: usize) -> &wgpu::BindGroup {
626        self.axes_grid_uniform_bind_groups
627            .get(axes_index)
628            .unwrap_or(&self.grid_uniform_bind_group)
629    }
630
631    pub fn set_depth_mode(&mut self, mode: DepthMode) {
632        if self.depth_mode != mode {
633            self.depth_mode = mode;
634            // Pipelines depend on depth compare; rebuild.
635            self.point_pipeline = None;
636            self.line_pipeline = None;
637            self.triangle_pipeline = None;
638            self.direct_line_pipeline = None;
639            self.direct_triangle_pipeline = None;
640            self.direct_point_pipeline = None;
641            self.image_pipeline = None;
642            self.grid_plane_pipeline = None;
643        }
644    }
645
646    /// Ensure MSAA state matches requested count. Rebuild pipelines if changed.
647    pub fn ensure_msaa(&mut self, requested_count: u32) {
648        let clamped = match requested_count {
649            0 => 1,
650            1 => 1,
651            2 => 2,
652            4 => 4,
653            8 => 8,
654            16 => 8, // clamp to 8 for portability
655            _ => 4,  // default reasonable MSAA
656        };
657        if self.msaa_sample_count != clamped {
658            self.msaa_sample_count = clamped;
659            // Drop pipelines so they are recreated with new MSAA count
660            self.point_pipeline = None;
661            self.line_pipeline = None;
662            self.triangle_pipeline = None;
663            self.direct_line_pipeline = None;
664            self.direct_triangle_pipeline = None;
665            self.direct_point_pipeline = None;
666            self.image_pipeline = None;
667            self.grid_plane_pipeline = None;
668            // Depth attachment must match sample count.
669            self.depth_texture = None;
670            self.depth_view = None;
671            self.depth_extent = (0, 0, 0);
672            self.msaa_color_texture = None;
673            self.msaa_color_view = None;
674            self.msaa_color_extent = (0, 0, 0);
675        }
676    }
677
678    fn depth_format() -> wgpu::TextureFormat {
679        // Prefer a higher-precision depth buffer on native; keep a web-friendly format on wasm.
680        #[cfg(target_arch = "wasm32")]
681        {
682            wgpu::TextureFormat::Depth24Plus
683        }
684        #[cfg(not(target_arch = "wasm32"))]
685        {
686            wgpu::TextureFormat::Depth32Float
687        }
688    }
689
690    fn depth_compare(&self) -> wgpu::CompareFunction {
691        match self.depth_mode {
692            DepthMode::Standard => wgpu::CompareFunction::LessEqual,
693            DepthMode::ReversedZ => wgpu::CompareFunction::GreaterEqual,
694        }
695    }
696
697    pub fn ensure_depth_view(&mut self) -> Arc<wgpu::TextureView> {
698        let width = self.surface_config.width.max(1);
699        let height = self.surface_config.height.max(1);
700        let samples = self.msaa_sample_count.max(1);
701        if self.depth_view.is_none() || self.depth_extent != (width, height, samples) {
702            let texture = self.device.create_texture(&wgpu::TextureDescriptor {
703                label: Some("runmat_depth_texture"),
704                size: wgpu::Extent3d {
705                    width,
706                    height,
707                    depth_or_array_layers: 1,
708                },
709                mip_level_count: 1,
710                sample_count: samples,
711                dimension: wgpu::TextureDimension::D2,
712                format: Self::depth_format(),
713                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
714                view_formats: &[],
715            });
716            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
717            self.depth_texture = Some(texture);
718            self.depth_view = Some(Arc::new(view));
719            self.depth_extent = (width, height, samples);
720        }
721        self.depth_view
722            .as_ref()
723            .cloned()
724            .expect("depth view missing")
725    }
726
727    pub fn ensure_msaa_color_view(&mut self) -> Arc<wgpu::TextureView> {
728        let width = self.surface_config.width.max(1);
729        let height = self.surface_config.height.max(1);
730        let samples = self.msaa_sample_count.max(1);
731        if self.msaa_color_view.is_none() || self.msaa_color_extent != (width, height, samples) {
732            let texture = self.device.create_texture(&wgpu::TextureDescriptor {
733                label: Some("runmat_msaa_color_plot"),
734                size: wgpu::Extent3d {
735                    width,
736                    height,
737                    depth_or_array_layers: 1,
738                },
739                mip_level_count: 1,
740                sample_count: samples,
741                dimension: wgpu::TextureDimension::D2,
742                format: self.surface_config.format,
743                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
744                view_formats: &[],
745            });
746            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
747            self.msaa_color_texture = Some(texture);
748            self.msaa_color_view = Some(Arc::new(view));
749            self.msaa_color_extent = (width, height, samples);
750        }
751        self.msaa_color_view
752            .as_ref()
753            .cloned()
754            .expect("msaa color view missing")
755    }
756
757    /// Create a GPU texture and bind group for an RGBA8 image
758    pub fn create_image_texture_and_bind_group(
759        &self,
760        width: u32,
761        height: u32,
762        data: &[u8],
763    ) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) {
764        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
765            label: Some("Image Texture"),
766            size: wgpu::Extent3d {
767                width,
768                height,
769                depth_or_array_layers: 1,
770            },
771            mip_level_count: 1,
772            sample_count: 1,
773            dimension: wgpu::TextureDimension::D2,
774            format: wgpu::TextureFormat::Rgba8UnormSrgb,
775            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
776            view_formats: &[],
777        });
778        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
779        // Upload data
780        self.queue.write_texture(
781            wgpu::ImageCopyTexture {
782                texture: &texture,
783                mip_level: 0,
784                origin: wgpu::Origin3d::ZERO,
785                aspect: wgpu::TextureAspect::All,
786            },
787            data,
788            wgpu::ImageDataLayout {
789                offset: 0,
790                bytes_per_row: Some(4 * width),
791                rows_per_image: Some(height),
792            },
793            wgpu::Extent3d {
794                width,
795                height,
796                depth_or_array_layers: 1,
797            },
798        );
799        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
800            label: Some("Image Bind Group"),
801            layout: &self.image_bind_group_layout,
802            entries: &[
803                wgpu::BindGroupEntry {
804                    binding: 0,
805                    resource: wgpu::BindingResource::Sampler(&self.image_sampler),
806                },
807                wgpu::BindGroupEntry {
808                    binding: 1,
809                    resource: wgpu::BindingResource::TextureView(&texture_view),
810                },
811            ],
812        });
813        (texture, texture_view, bind_group)
814    }
815
816    /// Create a vertex buffer from vertex data
817    pub fn create_vertex_buffer(&self, vertices: &[Vertex]) -> wgpu::Buffer {
818        self.device
819            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
820                label: Some("Vertex Buffer"),
821                contents: bytemuck::cast_slice(vertices),
822                usage: wgpu::BufferUsages::VERTEX,
823            })
824    }
825
826    /// Choose the most efficient vertex buffer source for the provided data.
827    pub fn vertex_buffer_from_sources(
828        &self,
829        gpu: Option<&GpuVertexBuffer>,
830        cpu_vertices: &[Vertex],
831    ) -> Option<Arc<wgpu::Buffer>> {
832        if let Some(buffer) = gpu {
833            Some(buffer.buffer.clone())
834        } else if !cpu_vertices.is_empty() {
835            Some(Arc::new(self.create_vertex_buffer(cpu_vertices)))
836        } else {
837            None
838        }
839    }
840
841    /// Create a vertex buffer for direct points by expanding each point to a quad.
842    /// This reuses Vertex but encodes corner index via tex_coords and marker size in normal.z
843    pub fn create_direct_point_vertices(&self, points: &[Vertex], size_px: f32) -> Vec<Vertex> {
844        let corners: [[f32; 2]; 6] = [
845            [-1.0, -1.0],
846            [1.0, -1.0],
847            [1.0, 1.0],
848            [-1.0, -1.0],
849            [1.0, 1.0],
850            [-1.0, 1.0],
851        ];
852        let mut out = Vec::with_capacity(points.len() * 6);
853        for p in points {
854            for c in corners {
855                let mut v = *p;
856                v.tex_coords = c; // tells shader which corner
857                let sz = if size_px > 0.0 { size_px } else { p.normal[2] };
858                v.normal = [p.normal[0], p.normal[1], sz];
859                out.push(v);
860            }
861        }
862        out
863    }
864
865    /// Create an index buffer from index data
866    pub fn create_index_buffer(&self, indices: &[u32]) -> wgpu::Buffer {
867        self.device
868            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
869                label: Some("Index Buffer"),
870                contents: bytemuck::cast_slice(indices),
871                usage: wgpu::BufferUsages::INDEX,
872            })
873    }
874
875    /// Update uniform buffer with new matrices
876    pub fn update_uniforms(&mut self, view_proj: Mat4, model: Mat4) {
877        self.uniforms.update_view_proj(view_proj);
878        self.uniforms.update_model(model);
879
880        self.queue.write_buffer(
881            &self.uniform_buffer,
882            0,
883            bytemuck::cast_slice(&[self.uniforms]),
884        );
885        self.ensure_axes_uniform_capacity(1);
886        self.queue.write_buffer(
887            &self.axes_uniform_buffers[0],
888            0,
889            bytemuck::cast_slice(&[self.uniforms]),
890        );
891    }
892
893    pub fn update_uniforms_for_axes(&mut self, axes_index: usize, view_proj: Mat4, model: Mat4) {
894        self.ensure_axes_uniform_capacity(axes_index + 1);
895        let mut uniforms = Uniforms::new();
896        uniforms.update_view_proj(view_proj);
897        uniforms.update_model(model);
898        self.queue.write_buffer(
899            &self.axes_uniform_buffers[axes_index],
900            0,
901            bytemuck::cast_slice(&[uniforms]),
902        );
903    }
904
905    /// Get the uniform bind group for rendering
906    pub fn get_uniform_bind_group(&self) -> &wgpu::BindGroup {
907        &self.uniform_bind_group
908    }
909
910    pub fn get_uniform_bind_group_for_axes(&self, axes_index: usize) -> &wgpu::BindGroup {
911        self.axes_uniform_bind_groups
912            .get(axes_index)
913            .unwrap_or(&self.uniform_bind_group)
914    }
915
916    /// Ensure pipeline exists for the specified type
917    pub fn ensure_pipeline(&mut self, pipeline_type: PipelineType) {
918        match pipeline_type {
919            PipelineType::Points => {
920                if self.point_pipeline.is_none() {
921                    self.point_pipeline = Some(self.create_point_pipeline());
922                }
923            }
924            PipelineType::Lines => {
925                if self.line_pipeline.is_none() {
926                    self.line_pipeline = Some(self.create_line_pipeline());
927                }
928            }
929            PipelineType::Triangles => {
930                if self.triangle_pipeline.is_none() {
931                    self.triangle_pipeline = Some(self.create_triangle_pipeline());
932                }
933            }
934            PipelineType::Scatter3 => {
935                // For now, use points pipeline - will optimize later
936                self.ensure_pipeline(PipelineType::Points);
937            }
938            PipelineType::Textured => {
939                if self.image_pipeline.is_none() {
940                    self.image_pipeline = Some(self.create_image_pipeline());
941                }
942            }
943        }
944    }
945
946    /// Get a pipeline reference (pipeline must already exist)
947    pub fn get_pipeline(&self, pipeline_type: PipelineType) -> &wgpu::RenderPipeline {
948        match pipeline_type {
949            PipelineType::Points => self.point_pipeline.as_ref().unwrap(),
950            PipelineType::Lines => self.line_pipeline.as_ref().unwrap(),
951            PipelineType::Triangles => self.triangle_pipeline.as_ref().unwrap(),
952            PipelineType::Scatter3 => self.get_pipeline(PipelineType::Points),
953            PipelineType::Textured => self.image_pipeline.as_ref().unwrap(),
954        }
955    }
956
957    pub fn ensure_grid_plane_pipeline(&mut self) {
958        if self.grid_plane_pipeline.is_none() {
959            self.grid_plane_pipeline = Some(self.create_grid_plane_pipeline());
960        }
961    }
962
963    pub fn grid_plane_pipeline(&self) -> Option<&wgpu::RenderPipeline> {
964        self.grid_plane_pipeline.as_ref()
965    }
966
967    /// Create point rendering pipeline
968    fn create_point_pipeline(&self) -> wgpu::RenderPipeline {
969        let shader = self
970            .device
971            .create_shader_module(wgpu::ShaderModuleDescriptor {
972                label: Some("Point Shader"),
973                source: wgpu::ShaderSource::Wgsl(shaders::vertex::POINT.into()),
974            });
975
976        let pipeline_layout = self
977            .device
978            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
979                label: Some("Point Pipeline Layout"),
980                bind_group_layouts: &[&self.uniform_bind_group_layout],
981                push_constant_ranges: &[],
982            });
983
984        self.device
985            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
986                label: Some("Point Pipeline"),
987                layout: Some(&pipeline_layout),
988                vertex: wgpu::VertexState {
989                    module: &shader,
990                    entry_point: "vs_main",
991                    buffers: &[Vertex::desc()],
992                },
993                fragment: Some(wgpu::FragmentState {
994                    module: &shader,
995                    entry_point: "fs_main",
996                    targets: &[Some(wgpu::ColorTargetState {
997                        format: self.surface_config.format,
998                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
999                        write_mask: wgpu::ColorWrites::ALL,
1000                    })],
1001                }),
1002                primitive: wgpu::PrimitiveState {
1003                    topology: wgpu::PrimitiveTopology::PointList,
1004                    strip_index_format: None,
1005                    front_face: wgpu::FrontFace::Ccw,
1006                    cull_mode: None,
1007                    polygon_mode: wgpu::PolygonMode::Fill,
1008                    unclipped_depth: false,
1009                    conservative: false,
1010                },
1011                depth_stencil: Some(wgpu::DepthStencilState {
1012                    format: Self::depth_format(),
1013                    depth_write_enabled: true,
1014                    depth_compare: self.depth_compare(),
1015                    stencil: wgpu::StencilState::default(),
1016                    bias: wgpu::DepthBiasState::default(),
1017                }),
1018                multisample: wgpu::MultisampleState {
1019                    count: self.msaa_sample_count,
1020                    mask: !0,
1021                    alpha_to_coverage_enabled: false,
1022                },
1023                multiview: None,
1024            })
1025    }
1026
1027    /// Create line rendering pipeline
1028    fn create_line_pipeline(&self) -> wgpu::RenderPipeline {
1029        let shader = self
1030            .device
1031            .create_shader_module(wgpu::ShaderModuleDescriptor {
1032                label: Some("Line Shader"),
1033                source: wgpu::ShaderSource::Wgsl(shaders::vertex::LINE.into()),
1034            });
1035
1036        let pipeline_layout = self
1037            .device
1038            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1039                label: Some("Line Pipeline Layout"),
1040                bind_group_layouts: &[&self.uniform_bind_group_layout],
1041                push_constant_ranges: &[],
1042            });
1043
1044        self.device
1045            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1046                label: Some("Line Pipeline"),
1047                layout: Some(&pipeline_layout),
1048                vertex: wgpu::VertexState {
1049                    module: &shader,
1050                    entry_point: "vs_main",
1051                    buffers: &[Vertex::desc()],
1052                },
1053                fragment: Some(wgpu::FragmentState {
1054                    module: &shader,
1055                    entry_point: "fs_main",
1056                    targets: &[Some(wgpu::ColorTargetState {
1057                        format: self.surface_config.format,
1058                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1059                        write_mask: wgpu::ColorWrites::ALL,
1060                    })],
1061                }),
1062                primitive: wgpu::PrimitiveState {
1063                    topology: wgpu::PrimitiveTopology::LineList,
1064                    strip_index_format: None,
1065                    front_face: wgpu::FrontFace::Ccw,
1066                    cull_mode: None,
1067                    polygon_mode: wgpu::PolygonMode::Fill,
1068                    unclipped_depth: false,
1069                    conservative: false,
1070                },
1071                depth_stencil: Some(wgpu::DepthStencilState {
1072                    format: Self::depth_format(),
1073                    depth_write_enabled: true,
1074                    depth_compare: self.depth_compare(),
1075                    stencil: wgpu::StencilState::default(),
1076                    bias: wgpu::DepthBiasState::default(),
1077                }),
1078                multisample: wgpu::MultisampleState {
1079                    count: self.msaa_sample_count,
1080                    mask: !0,
1081                    alpha_to_coverage_enabled: false,
1082                },
1083                multiview: None,
1084            })
1085    }
1086
1087    /// Create optimized direct rendering pipeline for precise viewport mapping
1088    fn create_direct_line_pipeline(&self) -> wgpu::RenderPipeline {
1089        let shader = self
1090            .device
1091            .create_shader_module(wgpu::ShaderModuleDescriptor {
1092                label: Some("Direct Line Shader"),
1093                source: wgpu::ShaderSource::Wgsl(shaders::vertex::LINE_DIRECT.into()),
1094            });
1095
1096        let pipeline_layout = self
1097            .device
1098            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1099                label: Some("Direct Line Pipeline Layout"),
1100                bind_group_layouts: &[&self.direct_uniform_bind_group_layout],
1101                push_constant_ranges: &[],
1102            });
1103
1104        self.device
1105            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1106                label: Some("Direct Line Pipeline"),
1107                layout: Some(&pipeline_layout),
1108                vertex: wgpu::VertexState {
1109                    module: &shader,
1110                    entry_point: "vs_main",
1111                    buffers: &[Vertex::desc()],
1112                },
1113                fragment: Some(wgpu::FragmentState {
1114                    module: &shader,
1115                    entry_point: "fs_main",
1116                    targets: &[Some(wgpu::ColorTargetState {
1117                        format: self.surface_config.format,
1118                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1119                        write_mask: wgpu::ColorWrites::ALL,
1120                    })],
1121                }),
1122                primitive: wgpu::PrimitiveState {
1123                    topology: wgpu::PrimitiveTopology::LineList,
1124                    strip_index_format: None,
1125                    front_face: wgpu::FrontFace::Ccw,
1126                    cull_mode: None,
1127                    polygon_mode: wgpu::PolygonMode::Fill,
1128                    unclipped_depth: false,
1129                    conservative: false,
1130                },
1131                // This pipeline is used inside a render pass that has a depth attachment.
1132                // To be compatible with that pass, we must specify a matching depth format.
1133                // Use CompareFunction::Always + no writes to effectively disable depth testing.
1134                depth_stencil: Some(wgpu::DepthStencilState {
1135                    format: Self::depth_format(),
1136                    depth_write_enabled: false,
1137                    depth_compare: wgpu::CompareFunction::Always,
1138                    stencil: wgpu::StencilState::default(),
1139                    bias: wgpu::DepthBiasState::default(),
1140                }),
1141                multisample: wgpu::MultisampleState {
1142                    count: self.msaa_sample_count,
1143                    mask: !0,
1144                    alpha_to_coverage_enabled: false,
1145                },
1146                multiview: None,
1147            })
1148    }
1149
1150    /// Create optimized direct triangle pipeline (2D fills) for precise viewport mapping
1151    fn create_direct_triangle_pipeline(&self) -> wgpu::RenderPipeline {
1152        let shader = self
1153            .device
1154            .create_shader_module(wgpu::ShaderModuleDescriptor {
1155                label: Some("Direct Triangle Shader"),
1156                source: wgpu::ShaderSource::Wgsl(shaders::vertex::LINE_DIRECT.into()),
1157            });
1158
1159        let pipeline_layout = self
1160            .device
1161            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1162                label: Some("Direct Triangle Pipeline Layout"),
1163                bind_group_layouts: &[&self.direct_uniform_bind_group_layout],
1164                push_constant_ranges: &[],
1165            });
1166
1167        self.device
1168            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1169                label: Some("Direct Triangle Pipeline"),
1170                layout: Some(&pipeline_layout),
1171                vertex: wgpu::VertexState {
1172                    module: &shader,
1173                    entry_point: "vs_main",
1174                    buffers: &[Vertex::desc()],
1175                },
1176                fragment: Some(wgpu::FragmentState {
1177                    module: &shader,
1178                    entry_point: "fs_main",
1179                    targets: &[Some(wgpu::ColorTargetState {
1180                        format: self.surface_config.format,
1181                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1182                        write_mask: wgpu::ColorWrites::ALL,
1183                    })],
1184                }),
1185                primitive: wgpu::PrimitiveState {
1186                    topology: wgpu::PrimitiveTopology::TriangleList,
1187                    strip_index_format: None,
1188                    front_face: wgpu::FrontFace::Ccw,
1189                    cull_mode: None,
1190                    polygon_mode: wgpu::PolygonMode::Fill,
1191                    unclipped_depth: false,
1192                    conservative: false,
1193                },
1194                depth_stencil: Some(wgpu::DepthStencilState {
1195                    format: Self::depth_format(),
1196                    depth_write_enabled: false,
1197                    depth_compare: wgpu::CompareFunction::Always,
1198                    stencil: wgpu::StencilState::default(),
1199                    bias: wgpu::DepthBiasState::default(),
1200                }),
1201                multisample: wgpu::MultisampleState {
1202                    count: self.msaa_sample_count,
1203                    mask: !0,
1204                    alpha_to_coverage_enabled: false,
1205                },
1206                multiview: None,
1207            })
1208    }
1209
1210    /// Create optimized direct point pipeline (instanced quads per point)
1211    fn create_direct_point_pipeline(&self) -> wgpu::RenderPipeline {
1212        let shader = self
1213            .device
1214            .create_shader_module(wgpu::ShaderModuleDescriptor {
1215                label: Some("Direct Point Shader"),
1216                source: wgpu::ShaderSource::Wgsl(shaders::vertex::POINT_DIRECT.into()),
1217            });
1218
1219        let pipeline_layout = self
1220            .device
1221            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1222                label: Some("Direct Point Pipeline Layout"),
1223                bind_group_layouts: &[
1224                    &self.direct_uniform_bind_group_layout,
1225                    &self.point_style_bind_group_layout,
1226                ],
1227                push_constant_ranges: &[],
1228            });
1229
1230        self.device
1231            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1232                label: Some("Direct Point Pipeline"),
1233                layout: Some(&pipeline_layout),
1234                vertex: wgpu::VertexState {
1235                    module: &shader,
1236                    entry_point: "vs_main",
1237                    buffers: &[Vertex::desc()],
1238                },
1239                fragment: Some(wgpu::FragmentState {
1240                    module: &shader,
1241                    entry_point: "fs_main",
1242                    targets: &[Some(wgpu::ColorTargetState {
1243                        format: self.surface_config.format,
1244                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1245                        write_mask: wgpu::ColorWrites::ALL,
1246                    })],
1247                }),
1248                primitive: wgpu::PrimitiveState {
1249                    topology: wgpu::PrimitiveTopology::TriangleList,
1250                    strip_index_format: None,
1251                    front_face: wgpu::FrontFace::Ccw,
1252                    cull_mode: None,
1253                    polygon_mode: wgpu::PolygonMode::Fill,
1254                    unclipped_depth: false,
1255                    conservative: false,
1256                },
1257                depth_stencil: Some(wgpu::DepthStencilState {
1258                    format: Self::depth_format(),
1259                    depth_write_enabled: false,
1260                    depth_compare: wgpu::CompareFunction::Always,
1261                    stencil: wgpu::StencilState::default(),
1262                    bias: wgpu::DepthBiasState::default(),
1263                }),
1264                multisample: wgpu::MultisampleState {
1265                    count: self.msaa_sample_count,
1266                    mask: !0,
1267                    alpha_to_coverage_enabled: false,
1268                },
1269                multiview: None,
1270            })
1271    }
1272
1273    /// Create style bind group for scatter points. Returns (buffer, bind_group).
1274    pub fn create_point_style_bind_group(
1275        &self,
1276        style: PointStyleUniforms,
1277    ) -> (wgpu::Buffer, wgpu::BindGroup) {
1278        let buffer = self
1279            .device
1280            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1281                label: Some("Point Style Uniform Buffer"),
1282                contents: bytemuck::bytes_of(&style),
1283                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1284            });
1285        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1286            label: Some("Point Style Bind Group"),
1287            layout: &self.point_style_bind_group_layout,
1288            entries: &[wgpu::BindGroupEntry {
1289                binding: 0,
1290                resource: buffer.as_entire_binding(),
1291            }],
1292        });
1293        (buffer, bind_group)
1294    }
1295
1296    /// Create textured image pipeline (direct viewport mapping + sampled texture)
1297    fn create_image_pipeline(&self) -> wgpu::RenderPipeline {
1298        let shader = self
1299            .device
1300            .create_shader_module(wgpu::ShaderModuleDescriptor {
1301                label: Some("Image Direct Shader"),
1302                source: wgpu::ShaderSource::Wgsl(shaders::vertex::IMAGE_DIRECT.into()),
1303            });
1304
1305        let pipeline_layout = self
1306            .device
1307            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1308                label: Some("Image Pipeline Layout"),
1309                bind_group_layouts: &[
1310                    &self.direct_uniform_bind_group_layout,
1311                    &self.image_bind_group_layout,
1312                ],
1313                push_constant_ranges: &[],
1314            });
1315
1316        self.device
1317            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1318                label: Some("Image Pipeline"),
1319                layout: Some(&pipeline_layout),
1320                vertex: wgpu::VertexState {
1321                    module: &shader,
1322                    entry_point: "vs_main",
1323                    buffers: &[Vertex::desc()],
1324                },
1325                fragment: Some(wgpu::FragmentState {
1326                    module: &shader,
1327                    entry_point: "fs_main",
1328                    targets: &[Some(wgpu::ColorTargetState {
1329                        format: self.surface_config.format,
1330                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1331                        write_mask: wgpu::ColorWrites::ALL,
1332                    })],
1333                }),
1334                primitive: wgpu::PrimitiveState {
1335                    topology: wgpu::PrimitiveTopology::TriangleList,
1336                    strip_index_format: None,
1337                    front_face: wgpu::FrontFace::Ccw,
1338                    cull_mode: None,
1339                    polygon_mode: wgpu::PolygonMode::Fill,
1340                    unclipped_depth: false,
1341                    conservative: false,
1342                },
1343                depth_stencil: Some(wgpu::DepthStencilState {
1344                    format: Self::depth_format(),
1345                    depth_write_enabled: false,
1346                    depth_compare: wgpu::CompareFunction::Always,
1347                    stencil: wgpu::StencilState::default(),
1348                    bias: wgpu::DepthBiasState::default(),
1349                }),
1350                multisample: wgpu::MultisampleState {
1351                    count: self.msaa_sample_count,
1352                    mask: !0,
1353                    alpha_to_coverage_enabled: false,
1354                },
1355                multiview: None,
1356            })
1357    }
1358
1359    /// Create triangle rendering pipeline
1360    fn create_triangle_pipeline(&self) -> wgpu::RenderPipeline {
1361        let shader = self
1362            .device
1363            .create_shader_module(wgpu::ShaderModuleDescriptor {
1364                label: Some("Triangle Shader"),
1365                source: wgpu::ShaderSource::Wgsl(shaders::vertex::TRIANGLE.into()),
1366            });
1367
1368        let pipeline_layout = self
1369            .device
1370            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1371                label: Some("Triangle Pipeline Layout"),
1372                bind_group_layouts: &[&self.uniform_bind_group_layout],
1373                push_constant_ranges: &[],
1374            });
1375
1376        self.device
1377            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1378                label: Some("Triangle Pipeline"),
1379                layout: Some(&pipeline_layout),
1380                vertex: wgpu::VertexState {
1381                    module: &shader,
1382                    entry_point: "vs_main",
1383                    buffers: &[Vertex::desc()],
1384                },
1385                fragment: Some(wgpu::FragmentState {
1386                    module: &shader,
1387                    entry_point: "fs_main",
1388                    targets: &[Some(wgpu::ColorTargetState {
1389                        format: self.surface_config.format,
1390                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1391                        write_mask: wgpu::ColorWrites::ALL,
1392                    })],
1393                }),
1394                primitive: wgpu::PrimitiveState {
1395                    topology: wgpu::PrimitiveTopology::TriangleList,
1396                    strip_index_format: None,
1397                    front_face: wgpu::FrontFace::Ccw,
1398                    cull_mode: None, // Disable culling for 2D plotting
1399                    polygon_mode: wgpu::PolygonMode::Fill,
1400                    unclipped_depth: false,
1401                    conservative: false,
1402                },
1403                depth_stencil: Some(wgpu::DepthStencilState {
1404                    format: Self::depth_format(),
1405                    depth_write_enabled: true,
1406                    depth_compare: self.depth_compare(),
1407                    stencil: wgpu::StencilState::default(),
1408                    bias: wgpu::DepthBiasState::default(),
1409                }),
1410                multisample: wgpu::MultisampleState {
1411                    count: self.msaa_sample_count,
1412                    mask: !0,
1413                    alpha_to_coverage_enabled: false,
1414                },
1415                multiview: None,
1416            })
1417    }
1418
1419    fn create_grid_plane_pipeline(&self) -> wgpu::RenderPipeline {
1420        let shader = self
1421            .device
1422            .create_shader_module(wgpu::ShaderModuleDescriptor {
1423                label: Some("Grid Plane Shader"),
1424                source: wgpu::ShaderSource::Wgsl(shaders::vertex::GRID_PLANE.into()),
1425            });
1426
1427        let pipeline_layout = self
1428            .device
1429            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1430                label: Some("Grid Plane Pipeline Layout"),
1431                bind_group_layouts: &[
1432                    &self.uniform_bind_group_layout,
1433                    &self.grid_uniform_bind_group_layout,
1434                ],
1435                push_constant_ranges: &[],
1436            });
1437
1438        self.device
1439            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1440                label: Some("Grid Plane Pipeline"),
1441                layout: Some(&pipeline_layout),
1442                vertex: wgpu::VertexState {
1443                    module: &shader,
1444                    entry_point: "vs_main",
1445                    buffers: &[Vertex::desc()],
1446                },
1447                fragment: Some(wgpu::FragmentState {
1448                    module: &shader,
1449                    entry_point: "fs_main",
1450                    targets: &[Some(wgpu::ColorTargetState {
1451                        format: self.surface_config.format,
1452                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1453                        write_mask: wgpu::ColorWrites::ALL,
1454                    })],
1455                }),
1456                primitive: wgpu::PrimitiveState {
1457                    topology: wgpu::PrimitiveTopology::TriangleList,
1458                    strip_index_format: None,
1459                    front_face: wgpu::FrontFace::Ccw,
1460                    cull_mode: None,
1461                    polygon_mode: wgpu::PolygonMode::Fill,
1462                    unclipped_depth: false,
1463                    conservative: false,
1464                },
1465                depth_stencil: Some(wgpu::DepthStencilState {
1466                    format: Self::depth_format(),
1467                    depth_write_enabled: false,
1468                    depth_compare: self.depth_compare(),
1469                    stencil: wgpu::StencilState::default(),
1470                    bias: wgpu::DepthBiasState::default(),
1471                }),
1472                multisample: wgpu::MultisampleState {
1473                    count: self.msaa_sample_count,
1474                    mask: !0,
1475                    alpha_to_coverage_enabled: false,
1476                },
1477                multiview: None,
1478            })
1479    }
1480
1481    /// Begin a render pass
1482    pub fn begin_render_pass<'a>(
1483        &'a self,
1484        encoder: &'a mut wgpu::CommandEncoder,
1485        view: &'a wgpu::TextureView,
1486        _depth_view: &'a wgpu::TextureView,
1487    ) -> wgpu::RenderPass<'a> {
1488        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1489            label: Some("Render Pass"),
1490            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1491                view,
1492                resolve_target: None,
1493                ops: wgpu::Operations {
1494                    load: wgpu::LoadOp::Clear(wgpu::Color {
1495                        r: 0.1,
1496                        g: 0.1,
1497                        b: 0.1,
1498                        a: 1.0,
1499                    }),
1500                    store: wgpu::StoreOp::Store,
1501                },
1502            })],
1503            depth_stencil_attachment: None, // No depth testing for 2D plotting
1504            occlusion_query_set: None,
1505            timestamp_writes: None,
1506        })
1507    }
1508
1509    /// Render vertices with the specified pipeline
1510    pub fn render_vertices<'a>(
1511        &'a mut self,
1512        render_pass: &mut wgpu::RenderPass<'a>,
1513        pipeline_type: PipelineType,
1514        vertex_buffer: &'a wgpu::Buffer,
1515        vertex_count: u32,
1516        index_buffer: Option<(&'a wgpu::Buffer, u32)>,
1517        indirect: Option<(&'a wgpu::Buffer, u64)>,
1518    ) {
1519        // Ensure the pipeline exists first
1520        self.ensure_pipeline(pipeline_type);
1521
1522        // Now get the pipeline and render
1523        let pipeline = self.get_pipeline(pipeline_type);
1524        render_pass.set_pipeline(pipeline);
1525        render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
1526        render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1527
1528        if let Some((args, offset)) = indirect {
1529            render_pass.draw_indirect(args, offset);
1530            return;
1531        }
1532
1533        match index_buffer {
1534            Some((indices, index_count)) => {
1535                render_pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
1536                render_pass.draw_indexed(0..index_count, 0, 0..1);
1537            }
1538            None => {
1539                render_pass.draw(0..vertex_count, 0..1);
1540            }
1541        }
1542    }
1543
1544    /// Ensure direct line pipeline exists
1545    pub fn ensure_direct_line_pipeline(&mut self) {
1546        if self.direct_line_pipeline.is_none() {
1547            self.direct_line_pipeline = Some(self.create_direct_line_pipeline());
1548        }
1549    }
1550
1551    /// Ensure direct triangle pipeline exists
1552    pub fn ensure_direct_triangle_pipeline(&mut self) {
1553        if self.direct_triangle_pipeline.is_none() {
1554            self.direct_triangle_pipeline = Some(self.create_direct_triangle_pipeline());
1555        }
1556    }
1557
1558    /// Ensure direct point pipeline exists
1559    pub fn ensure_direct_point_pipeline(&mut self) {
1560        if self.direct_point_pipeline.is_none() {
1561            self.direct_point_pipeline = Some(self.create_direct_point_pipeline());
1562        }
1563    }
1564
1565    /// Ensure image pipeline exists
1566    pub fn ensure_image_pipeline(&mut self) {
1567        if self.image_pipeline.is_none() {
1568            self.image_pipeline = Some(self.create_image_pipeline());
1569        }
1570    }
1571
1572    /// Update transformation uniforms for direct viewport rendering
1573    pub fn update_direct_uniforms(
1574        &mut self,
1575        data_min: [f32; 2],
1576        data_max: [f32; 2],
1577        viewport_min: [f32; 2],
1578        viewport_max: [f32; 2],
1579        viewport_px: [f32; 2],
1580    ) {
1581        self.direct_uniforms =
1582            DirectUniforms::new(data_min, data_max, viewport_min, viewport_max, viewport_px);
1583        self.queue.write_buffer(
1584            &self.direct_uniform_buffer,
1585            0,
1586            bytemuck::cast_slice(&[self.direct_uniforms]),
1587        );
1588        self.ensure_axes_uniform_capacity(1);
1589        self.queue.write_buffer(
1590            &self.axes_direct_uniform_buffers[0],
1591            0,
1592            bytemuck::cast_slice(&[self.direct_uniforms]),
1593        );
1594    }
1595
1596    pub fn update_direct_uniforms_for_axes(
1597        &mut self,
1598        axes_index: usize,
1599        data_min: [f32; 2],
1600        data_max: [f32; 2],
1601        viewport_min: [f32; 2],
1602        viewport_max: [f32; 2],
1603        viewport_px: [f32; 2],
1604    ) {
1605        self.ensure_axes_uniform_capacity(axes_index + 1);
1606        let uniforms =
1607            DirectUniforms::new(data_min, data_max, viewport_min, viewport_max, viewport_px);
1608        self.queue.write_buffer(
1609            &self.axes_direct_uniform_buffers[axes_index],
1610            0,
1611            bytemuck::cast_slice(&[uniforms]),
1612        );
1613    }
1614
1615    pub fn get_direct_uniform_bind_group_for_axes(&self, axes_index: usize) -> &wgpu::BindGroup {
1616        self.axes_direct_uniform_bind_groups
1617            .get(axes_index)
1618            .unwrap_or(&self.direct_uniform_bind_group)
1619    }
1620}
1621
1622/// Utility functions for creating common vertex patterns
1623pub mod vertex_utils {
1624    use super::*;
1625    use glam::Vec2;
1626
1627    /// Create vertices for a line from start to end point
1628    pub fn create_line(start: Vec3, end: Vec3, color: Vec4) -> Vec<Vertex> {
1629        vec![Vertex::new(start, color), Vertex::new(end, color)]
1630    }
1631
1632    /// CPU polyline extrusion for thick lines (butt caps, miter joins simplified)
1633    /// Input: contiguous points. Output: triangle list vertices.
1634    pub fn extrude_polyline(points: &[Vec3], color: Vec4, width: f32) -> Vec<Vertex> {
1635        let mut out: Vec<Vertex> = Vec::new();
1636        if points.len() < 2 {
1637            return out;
1638        }
1639        // `width` is expected to already be in data-space units. Do NOT clamp to 1.0 here:
1640        // plot axes ranges are often small (e.g. y in [-1,1]), and clamping would explode
1641        // thick line geometry into a huge filled shape.
1642        let half_w = width.max(0.0) * 0.5;
1643        for i in 0..points.len() - 1 {
1644            let p0 = points[i];
1645            let p1 = points[i + 1];
1646            let dir = (p1 - p0).truncate();
1647            let len = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-6);
1648            let nx = -dir.y / len;
1649            let ny = dir.x / len;
1650            let offset = Vec3::new(nx * half_w, ny * half_w, 0.0);
1651            // Quad corners in CCW
1652            let a = p0 - offset;
1653            let b = p0 + offset;
1654            let c = p1 + offset;
1655            let d = p1 - offset;
1656            // Two triangles: a-b-c and a-c-d
1657            out.push(Vertex::new(a, color));
1658            out.push(Vertex::new(b, color));
1659            out.push(Vertex::new(c, color));
1660            out.push(Vertex::new(a, color));
1661            out.push(Vertex::new(c, color));
1662            out.push(Vertex::new(d, color));
1663        }
1664        out
1665    }
1666
1667    fn line_intersection(p: Vec2, r: Vec2, q: Vec2, s: Vec2) -> Option<Vec2> {
1668        let rxs = r.perp_dot(s);
1669        if rxs.abs() < 1e-6 {
1670            return None;
1671        }
1672        let t = (q - p).perp_dot(s) / rxs;
1673        Some(p + r * t)
1674    }
1675
1676    /// Extrude polyline with join styles at internal vertices.
1677    pub fn extrude_polyline_with_join(
1678        points: &[Vec3],
1679        color: Vec4,
1680        width: f32,
1681        join: crate::plots::line::LineJoin,
1682    ) -> Vec<Vertex> {
1683        let mut out: Vec<Vertex> = Vec::new();
1684        if points.len() < 2 {
1685            return out;
1686        }
1687        // See `extrude_polyline` for rationale: keep width in data-space units.
1688        let half_w = width.max(0.0) * 0.5;
1689        // Base quads
1690        out.extend(extrude_polyline(points, color, width));
1691
1692        // Joins
1693        for i in 1..points.len() - 1 {
1694            let p_prev = points[i - 1];
1695            let p = points[i];
1696            let p_next = points[i + 1];
1697            let d0 = (p - p_prev).truncate();
1698            let d1 = (p_next - p).truncate();
1699            let l0 = d0.length().max(1e-6);
1700            let l1 = d1.length().max(1e-6);
1701            let n0 = Vec2::new(-d0.y / l0, d0.x / l0);
1702            let n1 = Vec2::new(-d1.y / l1, d1.x / l1);
1703            let turn = d0.perp_dot(d1); // >0 left turn, <0 right turn
1704
1705            if turn > 1e-6 {
1706                // Left turn: outer side is left (use +n)
1707                let left0 = p.truncate() + n0 * half_w;
1708                let left1 = p.truncate() + n1 * half_w;
1709                match join {
1710                    crate::plots::line::LineJoin::Bevel => {
1711                        // Triangle wedge (p, left0, left1)
1712                        out.push(Vertex::new(p, color));
1713                        out.push(Vertex::new(left0.extend(0.0), color));
1714                        out.push(Vertex::new(left1.extend(0.0), color));
1715                    }
1716                    crate::plots::line::LineJoin::Miter => {
1717                        let dir_edge0 = (p.truncate() - p_prev.truncate()).normalize_or_zero();
1718                        let dir_edge1 = (p_next.truncate() - p.truncate()).normalize_or_zero();
1719                        let l_edge = line_intersection(left0, dir_edge0, left1, dir_edge1);
1720                        if let Some(miter) = l_edge {
1721                            // fill wedge left0-miter-left1
1722                            out.push(Vertex::new(left0.extend(0.0), color));
1723                            out.push(Vertex::new(miter.extend(0.0), color));
1724                            out.push(Vertex::new(left1.extend(0.0), color));
1725                        } else {
1726                            // fallback to bevel
1727                            out.push(Vertex::new(p, color));
1728                            out.push(Vertex::new(left0.extend(0.0), color));
1729                            out.push(Vertex::new(left1.extend(0.0), color));
1730                        }
1731                    }
1732                    crate::plots::line::LineJoin::Round => {
1733                        // Arc fan from left0 -> left1 around p
1734                        let center = p.truncate();
1735                        let a0 = (left0 - center).to_array();
1736                        let a1 = (left1 - center).to_array();
1737                        let ang0 = a0[1].atan2(a0[0]);
1738                        let mut ang1 = a1[1].atan2(a1[0]);
1739                        // Ensure CCW sweep
1740                        if ang1 < ang0 {
1741                            ang1 += std::f32::consts::TAU;
1742                        }
1743                        let steps = 10usize;
1744                        let dtheta = (ang1 - ang0) / steps as f32;
1745                        let r = half_w;
1746                        for k in 0..steps {
1747                            let theta0 = ang0 + dtheta * k as f32;
1748                            let theta1 = ang0 + dtheta * (k + 1) as f32;
1749                            let v0 =
1750                                Vec2::new(center.x + theta0.cos() * r, center.y + theta0.sin() * r);
1751                            let v1 =
1752                                Vec2::new(center.x + theta1.cos() * r, center.y + theta1.sin() * r);
1753                            out.push(Vertex::new(p, color));
1754                            out.push(Vertex::new(v0.extend(0.0), color));
1755                            out.push(Vertex::new(v1.extend(0.0), color));
1756                        }
1757                    }
1758                }
1759            } else if turn < -1e-6 {
1760                // Right turn: outer side is right (use -n)
1761                let right0 = p.truncate() - n0 * half_w;
1762                let right1 = p.truncate() - n1 * half_w;
1763                match join {
1764                    crate::plots::line::LineJoin::Bevel => {
1765                        out.push(Vertex::new(p, color));
1766                        out.push(Vertex::new(right1.extend(0.0), color));
1767                        out.push(Vertex::new(right0.extend(0.0), color));
1768                    }
1769                    crate::plots::line::LineJoin::Miter => {
1770                        let dir_edge0 = (p.truncate() - p_prev.truncate()).normalize_or_zero();
1771                        let dir_edge1 = (p_next.truncate() - p.truncate()).normalize_or_zero();
1772                        let l_edge = line_intersection(right0, dir_edge0, right1, dir_edge1);
1773                        if let Some(miter) = l_edge {
1774                            out.push(Vertex::new(right1.extend(0.0), color));
1775                            out.push(Vertex::new(miter.extend(0.0), color));
1776                            out.push(Vertex::new(right0.extend(0.0), color));
1777                        } else {
1778                            out.push(Vertex::new(p, color));
1779                            out.push(Vertex::new(right1.extend(0.0), color));
1780                            out.push(Vertex::new(right0.extend(0.0), color));
1781                        }
1782                    }
1783                    crate::plots::line::LineJoin::Round => {
1784                        let center = p.truncate();
1785                        let a0 = (right0 - center).to_array();
1786                        let a1 = (right1 - center).to_array();
1787                        let mut ang0 = a0[1].atan2(a0[0]);
1788                        let mut ang1 = a1[1].atan2(a1[0]);
1789                        // Ensure CW sweep becomes CCW by swapping
1790                        if ang0 < ang1 {
1791                            std::mem::swap(&mut ang0, &mut ang1);
1792                        }
1793                        let steps = 10usize;
1794                        let dtheta = (ang0 - ang1) / steps as f32;
1795                        let r = half_w;
1796                        for k in 0..steps {
1797                            let theta0 = ang0 - dtheta * k as f32;
1798                            let theta1 = ang0 - dtheta * (k + 1) as f32;
1799                            let v0 =
1800                                Vec2::new(center.x + theta0.cos() * r, center.y + theta0.sin() * r);
1801                            let v1 =
1802                                Vec2::new(center.x + theta1.cos() * r, center.y + theta1.sin() * r);
1803                            out.push(Vertex::new(p, color));
1804                            out.push(Vertex::new(v0.extend(0.0), color));
1805                            out.push(Vertex::new(v1.extend(0.0), color));
1806                        }
1807                    }
1808                }
1809            }
1810        }
1811
1812        out
1813    }
1814
1815    /// Create vertices for a triangle
1816    pub fn create_triangle(p1: Vec3, p2: Vec3, p3: Vec3, color: Vec4) -> Vec<Vertex> {
1817        vec![
1818            Vertex::new(p1, color),
1819            Vertex::new(p2, color),
1820            Vertex::new(p3, color),
1821        ]
1822    }
1823
1824    /// Create vertices for a point cloud
1825    pub fn create_point_cloud(points: &[Vec3], colors: &[Vec4]) -> Vec<Vertex> {
1826        points
1827            .iter()
1828            .zip(colors.iter())
1829            .map(|(&pos, &color)| Vertex::new(pos, color))
1830            .collect()
1831    }
1832
1833    /// Create vertices for a parametric line plot (1px line segments)
1834    pub fn create_line_plot(x_data: &[f64], y_data: &[f64], color: Vec4) -> Vec<Vertex> {
1835        let mut vertices = Vec::new();
1836
1837        for i in 1..x_data.len() {
1838            let start = Vec3::new(x_data[i - 1] as f32, y_data[i - 1] as f32, 0.0);
1839            let end = Vec3::new(x_data[i] as f32, y_data[i] as f32, 0.0);
1840            vertices.extend(create_line(start, end, color));
1841        }
1842
1843        vertices
1844    }
1845
1846    /// Create dashed/dotted line vertices by selectively including segments.
1847    /// Approximation: pattern is applied per original segment index.
1848    pub fn create_line_plot_dashed(
1849        x_data: &[f64],
1850        y_data: &[f64],
1851        color: Vec4,
1852        style: crate::plots::line::LineStyle,
1853    ) -> Vec<Vertex> {
1854        let mut vertices = Vec::new();
1855        for i in 1..x_data.len() {
1856            let include = match style {
1857                crate::plots::line::LineStyle::Solid => true,
1858                crate::plots::line::LineStyle::Dashed => (i % 4) < 2, // on,on,off,off
1859                crate::plots::line::LineStyle::Dotted => false,       // handled elsewhere as points
1860                crate::plots::line::LineStyle::DashDot => {
1861                    let m = i % 6;
1862                    m < 2 || m == 3 // on,on,off,on,off,off
1863                }
1864            };
1865            if include {
1866                let start = Vec3::new(x_data[i - 1] as f32, y_data[i - 1] as f32, 0.0);
1867                let end = Vec3::new(x_data[i] as f32, y_data[i] as f32, 0.0);
1868                vertices.extend(create_line(start, end, color));
1869            }
1870        }
1871        vertices
1872    }
1873
1874    /// Create thick polyline as triangles (used when line width > 1)
1875    pub fn create_thick_polyline(
1876        x_data: &[f64],
1877        y_data: &[f64],
1878        color: Vec4,
1879        width_px: f32,
1880    ) -> Vec<Vertex> {
1881        let mut pts: Vec<Vec3> = Vec::with_capacity(x_data.len());
1882        for i in 0..x_data.len() {
1883            pts.push(Vec3::new(x_data[i] as f32, y_data[i] as f32, 0.0));
1884        }
1885        extrude_polyline(&pts, color, width_px)
1886    }
1887
1888    /// Thick polyline with join style
1889    pub fn create_thick_polyline_with_join(
1890        x_data: &[f64],
1891        y_data: &[f64],
1892        color: Vec4,
1893        width_px: f32,
1894        join: crate::plots::line::LineJoin,
1895    ) -> Vec<Vertex> {
1896        let mut pts: Vec<Vec3> = Vec::with_capacity(x_data.len());
1897        for i in 0..x_data.len() {
1898            pts.push(Vec3::new(x_data[i] as f32, y_data[i] as f32, 0.0));
1899        }
1900        extrude_polyline_with_join(&pts, color, width_px, join)
1901    }
1902
1903    /// Create dashed/dotted thick polyline by skipping segments in the extruder.
1904    pub fn create_thick_polyline_dashed(
1905        x_data: &[f64],
1906        y_data: &[f64],
1907        color: Vec4,
1908        width_px: f32,
1909        style: crate::plots::line::LineStyle,
1910    ) -> Vec<Vertex> {
1911        let mut out: Vec<Vertex> = Vec::new();
1912        if x_data.len() < 2 {
1913            return out;
1914        }
1915        let pts: Vec<Vec3> = x_data
1916            .iter()
1917            .zip(y_data.iter())
1918            .map(|(&x, &y)| Vec3::new(x as f32, y as f32, 0.0))
1919            .collect();
1920        for i in 0..pts.len() - 1 {
1921            let include = match style {
1922                crate::plots::line::LineStyle::Solid => true,
1923                crate::plots::line::LineStyle::Dashed => (i % 4) < 2,
1924                crate::plots::line::LineStyle::Dotted => false,
1925                crate::plots::line::LineStyle::DashDot => {
1926                    let m = i % 6;
1927                    m < 2 || m == 3
1928                }
1929            };
1930            if include {
1931                let seg = [pts[i], pts[i + 1]];
1932                out.extend(extrude_polyline(&seg, color, width_px));
1933            }
1934        }
1935        out
1936    }
1937
1938    /// Square caps variant: extend endpoints by half width
1939    pub fn create_thick_polyline_square_caps(
1940        x_data: &[f64],
1941        y_data: &[f64],
1942        color: Vec4,
1943        width_px: f32,
1944    ) -> Vec<Vertex> {
1945        if x_data.len() < 2 {
1946            return Vec::new();
1947        }
1948        let mut pts: Vec<Vec3> = Vec::with_capacity(x_data.len());
1949        for i in 0..x_data.len() {
1950            pts.push(Vec3::new(x_data[i] as f32, y_data[i] as f32, 0.0));
1951        }
1952        // extend start
1953        let dir0 = (pts[1] - pts[0]).truncate();
1954        let len0 = (dir0.x * dir0.x + dir0.y * dir0.y).sqrt().max(1e-6);
1955        let ext0 = Vec3::new(
1956            -(dir0.x / len0) * (width_px * 0.5),
1957            -(dir0.y / len0) * (width_px * 0.5),
1958            0.0,
1959        );
1960        pts[0] += ext0;
1961        // extend end
1962        let n = pts.len();
1963        let dir1 = (pts[n - 1] - pts[n - 2]).truncate();
1964        let len1 = (dir1.x * dir1.x + dir1.y * dir1.y).sqrt().max(1e-6);
1965        let ext1 = Vec3::new(
1966            (dir1.x / len1) * (width_px * 0.5),
1967            (dir1.y / len1) * (width_px * 0.5),
1968            0.0,
1969        );
1970        pts[n - 1] += ext1;
1971        extrude_polyline(&pts, color, width_px)
1972    }
1973
1974    /// Round caps variant: square caps plus approximated semicircle fan at ends
1975    pub fn create_thick_polyline_round_caps(
1976        x_data: &[f64],
1977        y_data: &[f64],
1978        color: Vec4,
1979        width_px: f32,
1980        segments: usize,
1981    ) -> Vec<Vertex> {
1982        let mut base = create_thick_polyline_square_caps(x_data, y_data, color, width_px);
1983        if x_data.len() < 2 {
1984            return base;
1985        }
1986        let r = width_px * 0.5;
1987        // start fan
1988        let p0 = Vec3::new(x_data[0] as f32, y_data[0] as f32, 0.0);
1989        let p1 = Vec3::new(x_data[1] as f32, y_data[1] as f32, 0.0);
1990        let dir0 = (p1 - p0).truncate();
1991        let theta0 = dir0.y.atan2(dir0.x) + std::f32::consts::PI; // facing backward
1992        for i in 0..segments {
1993            let a0 = theta0 - std::f32::consts::PI * (i as f32 / segments as f32);
1994            let a1 = theta0 - std::f32::consts::PI * ((i + 1) as f32 / segments as f32);
1995            let v0 = Vec3::new(p0.x + a0.cos() * r, p0.y + a0.sin() * r, 0.0);
1996            let v1 = Vec3::new(p0.x + a1.cos() * r, p0.y + a1.sin() * r, 0.0);
1997            base.push(Vertex::new(p0, color));
1998            base.push(Vertex::new(v0, color));
1999            base.push(Vertex::new(v1, color));
2000        }
2001        // end fan
2002        let n = x_data.len();
2003        let q0 = Vec3::new(x_data[n - 2] as f32, y_data[n - 2] as f32, 0.0);
2004        let q1 = Vec3::new(x_data[n - 1] as f32, y_data[n - 1] as f32, 0.0);
2005        let dir1 = (q1 - q0).truncate();
2006        let theta1 = dir1.y.atan2(dir1.x);
2007        let center = q1;
2008        for i in 0..segments {
2009            let a0 = theta1 - std::f32::consts::PI * (i as f32 / segments as f32);
2010            let a1 = theta1 - std::f32::consts::PI * ((i + 1) as f32 / segments as f32);
2011            let v0 = Vec3::new(center.x + a0.cos() * r, center.y + a0.sin() * r, 0.0);
2012            let v1 = Vec3::new(center.x + a1.cos() * r, center.y + a1.sin() * r, 0.0);
2013            base.push(Vertex::new(center, color));
2014            base.push(Vertex::new(v0, color));
2015            base.push(Vertex::new(v1, color));
2016        }
2017        base
2018    }
2019
2020    /// Create vertices for a scatter plot
2021    pub fn create_scatter_plot(x_data: &[f64], y_data: &[f64], color: Vec4) -> Vec<Vertex> {
2022        x_data
2023            .iter()
2024            .zip(y_data.iter())
2025            .map(|(&x, &y)| Vertex::new(Vec3::new(x as f32, y as f32, 0.0), color))
2026            .collect()
2027    }
2028}