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