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}