Skip to main content

polyscope_render/
pick.rs

1//! Pick buffer rendering for element selection.
2//!
3//! The pick buffer is an offscreen framebuffer where each element is rendered
4//! with a unique color encoding its ID. When the user clicks, we read the pixel
5//! at that position and decode the color to find what was clicked.
6
7use glam::Vec2;
8
9/// Element type for pick results.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum PickElementType {
12    /// No element type (background or unknown).
13    #[default]
14    None,
15    /// A point in a point cloud.
16    Point,
17    /// A vertex of a mesh.
18    Vertex,
19    /// A face of a mesh.
20    Face,
21    /// An edge of a mesh or curve network.
22    Edge,
23    /// A cell of a volume mesh.
24    Cell,
25}
26
27/// Result of a pick operation.
28#[derive(Debug, Clone, Default)]
29pub struct PickResult {
30    /// Whether something was hit.
31    pub hit: bool,
32    /// The type of structure that was hit (e.g., "`point_cloud`", "`surface_mesh`").
33    pub structure_type: String,
34    /// The name of the structure that was hit.
35    pub structure_name: String,
36    /// The index of the element within the structure.
37    pub element_index: u64,
38    /// The type of element that was hit.
39    pub element_type: PickElementType,
40    /// The screen position where the pick occurred.
41    pub screen_pos: Vec2,
42    /// The depth value at the pick location.
43    pub depth: f32,
44}
45
46/// Decodes a pick color back to an index.
47///
48/// The color is encoded as RGB where:
49/// - R contains bits 16-23
50/// - G contains bits 8-15
51/// - B contains bits 0-7
52#[must_use]
53pub fn color_to_index(r: u8, g: u8, b: u8) -> u32 {
54    (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b)
55}
56
57/// Encodes a structure ID and element ID into RGB pick color.
58/// GPU uniforms for pick rendering (flat 24-bit global index encoding).
59///
60/// Each structure is assigned a contiguous range `[global_start, global_start + num_elements)`.
61/// The shader encodes `global_start + element_index` as a 24-bit RGB color.
62#[repr(C)]
63#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
64#[allow(clippy::pub_underscore_fields)]
65pub struct PickUniforms {
66    /// The starting global index for this structure's elements.
67    pub global_start: u32,
68    /// Point radius for sphere impostor rendering.
69    pub point_radius: f32,
70    /// Padding to align to 16 bytes.
71    pub _padding: [f32; 2],
72}
73
74impl Default for PickUniforms {
75    fn default() -> Self {
76        Self {
77            global_start: 0,
78            point_radius: 0.01,
79            _padding: [0.0; 2],
80        }
81    }
82}
83
84/// GPU uniforms for tube-based curve network pick rendering (flat 24-bit global index encoding).
85#[repr(C)]
86#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
87#[allow(clippy::pub_underscore_fields)]
88pub struct TubePickUniforms {
89    /// The starting global index for this structure's elements.
90    pub global_start: u32,
91    /// Tube radius for ray-cylinder intersection.
92    pub radius: f32,
93    /// Minimum pick radius - ensures curves are always clickable even when very thin.
94    pub min_pick_radius: f32,
95    /// Padding to align to 16 bytes.
96    pub _padding: f32,
97}
98
99impl Default for TubePickUniforms {
100    fn default() -> Self {
101        Self {
102            global_start: 0,
103            radius: 0.01,
104            min_pick_radius: 0.02, // Default minimum pick radius for easier selection
105            _padding: 0.0,
106        }
107    }
108}
109
110/// GPU uniforms for mesh pick rendering (flat 24-bit global index encoding).
111///
112/// Includes the model transform since mesh positions are in object space.
113#[repr(C)]
114#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
115#[allow(clippy::pub_underscore_fields)]
116pub struct MeshPickUniforms {
117    /// The starting global index for this structure's face elements.
118    pub global_start: u32,
119    /// Padding to align model matrix to 16-byte boundary.
120    pub _padding: [f32; 3],
121    /// Model transform matrix.
122    pub model: [[f32; 4]; 4],
123}
124
125impl Default for MeshPickUniforms {
126    fn default() -> Self {
127        Self {
128            global_start: 0,
129            _padding: [0.0; 3],
130            model: [
131                [1.0, 0.0, 0.0, 0.0],
132                [0.0, 1.0, 0.0, 0.0],
133                [0.0, 0.0, 1.0, 0.0],
134                [0.0, 0.0, 0.0, 1.0],
135            ],
136        }
137    }
138}
139
140/// Encodes an index as a pick color.
141///
142/// Returns [R, G, B] where:
143/// - R contains bits 16-23
144/// - G contains bits 8-15
145/// - B contains bits 0-7
146#[must_use]
147pub fn index_to_color(index: u32) -> [u8; 3] {
148    [
149        ((index >> 16) & 0xFF) as u8,
150        ((index >> 8) & 0xFF) as u8,
151        (index & 0xFF) as u8,
152    ]
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_color_index_roundtrip() {
161        // Test various indices
162        for index in [
163            0,
164            1,
165            255,
166            256,
167            65535,
168            65536,
169            0x00FF_FFFF,
170            12_345_678 & 0x00FF_FFFF,
171        ] {
172            let color = index_to_color(index);
173            let decoded = color_to_index(color[0], color[1], color[2]);
174            assert_eq!(
175                decoded,
176                index & 0x00FF_FFFF,
177                "Roundtrip failed for index {index}",
178            );
179        }
180    }
181
182    #[test]
183    fn test_specific_colors() {
184        // Test that specific values encode correctly
185        assert_eq!(index_to_color(0), [0, 0, 0]);
186        assert_eq!(index_to_color(1), [0, 0, 1]);
187        assert_eq!(index_to_color(255), [0, 0, 255]);
188        assert_eq!(index_to_color(256), [0, 1, 0]);
189        assert_eq!(index_to_color(0x00FF_0000), [255, 0, 0]);
190        assert_eq!(index_to_color(0x0000_FF00), [0, 255, 0]);
191        assert_eq!(index_to_color(0x0000_00FF), [0, 0, 255]);
192    }
193}