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