Skip to main content

polyscope_structures/point_cloud/
mod.rs

1//! Point cloud structure.
2
3mod quantities;
4
5use glam::{Mat4, Vec3, Vec4};
6use polyscope_core::pick::PickResult;
7use polyscope_core::quantity::Quantity;
8use polyscope_core::structure::{HasQuantities, RenderContext, Structure};
9use polyscope_render::{ColorMapRegistry, PickUniforms, PointCloudRenderData, PointUniforms};
10use wgpu::util::DeviceExt;
11
12pub use quantities::*;
13
14/// A point cloud structure.
15pub struct PointCloud {
16    name: String,
17    points: Vec<Vec3>,
18    enabled: bool,
19    transform: Mat4,
20    quantities: Vec<Box<dyn Quantity>>,
21    render_data: Option<PointCloudRenderData>,
22    material: String,
23    point_radius: f32,
24    base_color: Vec4,
25    // GPU picking resources
26    pick_uniform_buffer: Option<wgpu::Buffer>,
27    pick_bind_group: Option<wgpu::BindGroup>,
28    global_start: u32,
29}
30
31impl PointCloud {
32    /// Creates a new point cloud.
33    pub fn new(name: impl Into<String>, points: Vec<Vec3>) -> Self {
34        Self {
35            name: name.into(),
36            points,
37            enabled: true,
38            transform: Mat4::IDENTITY,
39            quantities: Vec::new(),
40            render_data: None,
41            material: "clay".to_string(),
42            point_radius: 0.01,
43            base_color: Vec4::new(0.2, 0.5, 0.8, 1.0),
44            pick_uniform_buffer: None,
45            pick_bind_group: None,
46            global_start: 0,
47        }
48    }
49
50    /// Returns the number of points.
51    #[must_use]
52    pub fn num_points(&self) -> usize {
53        self.points.len()
54    }
55
56    /// Returns the points.
57    #[must_use]
58    pub fn points(&self) -> &[Vec3] {
59        &self.points
60    }
61
62    /// Updates the point positions.
63    pub fn update_points(&mut self, points: Vec<Vec3>) {
64        self.points = points;
65        self.refresh();
66    }
67
68    /// Adds a scalar quantity to this point cloud.
69    pub fn add_scalar_quantity(&mut self, name: impl Into<String>, values: Vec<f32>) -> &mut Self {
70        let quantity = PointCloudScalarQuantity::new(name, self.name.clone(), values);
71        self.add_quantity(Box::new(quantity));
72        self
73    }
74
75    /// Adds a vector quantity to this point cloud.
76    pub fn add_vector_quantity(
77        &mut self,
78        name: impl Into<String>,
79        vectors: Vec<Vec3>,
80    ) -> &mut Self {
81        let quantity = PointCloudVectorQuantity::new(name, self.name.clone(), vectors);
82        self.add_quantity(Box::new(quantity));
83        self
84    }
85
86    /// Adds a color quantity to this point cloud.
87    pub fn add_color_quantity(&mut self, name: impl Into<String>, colors: Vec<Vec3>) -> &mut Self {
88        let quantity = PointCloudColorQuantity::new(name, self.name.clone(), colors);
89        self.add_quantity(Box::new(quantity));
90        self
91    }
92
93    /// Initializes GPU resources for this point cloud.
94    pub fn init_gpu_resources(
95        &mut self,
96        device: &wgpu::Device,
97        bind_group_layout: &wgpu::BindGroupLayout,
98        camera_buffer: &wgpu::Buffer,
99    ) {
100        self.render_data = Some(PointCloudRenderData::new(
101            device,
102            bind_group_layout,
103            camera_buffer,
104            &self.points,
105            None, // No per-point colors yet
106        ));
107    }
108
109    /// Returns the render data if initialized.
110    #[must_use]
111    pub fn render_data(&self) -> Option<&PointCloudRenderData> {
112        self.render_data.as_ref()
113    }
114
115    /// Initializes GPU resources for pick rendering.
116    pub fn init_pick_resources(
117        &mut self,
118        device: &wgpu::Device,
119        pick_bind_group_layout: &wgpu::BindGroupLayout,
120        camera_buffer: &wgpu::Buffer,
121        global_start: u32,
122    ) {
123        self.global_start = global_start;
124
125        // Create pick uniform buffer
126        let pick_uniforms = PickUniforms {
127            global_start,
128            point_radius: self.point_radius,
129            _padding: [0.0; 2],
130        };
131        let pick_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
132            label: Some("point cloud pick uniforms"),
133            contents: bytemuck::cast_slice(&[pick_uniforms]),
134            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
135        });
136
137        // Create pick bind group (reuses position buffer from render_data)
138        if let Some(render_data) = &self.render_data {
139            let pick_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
140                label: Some("point cloud pick bind group"),
141                layout: pick_bind_group_layout,
142                entries: &[
143                    wgpu::BindGroupEntry {
144                        binding: 0,
145                        resource: camera_buffer.as_entire_binding(),
146                    },
147                    wgpu::BindGroupEntry {
148                        binding: 1,
149                        resource: pick_uniform_buffer.as_entire_binding(),
150                    },
151                    wgpu::BindGroupEntry {
152                        binding: 2,
153                        resource: render_data.position_buffer.as_entire_binding(),
154                    },
155                ],
156            });
157            self.pick_bind_group = Some(pick_bind_group);
158        }
159
160        self.pick_uniform_buffer = Some(pick_uniform_buffer);
161    }
162
163    /// Returns the pick bind group if initialized.
164    #[must_use]
165    pub fn pick_bind_group(&self) -> Option<&wgpu::BindGroup> {
166        self.pick_bind_group.as_ref()
167    }
168
169    /// Updates pick uniforms (e.g., when point radius changes).
170    pub fn update_pick_uniforms(&self, queue: &wgpu::Queue) {
171        if let Some(buffer) = &self.pick_uniform_buffer {
172            let pick_uniforms = PickUniforms {
173                global_start: self.global_start,
174                point_radius: self.point_radius,
175                _padding: [0.0; 2],
176            };
177            queue.write_buffer(buffer, 0, bytemuck::cast_slice(&[pick_uniforms]));
178        }
179    }
180
181    /// Sets the point radius.
182    pub fn set_point_radius(&mut self, radius: f32) {
183        self.point_radius = radius;
184    }
185
186    /// Gets the point radius.
187    #[must_use]
188    pub fn point_radius(&self) -> f32 {
189        self.point_radius
190    }
191
192    /// Sets the base color.
193    pub fn set_base_color(&mut self, color: Vec3) {
194        self.base_color = color.extend(1.0);
195    }
196
197    /// Gets the base color.
198    #[must_use]
199    pub fn base_color(&self) -> Vec4 {
200        self.base_color
201    }
202
203    /// Returns the currently active color quantity, if any.
204    #[must_use]
205    pub fn active_color_quantity(&self) -> Option<&PointCloudColorQuantity> {
206        use polyscope_core::quantity::QuantityKind;
207
208        for q in &self.quantities {
209            if q.is_enabled() && q.kind() == QuantityKind::Color {
210                if let Some(cq) = q.as_any().downcast_ref::<PointCloudColorQuantity>() {
211                    return Some(cq);
212                }
213            }
214        }
215        None
216    }
217
218    /// Returns the currently active scalar quantity, if any.
219    #[must_use]
220    pub fn active_scalar_quantity(&self) -> Option<&PointCloudScalarQuantity> {
221        use polyscope_core::quantity::QuantityKind;
222
223        for q in &self.quantities {
224            if q.is_enabled() && q.kind() == QuantityKind::Scalar {
225                if let Some(sq) = q.as_any().downcast_ref::<PointCloudScalarQuantity>() {
226                    return Some(sq);
227                }
228            }
229        }
230        None
231    }
232
233    /// Returns the currently active vector quantity, if any.
234    #[must_use]
235    pub fn active_vector_quantity(&self) -> Option<&PointCloudVectorQuantity> {
236        use polyscope_core::quantity::QuantityKind;
237
238        for q in &self.quantities {
239            if q.is_enabled() && q.kind() == QuantityKind::Vector {
240                if let Some(vq) = q.as_any().downcast_ref::<PointCloudVectorQuantity>() {
241                    return Some(vq);
242                }
243            }
244        }
245        None
246    }
247
248    /// Returns a mutable reference to the active vector quantity.
249    pub fn active_vector_quantity_mut(&mut self) -> Option<&mut PointCloudVectorQuantity> {
250        use polyscope_core::quantity::QuantityKind;
251
252        for q in &mut self.quantities {
253            if q.is_enabled() && q.kind() == QuantityKind::Vector {
254                if let Some(vq) = q.as_any_mut().downcast_mut::<PointCloudVectorQuantity>() {
255                    return Some(vq);
256                }
257            }
258        }
259        None
260    }
261
262    /// Builds the egui UI for this point cloud.
263    pub fn build_egui_ui(&mut self, ui: &mut egui::Ui, available_materials: &[&str]) {
264        let mut color = [self.base_color.x, self.base_color.y, self.base_color.z];
265        let mut radius = self.point_radius;
266
267        if polyscope_ui::build_point_cloud_ui(
268            ui,
269            self.points.len(),
270            &mut radius,
271            &mut color,
272            &mut self.material,
273            available_materials,
274        ) {
275            self.base_color = Vec4::new(color[0], color[1], color[2], self.base_color.w);
276            self.point_radius = radius;
277        }
278
279        // Show quantities
280        if !self.quantities.is_empty() {
281            ui.separator();
282            ui.label("Quantities:");
283            for quantity in &mut self.quantities {
284                // Cast to concrete type and call build_egui_ui
285                if let Some(sq) = quantity
286                    .as_any_mut()
287                    .downcast_mut::<PointCloudScalarQuantity>()
288                {
289                    sq.build_egui_ui(ui);
290                } else if let Some(cq) = quantity
291                    .as_any_mut()
292                    .downcast_mut::<PointCloudColorQuantity>()
293                {
294                    cq.build_egui_ui(ui);
295                } else if let Some(vq) = quantity
296                    .as_any_mut()
297                    .downcast_mut::<PointCloudVectorQuantity>()
298                {
299                    vq.build_egui_ui(ui);
300                }
301            }
302        }
303    }
304
305    /// Updates GPU buffers based on current state.
306    pub fn update_gpu_buffers(&self, queue: &wgpu::Queue, color_maps: &ColorMapRegistry) {
307        let Some(render_data) = &self.render_data else {
308            return;
309        };
310
311        // Convert glam Mat4 to [[f32; 4]; 4] for GPU
312        let model_matrix = self.transform.to_cols_array_2d();
313
314        let mut uniforms = PointUniforms {
315            model_matrix,
316            point_radius: self.point_radius,
317            use_per_point_color: 0,
318            _padding: [0.0; 2],
319            base_color: self.base_color.to_array(),
320        };
321
322        // Priority: color quantity > scalar quantity > base color
323        if let Some(color_q) = self.active_color_quantity() {
324            uniforms.use_per_point_color = 1;
325            color_q.apply_to_render_data(queue, render_data);
326        } else if let Some(scalar_q) = self.active_scalar_quantity() {
327            if let Some(colormap) = color_maps.get(scalar_q.colormap_name()) {
328                uniforms.use_per_point_color = 1;
329                let colors = scalar_q.compute_colors(colormap);
330                render_data.update_colors(queue, &colors);
331            }
332        }
333
334        render_data.update_uniforms(queue, &uniforms);
335    }
336}
337
338impl Structure for PointCloud {
339    fn as_any(&self) -> &dyn std::any::Any {
340        self
341    }
342
343    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
344        self
345    }
346
347    fn name(&self) -> &str {
348        &self.name
349    }
350
351    fn type_name(&self) -> &'static str {
352        "PointCloud"
353    }
354
355    fn bounding_box(&self) -> Option<(Vec3, Vec3)> {
356        if self.points.is_empty() {
357            return None;
358        }
359
360        let mut min = Vec3::splat(f32::MAX);
361        let mut max = Vec3::splat(f32::MIN);
362
363        for &p in &self.points {
364            min = min.min(p);
365            max = max.max(p);
366        }
367
368        // Apply transform
369        let transform = self.transform;
370        let corners = [
371            transform.transform_point3(Vec3::new(min.x, min.y, min.z)),
372            transform.transform_point3(Vec3::new(max.x, min.y, min.z)),
373            transform.transform_point3(Vec3::new(min.x, max.y, min.z)),
374            transform.transform_point3(Vec3::new(max.x, max.y, min.z)),
375            transform.transform_point3(Vec3::new(min.x, min.y, max.z)),
376            transform.transform_point3(Vec3::new(max.x, min.y, max.z)),
377            transform.transform_point3(Vec3::new(min.x, max.y, max.z)),
378            transform.transform_point3(Vec3::new(max.x, max.y, max.z)),
379        ];
380
381        let mut world_min = Vec3::splat(f32::MAX);
382        let mut world_max = Vec3::splat(f32::MIN);
383        for corner in corners {
384            world_min = world_min.min(corner);
385            world_max = world_max.max(corner);
386        }
387
388        Some((world_min, world_max))
389    }
390
391    fn length_scale(&self) -> f32 {
392        self.bounding_box()
393            .map_or(1.0, |(min, max)| (max - min).length())
394    }
395
396    fn transform(&self) -> Mat4 {
397        self.transform
398    }
399
400    fn set_transform(&mut self, transform: Mat4) {
401        self.transform = transform;
402    }
403
404    fn is_enabled(&self) -> bool {
405        self.enabled
406    }
407
408    fn set_enabled(&mut self, enabled: bool) {
409        self.enabled = enabled;
410    }
411
412    fn material(&self) -> &str {
413        &self.material
414    }
415
416    fn set_material(&mut self, material: &str) {
417        self.material = material.to_string();
418    }
419
420    fn draw(&self, _ctx: &mut dyn RenderContext) {
421        // Rendering is handled by polyscope/src/app/render.rs
422    }
423
424    fn draw_pick(&self, _ctx: &mut dyn RenderContext) {
425        // Pick rendering is handled by polyscope/src/app/render.rs
426    }
427
428    fn build_ui(&mut self, _ui: &dyn std::any::Any) {
429        // UI is handled by polyscope-ui/src/structure_ui.rs
430    }
431
432    fn build_pick_ui(&self, _ui: &dyn std::any::Any, _pick: &PickResult) {
433        // Pick UI is handled by polyscope-ui/src/panels.rs
434    }
435
436    fn clear_gpu_resources(&mut self) {
437        self.render_data = None;
438        self.pick_uniform_buffer = None;
439        self.pick_bind_group = None;
440        for quantity in &mut self.quantities {
441            quantity.clear_gpu_resources();
442        }
443    }
444
445    fn refresh(&mut self) {
446        for quantity in &mut self.quantities {
447            quantity.refresh();
448        }
449    }
450}
451
452impl HasQuantities for PointCloud {
453    fn add_quantity(&mut self, quantity: Box<dyn Quantity>) {
454        self.quantities.push(quantity);
455    }
456
457    fn get_quantity(&self, name: &str) -> Option<&dyn Quantity> {
458        self.quantities
459            .iter()
460            .find(|q| q.name() == name)
461            .map(std::convert::AsRef::as_ref)
462    }
463
464    fn get_quantity_mut(&mut self, name: &str) -> Option<&mut Box<dyn Quantity>> {
465        self.quantities.iter_mut().find(|q| q.name() == name)
466    }
467
468    fn remove_quantity(&mut self, name: &str) -> Option<Box<dyn Quantity>> {
469        let idx = self.quantities.iter().position(|q| q.name() == name)?;
470        Some(self.quantities.remove(idx))
471    }
472
473    fn quantities(&self) -> &[Box<dyn Quantity>] {
474        &self.quantities
475    }
476}