Skip to main content

viewport_lib/resources/overlay/
overlays.rs

1use crate::resources::*;
2
3impl DeviceResources {
4    /// Re-upload the gizmo mesh with updated hover highlight colours.
5    ///
6    /// Called each frame when the hovered axis changes to brighten the appropriate axis colour.
7    /// The gizmo mesh is small (~300 vertices), so re-uploading every frame is acceptable.
8    pub fn update_gizmo_mesh(
9        &mut self,
10        device: &wgpu::Device,
11        queue: &wgpu::Queue,
12        mode: crate::interaction::manipulation::gizmo::GizmoMode,
13        hovered: crate::interaction::manipulation::gizmo::GizmoAxis,
14        space_orientation: glam::Quat,
15    ) {
16        let (verts, indices) = crate::interaction::manipulation::gizmo::build_gizmo_mesh(
17            mode,
18            hovered,
19            space_orientation,
20        );
21
22        let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
23        let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
24
25        // Recreate buffers if the new mesh is larger than the current allocation.
26        if vert_bytes.len() as u64 > self.gizmo_vertex_buffer.size() {
27            self.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
28                label: Some("gizmo_vertex_buf"),
29                size: vert_bytes.len() as u64,
30                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
31                mapped_at_creation: false,
32            });
33        }
34        if idx_bytes.len() as u64 > self.gizmo_index_buffer.size() {
35            self.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
36                label: Some("gizmo_index_buf"),
37                size: idx_bytes.len() as u64,
38                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
39                mapped_at_creation: false,
40            });
41        }
42
43        queue.write_buffer(&self.gizmo_vertex_buffer, 0, vert_bytes);
44        queue.write_buffer(&self.gizmo_index_buffer, 0, idx_bytes);
45        self.gizmo_index_count = indices.len() as u32;
46    }
47
48    /// Update the gizmo model matrix uniform (translation to gizmo center + scale for screen size).
49    pub fn update_gizmo_uniform(&self, queue: &wgpu::Queue, model: glam::Mat4) {
50        let uniform = crate::interaction::manipulation::gizmo::GizmoUniform {
51            model: model.to_cols_array_2d(),
52        };
53        queue.write_buffer(&self.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
54    }
55
56    /// Create a line-list overlay for an active transform constraint.
57    pub fn create_constraint_overlay(
58        &self,
59        device: &wgpu::Device,
60        overlay: &crate::interaction::query::snap::ConstraintOverlay,
61    ) -> (
62        wgpu::Buffer,
63        wgpu::Buffer,
64        u32,
65        wgpu::Buffer,
66        wgpu::BindGroup,
67    ) {
68        use bytemuck::cast_slice;
69
70        let (vertices, colour): (Vec<OverlayVertex>, [f32; 4]) = match overlay {
71            crate::interaction::query::snap::ConstraintOverlay::AxisLine {
72                origin,
73                direction,
74                colour,
75            } => (
76                vec![
77                    OverlayVertex {
78                        position: (*origin - *direction).to_array(),
79                    },
80                    OverlayVertex {
81                        position: (*origin + *direction).to_array(),
82                    },
83                ],
84                *colour,
85            ),
86            crate::interaction::query::snap::ConstraintOverlay::Plane {
87                origin,
88                axis_a,
89                axis_b,
90                colour,
91            } => (
92                vec![
93                    OverlayVertex {
94                        position: (*origin - *axis_a).to_array(),
95                    },
96                    OverlayVertex {
97                        position: (*origin + *axis_a).to_array(),
98                    },
99                    OverlayVertex {
100                        position: (*origin - *axis_b).to_array(),
101                    },
102                    OverlayVertex {
103                        position: (*origin + *axis_b).to_array(),
104                    },
105                ],
106                *colour,
107            ),
108        };
109        let indices: Vec<u32> = (0..vertices.len() as u32).collect();
110
111        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
112            label: Some("constraint_overlay_vbuf"),
113            size: (std::mem::size_of::<OverlayVertex>() * vertices.len()) as u64,
114            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
115            mapped_at_creation: true,
116        });
117        vertex_buffer
118            .slice(..)
119            .get_mapped_range_mut()
120            .copy_from_slice(cast_slice(&vertices));
121        vertex_buffer.unmap();
122
123        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
124            label: Some("constraint_overlay_ibuf"),
125            size: (std::mem::size_of::<u32>() * indices.len()) as u64,
126            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
127            mapped_at_creation: true,
128        });
129        index_buffer
130            .slice(..)
131            .get_mapped_range_mut()
132            .copy_from_slice(cast_slice(&indices));
133        index_buffer.unmap();
134
135        let uniform_data = OverlayUniform {
136            model: glam::Mat4::IDENTITY.to_cols_array_2d(),
137            colour,
138        };
139        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
140            label: Some("constraint_overlay_ubuf"),
141            size: std::mem::size_of::<OverlayUniform>() as u64,
142            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
143            mapped_at_creation: true,
144        });
145        uniform_buffer
146            .slice(..)
147            .get_mapped_range_mut()
148            .copy_from_slice(cast_slice(&[uniform_data]));
149        uniform_buffer.unmap();
150
151        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
152            label: Some("constraint_overlay_bg"),
153            layout: &self.overlay_bind_group_layout,
154            entries: &[wgpu::BindGroupEntry {
155                binding: 0,
156                resource: uniform_buffer.as_entire_binding(),
157            }],
158        });
159
160        (
161            vertex_buffer,
162            index_buffer,
163            indices.len() as u32,
164            uniform_buffer,
165            bind_group,
166        )
167    }
168
169    /// Create a triangle-list fill overlay for a clip plane handle quad.
170    ///
171    /// Produces a semi-transparent filled quad at the plane's world position.
172    pub(crate) fn create_clip_plane_fill_overlay(
173        &self,
174        device: &wgpu::Device,
175        overlay: &crate::interaction::manipulation::clip_plane::ClipPlaneOverlay,
176    ) -> (
177        wgpu::Buffer,
178        wgpu::Buffer,
179        u32,
180        wgpu::Buffer,
181        wgpu::BindGroup,
182    ) {
183        use crate::interaction::manipulation::clip_plane::plane_tangents;
184        use bytemuck::cast_slice;
185
186        let (t1, t2) = plane_tangents(overlay.normal);
187        let e = overlay.extent;
188        let c = overlay.center;
189
190        // 4 corners of the quad.
191        let corners = [
192            c + e * t1 + e * t2,
193            c - e * t1 + e * t2,
194            c - e * t1 - e * t2,
195            c + e * t1 - e * t2,
196        ];
197
198        let vertices: Vec<OverlayVertex> = corners
199            .iter()
200            .map(|p| OverlayVertex {
201                position: p.to_array(),
202            })
203            .collect();
204        // Two triangles: (0,1,2) and (0,2,3).
205        let indices: Vec<u32> = vec![0, 1, 2, 0, 2, 3];
206
207        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
208            label: Some("clip_plane_fill_vbuf"),
209            size: (std::mem::size_of::<OverlayVertex>() * vertices.len()) as u64,
210            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
211            mapped_at_creation: true,
212        });
213        vertex_buffer
214            .slice(..)
215            .get_mapped_range_mut()
216            .copy_from_slice(cast_slice(&vertices));
217        vertex_buffer.unmap();
218
219        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
220            label: Some("clip_plane_fill_ibuf"),
221            size: (std::mem::size_of::<u32>() * indices.len()) as u64,
222            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
223            mapped_at_creation: true,
224        });
225        index_buffer
226            .slice(..)
227            .get_mapped_range_mut()
228            .copy_from_slice(cast_slice(&indices));
229        index_buffer.unmap();
230
231        let uniform_data = OverlayUniform {
232            model: glam::Mat4::IDENTITY.to_cols_array_2d(),
233            colour: overlay.fill_colour,
234        };
235        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
236            label: Some("clip_plane_fill_ubuf"),
237            size: std::mem::size_of::<OverlayUniform>() as u64,
238            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
239            mapped_at_creation: true,
240        });
241        uniform_buffer
242            .slice(..)
243            .get_mapped_range_mut()
244            .copy_from_slice(cast_slice(&[uniform_data]));
245        uniform_buffer.unmap();
246
247        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
248            label: Some("clip_plane_fill_bg"),
249            layout: &self.overlay_bind_group_layout,
250            entries: &[wgpu::BindGroupEntry {
251                binding: 0,
252                resource: uniform_buffer.as_entire_binding(),
253            }],
254        });
255
256        (
257            vertex_buffer,
258            index_buffer,
259            indices.len() as u32,
260            uniform_buffer,
261            bind_group,
262        )
263    }
264
265    /// Create a line-list border + normal indicator overlay for a clip plane handle.
266    ///
267    /// Produces 4 border edges around the quad and a short line along the normal direction.
268    pub(crate) fn create_clip_plane_line_overlay(
269        &self,
270        device: &wgpu::Device,
271        overlay: &crate::interaction::manipulation::clip_plane::ClipPlaneOverlay,
272    ) -> (
273        wgpu::Buffer,
274        wgpu::Buffer,
275        u32,
276        wgpu::Buffer,
277        wgpu::BindGroup,
278    ) {
279        use crate::interaction::manipulation::clip_plane::plane_tangents;
280        use bytemuck::cast_slice;
281
282        let (t1, t2) = plane_tangents(overlay.normal);
283        let e = overlay.extent;
284        let c = overlay.center;
285
286        // 4 quad corners (shared between border edges).
287        let c0 = c + e * t1 + e * t2;
288        let c1 = c - e * t1 + e * t2;
289        let c2 = c - e * t1 - e * t2;
290        let c3 = c + e * t1 - e * t2;
291
292        // Normal indicator: short line from center along the normal.
293        let n_tip = c + overlay.normal * (e * 0.5);
294
295        // LineList vertices: each pair is one segment.
296        // 4 border edges + 1 normal indicator = 10 vertices.
297        let vertices: Vec<OverlayVertex> = vec![
298            // Edge 0->1
299            OverlayVertex {
300                position: c0.to_array(),
301            },
302            OverlayVertex {
303                position: c1.to_array(),
304            },
305            // Edge 1->2
306            OverlayVertex {
307                position: c1.to_array(),
308            },
309            OverlayVertex {
310                position: c2.to_array(),
311            },
312            // Edge 2->3
313            OverlayVertex {
314                position: c2.to_array(),
315            },
316            OverlayVertex {
317                position: c3.to_array(),
318            },
319            // Edge 3->0
320            OverlayVertex {
321                position: c3.to_array(),
322            },
323            OverlayVertex {
324                position: c0.to_array(),
325            },
326            // Normal indicator
327            OverlayVertex {
328                position: c.to_array(),
329            },
330            OverlayVertex {
331                position: n_tip.to_array(),
332            },
333        ];
334        let indices: Vec<u32> = (0..vertices.len() as u32).collect();
335
336        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
337            label: Some("clip_plane_line_vbuf"),
338            size: (std::mem::size_of::<OverlayVertex>() * vertices.len()) as u64,
339            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
340            mapped_at_creation: true,
341        });
342        vertex_buffer
343            .slice(..)
344            .get_mapped_range_mut()
345            .copy_from_slice(cast_slice(&vertices));
346        vertex_buffer.unmap();
347
348        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
349            label: Some("clip_plane_line_ibuf"),
350            size: (std::mem::size_of::<u32>() * indices.len()) as u64,
351            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
352            mapped_at_creation: true,
353        });
354        index_buffer
355            .slice(..)
356            .get_mapped_range_mut()
357            .copy_from_slice(cast_slice(&indices));
358        index_buffer.unmap();
359
360        let uniform_data = OverlayUniform {
361            model: glam::Mat4::IDENTITY.to_cols_array_2d(),
362            colour: overlay.border_colour,
363        };
364        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
365            label: Some("clip_plane_line_ubuf"),
366            size: std::mem::size_of::<OverlayUniform>() as u64,
367            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
368            mapped_at_creation: true,
369        });
370        uniform_buffer
371            .slice(..)
372            .get_mapped_range_mut()
373            .copy_from_slice(cast_slice(&[uniform_data]));
374        uniform_buffer.unmap();
375
376        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
377            label: Some("clip_plane_line_bg"),
378            layout: &self.overlay_bind_group_layout,
379            entries: &[wgpu::BindGroupEntry {
380                binding: 0,
381                resource: uniform_buffer.as_entire_binding(),
382            }],
383        });
384
385        (
386            vertex_buffer,
387            index_buffer,
388            indices.len() as u32,
389            uniform_buffer,
390            bind_group,
391        )
392    }
393
394    /// Upload cap geometry (cross-section fill) as transient overlay buffers.
395    ///
396    /// Uses the overlay pipeline (position-only vertices + flat colour uniform).
397    pub(crate) fn upload_cap_geometry(
398        &self,
399        device: &wgpu::Device,
400        cap: &crate::geometry::cap_geometry::CapMesh,
401        colour: [f32; 4],
402    ) -> (
403        wgpu::Buffer,
404        wgpu::Buffer,
405        u32,
406        wgpu::Buffer,
407        wgpu::BindGroup,
408    ) {
409        use bytemuck::cast_slice;
410
411        let vertices: Vec<OverlayVertex> = cap
412            .positions
413            .iter()
414            .map(|p| OverlayVertex { position: *p })
415            .collect();
416
417        let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
418            label: Some("cap_vbuf"),
419            size: (std::mem::size_of::<OverlayVertex>() * vertices.len()) as u64,
420            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
421            mapped_at_creation: true,
422        });
423        vertex_buffer
424            .slice(..)
425            .get_mapped_range_mut()
426            .copy_from_slice(cast_slice(&vertices));
427        vertex_buffer.unmap();
428
429        let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
430            label: Some("cap_ibuf"),
431            size: (std::mem::size_of::<u32>() * cap.indices.len()) as u64,
432            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
433            mapped_at_creation: true,
434        });
435        index_buffer
436            .slice(..)
437            .get_mapped_range_mut()
438            .copy_from_slice(cast_slice(&cap.indices));
439        index_buffer.unmap();
440
441        let uniform_data = OverlayUniform {
442            model: glam::Mat4::IDENTITY.to_cols_array_2d(),
443            colour,
444        };
445        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
446            label: Some("cap_ubuf"),
447            size: std::mem::size_of::<OverlayUniform>() as u64,
448            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
449            mapped_at_creation: true,
450        });
451        uniform_buffer
452            .slice(..)
453            .get_mapped_range_mut()
454            .copy_from_slice(cast_slice(&[uniform_data]));
455        uniform_buffer.unmap();
456
457        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
458            label: Some("cap_bg"),
459            layout: &self.overlay_bind_group_layout,
460            entries: &[wgpu::BindGroupEntry {
461                binding: 0,
462                resource: uniform_buffer.as_entire_binding(),
463            }],
464        });
465
466        let idx_count = cap.indices.len() as u32;
467        (
468            vertex_buffer,
469            index_buffer,
470            idx_count,
471            uniform_buffer,
472            bind_group,
473        )
474    }
475}
476
477/// Per-vertex data for overlay rendering: position only (no normal/colour in vertex).
478///
479/// Colour is provided via the OverlayUniform rather than per-vertex to keep
480/// the buffer minimal : all vertices of a single overlay quad share the same colour.
481#[repr(C)]
482#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
483pub struct OverlayVertex {
484    /// World-space XYZ position of this overlay vertex.
485    pub position: [f32; 3],
486}
487
488impl OverlayVertex {
489    /// wgpu vertex buffer layout matching shader location 0 (position vec3f).
490    pub fn buffer_layout() -> wgpu::VertexBufferLayout<'static> {
491        wgpu::VertexBufferLayout {
492            array_stride: std::mem::size_of::<OverlayVertex>() as wgpu::BufferAddress,
493            step_mode: wgpu::VertexStepMode::Vertex,
494            attributes: &[wgpu::VertexAttribute {
495                offset: 0,
496                shader_location: 0,
497                format: wgpu::VertexFormat::Float32x3,
498            }],
499        }
500    }
501}
502
503/// Per-overlay uniform: model matrix and RGBA colour with alpha for transparency.
504#[repr(C)]
505#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
506pub(crate) struct OverlayUniform {
507    pub(crate) model: [[f32; 4]; 4],
508    pub(crate) colour: [f32; 4], // RGBA with alpha for transparency
509}
510/// Cached GPU textures for the backdrop blur (frosted glass) effect.
511///
512/// Stored on `ViewportRenderer` and recreated when the viewport size changes.
513/// Contains a full-resolution intermediate (for rendering the scene when the
514/// output surface lacks `TEXTURE_BINDING`), two half-resolution ping-pong
515/// textures for the separable blur passes, and pre-built bind groups.
516pub(crate) struct BackdropBlurState {
517    /// Full-resolution intermediate render target. The scene is rendered here
518    /// instead of directly to the surface so the result can be sampled. Kept
519    /// alive so the matching view remains valid.
520    #[allow(dead_code)]
521    pub intermediate_texture: wgpu::Texture,
522    pub intermediate_view: wgpu::TextureView,
523    /// Half-resolution blur ping-pong texture A. Kept alive for its view.
524    #[allow(dead_code)]
525    pub blur_a_texture: wgpu::Texture,
526    pub blur_a_view: wgpu::TextureView,
527    /// Half-resolution blur ping-pong texture B. Kept alive for its view.
528    #[allow(dead_code)]
529    pub blur_b_texture: wgpu::Texture,
530    pub blur_b_view: wgpu::TextureView,
531    /// Viewport physical size the textures were created for.
532    pub size: [u32; 2],
533    /// Format the textures were created with.
534    pub format: wgpu::TextureFormat,
535}
536
537/// Uniform buffer layout for the full-screen ground plane shader.
538///
539/// Matches `GroundPlaneUniform` in `ground_plane.wgsl` exactly (256 bytes, 16-byte aligned).
540#[repr(C)]
541#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
542pub(crate) struct GroundPlaneUniform {
543    pub view_proj: [[f32; 4]; 4], // offset   0, 64 bytes
544    pub cam_right: [f32; 4],      // offset  64, 16 bytes
545    pub cam_up: [f32; 4],         // offset  80, 16 bytes
546    pub cam_back: [f32; 4],       // offset  96, 16 bytes
547    pub eye_pos: [f32; 3],        // offset 112, 12 bytes
548    pub height: f32,              // offset 124,  4 bytes
549    pub colour: [f32; 4],         // offset 128, 16 bytes
550    pub shadow_colour: [f32; 4],  // offset 144, 16 bytes
551    pub light_vp: [[f32; 4]; 4],  // offset 160, 64 bytes
552    pub tan_half_fov: f32,        // offset 224,  4 bytes
553    pub aspect: f32,              // offset 228,  4 bytes
554    pub tile_size: f32,           // offset 232,  4 bytes
555    pub shadow_bias: f32,         // offset 236,  4 bytes
556    pub mode: u32,                // offset 240,  4 bytes
557    pub shadow_opacity: f32,      // offset 244,  4 bytes
558    pub _pad: [f32; 2],           // offset 248,  8 bytes
559    pub colour2: [f32; 4],        // offset 256, 16 bytes : second tile colour
560} // total  272 bytes
561
562/// Uniform buffer layout for the full-screen analytical grid shader.
563///
564/// Contains all data needed by `grid.wgsl`: camera matrices for ray unprojection,
565/// eye position, grid plane height, spacing for minor/major lines, and RGBA colours.
566/// Total size: 192 bytes (fits in one 256-byte UBO slot).
567#[repr(C)]
568#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
569pub(crate) struct GridUniform {
570    /// Combined view-projection matrix for computing clip-space depth of grid hits.
571    pub view_proj: [[f32; 4]; 4], // offset   0, 64 bytes
572    /// Camera-to-world rotation matrix (3 columns as vec4 with w=0 padding, matching
573    /// WGSL mat3x3<f32> layout). Col 0 = right, Col 1 = up, Col 2 = back (camera +Z).
574    /// Used to rotate the analytical camera-space ray direction into world space,
575    /// bypassing the ill-conditioned inv(view_proj) at large camera distances.
576    pub cam_to_world: [[f32; 4]; 3], // offset  64, 48 bytes
577    /// tan(fov_y / 2) : scales NDC x/y to camera-space ray direction.
578    pub tan_half_fov: f32, // offset 112,  4 bytes
579    /// Viewport aspect ratio (width / height).
580    pub aspect: f32, // offset 116,  4 bytes
581    /// Padding to keep snap_origin at offset 152 (8-byte aligned).
582    pub _pad_ivp: [f32; 2], // offset 120,  8 bytes
583    /// Eye (camera) position in world space.
584    pub eye_pos: [f32; 3], // offset 128, 12 bytes
585    /// Z-coordinate of the horizontal grid plane (Z-up, XY ground plane).
586    pub grid_z: f32, // offset 140,  4 bytes
587    /// Minor grid line spacing (world units).
588    pub spacing_minor: f32, // offset 144,  4 bytes
589    /// Major grid line spacing (world units, typically spacing_minor * 10).
590    pub spacing_major: f32, // offset 148,  4 bytes
591    /// XZ origin used to keep `hit.xz - snap_origin` small for f32 precision.
592    /// Set to `floor(eye.xz / spacing_major) * spacing_major` each frame.
593    pub snap_origin: [f32; 2], // offset 152,  8 bytes
594    /// RGBA colour for minor grid lines.
595    pub colour_minor: [f32; 4], // offset 160, 16 bytes
596    /// RGBA colour for major grid lines.
597    pub colour_major: [f32; 4], // offset 176, 16 bytes
598                                // Total: 192 bytes
599}