1use glam::{UVec3, Vec3};
4use polyscope_core::quantity::{Quantity, QuantityKind};
5use polyscope_core::{McmMesh, marching_cubes};
6use polyscope_render::{GridcubePickUniforms, GridcubeRenderData, IsosurfaceRenderData};
7use wgpu::util::DeviceExt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum VolumeGridVizMode {
12 Gridcube,
14 Isosurface,
16}
17
18pub struct VolumeGridNodeScalarQuantity {
20 name: String,
21 structure_name: String,
22 values: Vec<f32>,
23 node_dim: UVec3,
24 enabled: bool,
25
26 color_map: String,
28 data_min: f32,
29 data_max: f32,
30
31 viz_mode: VolumeGridVizMode,
33
34 gridcube_render_data: Option<GridcubeRenderData>,
36 gridcube_dirty: bool,
37
38 isosurface_level: f32,
40 isosurface_color: Vec3,
41 isosurface_render_data: Option<IsosurfaceRenderData>,
42 isosurface_mesh_cache: Option<McmMesh>,
43 isosurface_dirty: bool,
44
45 bound_min: Vec3,
47 bound_max: Vec3,
48
49 register_as_mesh_requested: bool,
51
52 pick_uniform_buffer: Option<wgpu::Buffer>,
54 pick_bind_group: Option<wgpu::BindGroup>,
55 global_start: u32,
56}
57
58impl VolumeGridNodeScalarQuantity {
59 pub fn new(
61 name: impl Into<String>,
62 structure_name: impl Into<String>,
63 values: Vec<f32>,
64 node_dim: UVec3,
65 bound_min: Vec3,
66 bound_max: Vec3,
67 ) -> Self {
68 let (data_min, data_max) = Self::compute_range(&values);
69 let isosurface_level = (data_min + data_max) * 0.5;
70 Self {
71 name: name.into(),
72 structure_name: structure_name.into(),
73 values,
74 node_dim,
75 enabled: false,
76 color_map: "viridis".to_string(),
77 data_min,
78 data_max,
79 viz_mode: VolumeGridVizMode::Gridcube,
80 gridcube_render_data: None,
81 gridcube_dirty: true,
82 isosurface_level,
83 isosurface_color: Vec3::new(0.047, 0.451, 0.690), isosurface_render_data: None,
85 isosurface_mesh_cache: None,
86 isosurface_dirty: true,
87 bound_min,
88 bound_max,
89 register_as_mesh_requested: false,
90 pick_uniform_buffer: None,
91 pick_bind_group: None,
92 global_start: 0,
93 }
94 }
95
96 fn compute_range(values: &[f32]) -> (f32, f32) {
97 let mut min = f32::MAX;
98 let mut max = f32::MIN;
99 for &v in values {
100 if v.is_finite() {
101 min = min.min(v);
102 max = max.max(v);
103 }
104 }
105 if min > max { (0.0, 1.0) } else { (min, max) }
106 }
107
108 #[must_use]
110 pub fn values(&self) -> &[f32] {
111 &self.values
112 }
113
114 #[must_use]
116 pub fn node_dim(&self) -> UVec3 {
117 self.node_dim
118 }
119
120 #[must_use]
122 pub fn get(&self, i: u32, j: u32, k: u32) -> f32 {
123 let idx = i as usize
124 + j as usize * self.node_dim.x as usize
125 + k as usize * self.node_dim.x as usize * self.node_dim.y as usize;
126 self.values.get(idx).copied().unwrap_or(0.0)
127 }
128
129 #[must_use]
131 pub fn color_map(&self) -> &str {
132 &self.color_map
133 }
134
135 pub fn set_color_map(&mut self, name: impl Into<String>) -> &mut Self {
137 self.color_map = name.into();
138 self.gridcube_dirty = true;
139 self
140 }
141
142 #[must_use]
144 pub fn data_range(&self) -> (f32, f32) {
145 (self.data_min, self.data_max)
146 }
147
148 pub fn set_data_range(&mut self, min: f32, max: f32) -> &mut Self {
150 self.data_min = min;
151 self.data_max = max;
152 self
153 }
154
155 #[must_use]
159 pub fn viz_mode(&self) -> VolumeGridVizMode {
160 self.viz_mode
161 }
162
163 pub fn set_viz_mode(&mut self, mode: VolumeGridVizMode) -> &mut Self {
165 self.viz_mode = mode;
166 self
167 }
168
169 #[must_use]
173 pub fn isosurface_level(&self) -> f32 {
174 self.isosurface_level
175 }
176
177 pub fn set_isosurface_level(&mut self, level: f32) -> &mut Self {
179 self.isosurface_level = level;
180 self.isosurface_dirty = true;
181 self
183 }
184
185 #[must_use]
187 pub fn isosurface_color(&self) -> Vec3 {
188 self.isosurface_color
189 }
190
191 pub fn set_isosurface_color(&mut self, color: Vec3) -> &mut Self {
193 self.isosurface_color = color;
194 self
195 }
196
197 #[must_use]
199 pub fn isosurface_dirty(&self) -> bool {
200 self.isosurface_dirty
201 }
202
203 #[must_use]
205 pub fn gridcube_dirty(&self) -> bool {
206 self.gridcube_dirty
207 }
208
209 pub fn extract_isosurface(&mut self) -> &McmMesh {
223 if self.isosurface_mesh_cache.is_none() || self.isosurface_dirty {
224 let nx = self.node_dim.x;
225 let ny = self.node_dim.y;
226 let nz = self.node_dim.z;
227
228 let mut mesh = marching_cubes(&self.values, self.isosurface_level, nz, ny, nx);
229
230 let cell_dim = Vec3::new(
231 (nx - 1).max(1) as f32,
232 (ny - 1).max(1) as f32,
233 (nz - 1).max(1) as f32,
234 );
235 let spacing = (self.bound_max - self.bound_min) / cell_dim;
236
237 for v in &mut mesh.vertices {
238 *v = Vec3::new(
239 v.z * spacing.x + self.bound_min.x,
240 v.y * spacing.y + self.bound_min.y,
241 v.x * spacing.z + self.bound_min.z,
242 );
243 }
244
245 for n in &mut mesh.normals {
246 *n = Vec3::new(n.z / spacing.x, n.y / spacing.y, n.x / spacing.z);
247 let len = n.length();
248 if len > 0.0 {
249 *n /= len;
250 }
251 }
252
253 for tri in mesh.indices.chunks_exact_mut(3) {
254 tri.swap(1, 2);
255 }
256
257 self.isosurface_mesh_cache = Some(mesh);
258 self.isosurface_dirty = false;
259 }
260 self.isosurface_mesh_cache.as_ref().unwrap()
261 }
262
263 #[must_use]
265 pub fn isosurface_mesh(&self) -> Option<&McmMesh> {
266 self.isosurface_mesh_cache.as_ref()
267 }
268
269 #[must_use]
273 pub fn gridcube_render_data(&self) -> Option<&GridcubeRenderData> {
274 self.gridcube_render_data.as_ref()
275 }
276
277 pub fn gridcube_render_data_mut(&mut self) -> Option<&mut GridcubeRenderData> {
279 self.gridcube_render_data.as_mut()
280 }
281
282 pub fn set_gridcube_render_data(&mut self, data: GridcubeRenderData) {
284 self.gridcube_render_data = Some(data);
285 self.gridcube_dirty = false;
286 }
287
288 #[must_use]
290 pub fn isosurface_render_data(&self) -> Option<&IsosurfaceRenderData> {
291 self.isosurface_render_data.as_ref()
292 }
293
294 pub fn isosurface_render_data_mut(&mut self) -> Option<&mut IsosurfaceRenderData> {
296 self.isosurface_render_data.as_mut()
297 }
298
299 pub fn set_isosurface_render_data(&mut self, data: IsosurfaceRenderData) {
301 self.isosurface_render_data = Some(data);
302 self.isosurface_dirty = false;
303 }
304
305 pub fn clear_isosurface_render_data(&mut self) {
307 self.isosurface_render_data = None;
308 self.isosurface_dirty = false;
309 }
310
311 pub fn init_pick_resources(
317 &mut self,
318 device: &wgpu::Device,
319 pick_bind_group_layout: &wgpu::BindGroupLayout,
320 camera_buffer: &wgpu::Buffer,
321 global_start: u32,
322 ) {
323 self.global_start = global_start;
324
325 let Some(gridcube_rd) = &self.gridcube_render_data else {
326 return;
327 };
328
329 let uniforms = GridcubePickUniforms {
330 global_start,
331 cube_size_factor: 1.0,
332 ..Default::default()
333 };
334
335 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
336 label: Some("gridcube node pick uniforms"),
337 contents: bytemuck::cast_slice(&[uniforms]),
338 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
339 });
340
341 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
342 label: Some("gridcube node pick bind group"),
343 layout: pick_bind_group_layout,
344 entries: &[
345 wgpu::BindGroupEntry {
346 binding: 0,
347 resource: camera_buffer.as_entire_binding(),
348 },
349 wgpu::BindGroupEntry {
350 binding: 1,
351 resource: uniform_buffer.as_entire_binding(),
352 },
353 wgpu::BindGroupEntry {
354 binding: 2,
355 resource: gridcube_rd.position_buffer.as_entire_binding(),
356 },
357 ],
358 });
359
360 self.pick_uniform_buffer = Some(uniform_buffer);
361 self.pick_bind_group = Some(bind_group);
362 }
363
364 #[must_use]
366 pub fn pick_bind_group(&self) -> Option<&wgpu::BindGroup> {
367 self.pick_bind_group.as_ref()
368 }
369
370 pub fn update_pick_uniforms(
372 &self,
373 queue: &wgpu::Queue,
374 model: [[f32; 4]; 4],
375 cube_size_factor: f32,
376 ) {
377 if let Some(buffer) = &self.pick_uniform_buffer {
378 let uniforms = GridcubePickUniforms {
379 model,
380 global_start: self.global_start,
381 cube_size_factor,
382 ..Default::default()
383 };
384 queue.write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
385 }
386 }
387
388 #[must_use]
390 pub fn num_pick_elements(&self) -> u32 {
391 self.gridcube_render_data
392 .as_ref()
393 .map_or(0, |rd| rd.num_instances)
394 }
395
396 #[must_use]
398 pub fn pick_total_vertices(&self) -> u32 {
399 self.gridcube_render_data
400 .as_ref()
401 .map_or(0, GridcubeRenderData::total_vertices)
402 }
403
404 #[must_use]
406 pub fn register_as_mesh_requested(&self) -> bool {
407 self.register_as_mesh_requested
408 }
409
410 pub fn clear_register_as_mesh_request(&mut self) {
412 self.register_as_mesh_requested = false;
413 }
414
415 pub fn build_egui_ui(&mut self, ui: &mut egui::Ui, colormap_names: &[&str]) {
417 ui.horizontal(|ui| {
418 let mut enabled = self.enabled;
419 if ui.checkbox(&mut enabled, "").changed() {
420 self.enabled = enabled;
421 }
422 ui.label(&self.name);
423 ui.label(format!("[{:.3}, {:.3}]", self.data_min, self.data_max));
424 });
425
426 if self.enabled {
427 let indent_id = egui::Id::new(&self.name).with("node_scalar_indent");
428 ui.indent(indent_id, |ui| {
429 ui.horizontal(|ui| {
431 ui.label("Mode:");
432 if ui
433 .selectable_label(self.viz_mode == VolumeGridVizMode::Gridcube, "Gridcube")
434 .clicked()
435 {
436 self.viz_mode = VolumeGridVizMode::Gridcube;
437 }
438 if ui
439 .selectable_label(
440 self.viz_mode == VolumeGridVizMode::Isosurface,
441 "Isosurface",
442 )
443 .clicked()
444 {
445 self.viz_mode = VolumeGridVizMode::Isosurface;
446 }
447 });
448
449 match self.viz_mode {
450 VolumeGridVizMode::Gridcube => {
451 self.build_gridcube_ui(ui, colormap_names);
452 }
453 VolumeGridVizMode::Isosurface => {
454 self.build_isosurface_ui(ui);
455 }
456 }
457 });
458 }
459 }
460
461 fn build_gridcube_ui(&mut self, ui: &mut egui::Ui, colormap_names: &[&str]) {
462 if !colormap_names.is_empty() {
464 ui.horizontal(|ui| {
465 ui.label("Colormap:");
466 egui::ComboBox::from_id_salt(format!("{}_colormap", self.name))
467 .selected_text(&self.color_map)
468 .show_ui(ui, |ui| {
469 for &name in colormap_names {
470 if ui.selectable_label(self.color_map == name, name).clicked() {
471 self.color_map = name.to_string();
472 self.gridcube_dirty = true;
473 }
474 }
475 });
476 });
477 }
478
479 ui.horizontal(|ui| {
481 ui.label("Range:");
482 let mut min = self.data_min;
483 let mut max = self.data_max;
484 let speed = (max - min).abs() * 0.01;
485 let speed = if speed > 0.0 { speed } else { 0.01 };
486 ui.add(egui::DragValue::new(&mut min).speed(speed));
487 ui.label("–");
488 ui.add(egui::DragValue::new(&mut max).speed(speed));
489 if (min - self.data_min).abs() > f32::EPSILON
490 || (max - self.data_max).abs() > f32::EPSILON
491 {
492 self.data_min = min;
493 self.data_max = max;
494 }
495 });
496 }
497
498 fn build_isosurface_ui(&mut self, ui: &mut egui::Ui) {
499 egui::Grid::new(format!("{}_iso_grid", self.name))
500 .num_columns(2)
501 .show(ui, |ui| {
502 ui.label("Color:");
503 let mut color = [
504 self.isosurface_color.x,
505 self.isosurface_color.y,
506 self.isosurface_color.z,
507 ];
508 if ui.color_edit_button_rgb(&mut color).changed() {
509 self.isosurface_color = Vec3::new(color[0], color[1], color[2]);
510 }
511 ui.end_row();
512
513 ui.label("Level:");
514 let mut level = self.isosurface_level;
515 let (range_min, range_max) = (self.data_min, self.data_max);
516 if ui
517 .add(egui::Slider::new(&mut level, range_min..=range_max))
518 .changed()
519 {
520 self.isosurface_level = level;
521 self.isosurface_dirty = true;
522 }
523 ui.end_row();
524 });
525
526 if let Some(mesh) = &self.isosurface_mesh_cache {
528 ui.label(format!("{} tris", mesh.indices.len() / 3));
529 }
530
531 let has_cache = self.isosurface_mesh_cache.is_some();
533 if has_cache {
534 ui.columns(2, |cols| {
535 let w = cols[0].available_width();
536 let h = cols[0].spacing().interact_size.y;
537 if cols[0]
538 .add_sized([w, h], egui::Button::new("Refresh"))
539 .clicked()
540 {
541 self.isosurface_dirty = true;
542 self.isosurface_mesh_cache = None;
543 self.isosurface_render_data = None;
544 }
545 if cols[1]
546 .add_sized([w, h], egui::Button::new("Register Mesh"))
547 .clicked()
548 {
549 self.register_as_mesh_requested = true;
550 }
551 });
552 } else if ui.button("Refresh").clicked() {
553 self.isosurface_dirty = true;
554 self.isosurface_mesh_cache = None;
555 self.isosurface_render_data = None;
556 }
557 }
558}
559
560impl Quantity for VolumeGridNodeScalarQuantity {
561 fn name(&self) -> &str {
562 &self.name
563 }
564
565 fn structure_name(&self) -> &str {
566 &self.structure_name
567 }
568
569 fn kind(&self) -> QuantityKind {
570 QuantityKind::Scalar
571 }
572
573 fn is_enabled(&self) -> bool {
574 self.enabled
575 }
576
577 fn set_enabled(&mut self, enabled: bool) {
578 self.enabled = enabled;
579 }
580
581 fn data_size(&self) -> usize {
582 self.values.len()
583 }
584
585 fn build_ui(&mut self, _ui: &dyn std::any::Any) {
586 }
588
589 fn refresh(&mut self) {
590 self.gridcube_render_data = None;
591 self.gridcube_dirty = true;
592 self.isosurface_render_data = None;
593 self.isosurface_mesh_cache = None;
594 self.isosurface_dirty = true;
595 self.pick_uniform_buffer = None;
596 self.pick_bind_group = None;
597 }
598
599 fn clear_gpu_resources(&mut self) {
600 self.gridcube_render_data = None;
601 self.isosurface_render_data = None;
602 }
603
604 fn as_any(&self) -> &dyn std::any::Any {
605 self
606 }
607
608 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
609 self
610 }
611}
612
613pub struct VolumeGridCellScalarQuantity {
615 name: String,
616 structure_name: String,
617 values: Vec<f32>,
618 cell_dim: UVec3,
619 enabled: bool,
620
621 color_map: String,
623 data_min: f32,
624 data_max: f32,
625
626 gridcube_render_data: Option<GridcubeRenderData>,
628 gridcube_dirty: bool,
629
630 #[allow(dead_code)]
632 bound_min: Vec3,
633 #[allow(dead_code)]
634 bound_max: Vec3,
635
636 pick_uniform_buffer: Option<wgpu::Buffer>,
638 pick_bind_group: Option<wgpu::BindGroup>,
639 global_start: u32,
640}
641
642impl VolumeGridCellScalarQuantity {
643 pub fn new(
645 name: impl Into<String>,
646 structure_name: impl Into<String>,
647 values: Vec<f32>,
648 cell_dim: UVec3,
649 bound_min: Vec3,
650 bound_max: Vec3,
651 ) -> Self {
652 let (data_min, data_max) = Self::compute_range(&values);
653 Self {
654 name: name.into(),
655 structure_name: structure_name.into(),
656 values,
657 cell_dim,
658 enabled: false,
659 color_map: "viridis".to_string(),
660 data_min,
661 data_max,
662 gridcube_render_data: None,
663 gridcube_dirty: true,
664 bound_min,
665 bound_max,
666 pick_uniform_buffer: None,
667 pick_bind_group: None,
668 global_start: 0,
669 }
670 }
671
672 fn compute_range(values: &[f32]) -> (f32, f32) {
673 let mut min = f32::MAX;
674 let mut max = f32::MIN;
675 for &v in values {
676 if v.is_finite() {
677 min = min.min(v);
678 max = max.max(v);
679 }
680 }
681 if min > max { (0.0, 1.0) } else { (min, max) }
682 }
683
684 #[must_use]
686 pub fn values(&self) -> &[f32] {
687 &self.values
688 }
689
690 #[must_use]
692 pub fn cell_dim(&self) -> UVec3 {
693 self.cell_dim
694 }
695
696 #[must_use]
698 pub fn get(&self, i: u32, j: u32, k: u32) -> f32 {
699 let idx = i as usize
700 + j as usize * self.cell_dim.x as usize
701 + k as usize * self.cell_dim.x as usize * self.cell_dim.y as usize;
702 self.values.get(idx).copied().unwrap_or(0.0)
703 }
704
705 #[must_use]
707 pub fn color_map(&self) -> &str {
708 &self.color_map
709 }
710
711 pub fn set_color_map(&mut self, name: impl Into<String>) -> &mut Self {
713 self.color_map = name.into();
714 self.gridcube_dirty = true;
715 self
716 }
717
718 #[must_use]
720 pub fn data_range(&self) -> (f32, f32) {
721 (self.data_min, self.data_max)
722 }
723
724 pub fn set_data_range(&mut self, min: f32, max: f32) -> &mut Self {
726 self.data_min = min;
727 self.data_max = max;
728 self
729 }
730
731 #[must_use]
733 pub fn gridcube_dirty(&self) -> bool {
734 self.gridcube_dirty
735 }
736
737 #[must_use]
739 pub fn gridcube_render_data(&self) -> Option<&GridcubeRenderData> {
740 self.gridcube_render_data.as_ref()
741 }
742
743 pub fn gridcube_render_data_mut(&mut self) -> Option<&mut GridcubeRenderData> {
745 self.gridcube_render_data.as_mut()
746 }
747
748 pub fn set_gridcube_render_data(&mut self, data: GridcubeRenderData) {
750 self.gridcube_render_data = Some(data);
751 self.gridcube_dirty = false;
752 }
753
754 pub fn init_pick_resources(
758 &mut self,
759 device: &wgpu::Device,
760 pick_bind_group_layout: &wgpu::BindGroupLayout,
761 camera_buffer: &wgpu::Buffer,
762 global_start: u32,
763 ) {
764 self.global_start = global_start;
765
766 let Some(gridcube_rd) = &self.gridcube_render_data else {
767 return;
768 };
769
770 let uniforms = GridcubePickUniforms {
771 global_start,
772 cube_size_factor: 1.0,
773 ..Default::default()
774 };
775
776 let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
777 label: Some("gridcube cell pick uniforms"),
778 contents: bytemuck::cast_slice(&[uniforms]),
779 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
780 });
781
782 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
783 label: Some("gridcube cell pick bind group"),
784 layout: pick_bind_group_layout,
785 entries: &[
786 wgpu::BindGroupEntry {
787 binding: 0,
788 resource: camera_buffer.as_entire_binding(),
789 },
790 wgpu::BindGroupEntry {
791 binding: 1,
792 resource: uniform_buffer.as_entire_binding(),
793 },
794 wgpu::BindGroupEntry {
795 binding: 2,
796 resource: gridcube_rd.position_buffer.as_entire_binding(),
797 },
798 ],
799 });
800
801 self.pick_uniform_buffer = Some(uniform_buffer);
802 self.pick_bind_group = Some(bind_group);
803 }
804
805 #[must_use]
807 pub fn pick_bind_group(&self) -> Option<&wgpu::BindGroup> {
808 self.pick_bind_group.as_ref()
809 }
810
811 pub fn update_pick_uniforms(
813 &self,
814 queue: &wgpu::Queue,
815 model: [[f32; 4]; 4],
816 cube_size_factor: f32,
817 ) {
818 if let Some(buffer) = &self.pick_uniform_buffer {
819 let uniforms = GridcubePickUniforms {
820 model,
821 global_start: self.global_start,
822 cube_size_factor,
823 ..Default::default()
824 };
825 queue.write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
826 }
827 }
828
829 #[must_use]
831 pub fn num_pick_elements(&self) -> u32 {
832 self.gridcube_render_data
833 .as_ref()
834 .map_or(0, |rd| rd.num_instances)
835 }
836
837 #[must_use]
839 pub fn pick_total_vertices(&self) -> u32 {
840 self.gridcube_render_data
841 .as_ref()
842 .map_or(0, GridcubeRenderData::total_vertices)
843 }
844
845 pub fn build_egui_ui(&mut self, ui: &mut egui::Ui, colormap_names: &[&str]) {
847 ui.horizontal(|ui| {
848 let mut enabled = self.enabled;
849 if ui.checkbox(&mut enabled, "").changed() {
850 self.enabled = enabled;
851 }
852 ui.label(&self.name);
853 ui.label(format!("[{:.3}, {:.3}]", self.data_min, self.data_max));
854 });
855
856 if self.enabled {
857 let indent_id = egui::Id::new(&self.name).with("cell_scalar_indent");
858 ui.indent(indent_id, |ui| {
859 if !colormap_names.is_empty() {
861 ui.horizontal(|ui| {
862 ui.label("Colormap:");
863 egui::ComboBox::from_id_salt(format!("{}_colormap", self.name))
864 .selected_text(&self.color_map)
865 .show_ui(ui, |ui| {
866 for &name in colormap_names {
867 if ui.selectable_label(self.color_map == name, name).clicked() {
868 self.color_map = name.to_string();
869 self.gridcube_dirty = true;
870 }
871 }
872 });
873 });
874 }
875
876 ui.horizontal(|ui| {
878 ui.label("Range:");
879 let mut min = self.data_min;
880 let mut max = self.data_max;
881 let speed = (max - min).abs() * 0.01;
882 let speed = if speed > 0.0 { speed } else { 0.01 };
883 ui.add(egui::DragValue::new(&mut min).speed(speed));
884 ui.label("–");
885 ui.add(egui::DragValue::new(&mut max).speed(speed));
886 if (min - self.data_min).abs() > f32::EPSILON
887 || (max - self.data_max).abs() > f32::EPSILON
888 {
889 self.data_min = min;
890 self.data_max = max;
891 }
892 });
893 });
894 }
895 }
896}
897
898impl Quantity for VolumeGridCellScalarQuantity {
899 fn name(&self) -> &str {
900 &self.name
901 }
902
903 fn structure_name(&self) -> &str {
904 &self.structure_name
905 }
906
907 fn kind(&self) -> QuantityKind {
908 QuantityKind::Scalar
909 }
910
911 fn is_enabled(&self) -> bool {
912 self.enabled
913 }
914
915 fn set_enabled(&mut self, enabled: bool) {
916 self.enabled = enabled;
917 }
918
919 fn data_size(&self) -> usize {
920 self.values.len()
921 }
922
923 fn build_ui(&mut self, _ui: &dyn std::any::Any) {
924 }
926
927 fn refresh(&mut self) {
928 self.gridcube_render_data = None;
929 self.gridcube_dirty = true;
930 self.pick_uniform_buffer = None;
931 self.pick_bind_group = None;
932 }
933
934 fn clear_gpu_resources(&mut self) {
935 self.gridcube_render_data = None;
936 }
937
938 fn as_any(&self) -> &dyn std::any::Any {
939 self
940 }
941
942 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
943 self
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 #[test]
958 fn isosurface_x_aligned_non_uniform_grid() {
959 let nx: u32 = 3;
960 let ny: u32 = 4;
961 let nz: u32 = 5;
962 let bound_min = Vec3::new(0.0, 0.0, 0.0);
963 let bound_max = Vec3::new(2.0, 3.0, 4.0); let mut values = Vec::with_capacity((nx * ny * nz) as usize);
966 for _k in 0..nz {
967 for _j in 0..ny {
968 for i in 0..nx {
969 values.push(i as f32);
970 }
971 }
972 }
973
974 let mut q = VolumeGridNodeScalarQuantity::new(
975 "test",
976 "grid",
977 values,
978 UVec3::new(nx, ny, nz),
979 bound_min,
980 bound_max,
981 );
982 q.set_isosurface_level(1.5);
983
984 let mesh = q.extract_isosurface();
985 assert!(!mesh.vertices.is_empty(), "isosurface should not be empty");
986 for v in &mesh.vertices {
987 assert!(
988 (v.x - 1.5).abs() < 1e-4,
989 "vertex {v:?} should lie on plane world_x = 1.5"
990 );
991 assert!(
992 v.y >= bound_min.y - 1e-4 && v.y <= bound_max.y + 1e-4,
993 "vertex {v:?} world_y outside [0, 3]"
994 );
995 assert!(
996 v.z >= bound_min.z - 1e-4 && v.z <= bound_max.z + 1e-4,
997 "vertex {v:?} world_z outside [0, 4]"
998 );
999 }
1000
1001 for n in &mesh.normals {
1006 assert!(
1007 n.x > 0.5,
1008 "stored normal {n:?} should point in +X direction"
1009 );
1010 }
1011 for tri in mesh.indices.chunks_exact(3) {
1012 let v0 = mesh.vertices[tri[0] as usize];
1013 let v1 = mesh.vertices[tri[1] as usize];
1014 let v2 = mesh.vertices[tri[2] as usize];
1015 let geom_normal = (v1 - v0).cross(v2 - v0);
1016 assert!(
1017 geom_normal.x > 0.0,
1018 "triangle winding gives normal {geom_normal:?}, expected +X"
1019 );
1020 }
1021 }
1022
1023 #[test]
1028 fn isosurface_anisotropic_spacing_x_axis() {
1029 let nx: u32 = 3;
1030 let ny: u32 = 4;
1031 let nz: u32 = 5;
1032 let bound_min = Vec3::new(0.0, 0.0, 0.0);
1033 let bound_max = Vec3::new(20.0, 3.0, 4.0);
1034
1035 let mut values = Vec::with_capacity((nx * ny * nz) as usize);
1036 for _k in 0..nz {
1037 for _j in 0..ny {
1038 for i in 0..nx {
1039 values.push(i as f32);
1040 }
1041 }
1042 }
1043
1044 let mut q = VolumeGridNodeScalarQuantity::new(
1045 "test",
1046 "grid",
1047 values,
1048 UVec3::new(nx, ny, nz),
1049 bound_min,
1050 bound_max,
1051 );
1052 q.set_isosurface_level(1.5);
1053
1054 let mesh = q.extract_isosurface();
1055 assert!(!mesh.vertices.is_empty());
1056 for v in &mesh.vertices {
1057 assert!(
1058 (v.x - 15.0).abs() < 1e-3,
1059 "vertex {v:?} should lie on plane world_x = 15.0"
1060 );
1061 }
1062 }
1063
1064 #[test]
1067 fn isosurface_z_aligned_non_uniform_grid() {
1068 let nx: u32 = 3;
1069 let ny: u32 = 4;
1070 let nz: u32 = 5;
1071 let bound_min = Vec3::new(0.0, 0.0, 0.0);
1072 let bound_max = Vec3::new(2.0, 3.0, 4.0);
1073
1074 let mut values = Vec::with_capacity((nx * ny * nz) as usize);
1075 for k in 0..nz {
1076 for _j in 0..ny {
1077 for _i in 0..nx {
1078 values.push(k as f32);
1079 }
1080 }
1081 }
1082
1083 let mut q = VolumeGridNodeScalarQuantity::new(
1084 "test",
1085 "grid",
1086 values,
1087 UVec3::new(nx, ny, nz),
1088 bound_min,
1089 bound_max,
1090 );
1091 q.set_isosurface_level(2.5);
1092
1093 let mesh = q.extract_isosurface();
1094 assert!(!mesh.vertices.is_empty(), "isosurface should not be empty");
1095 for v in &mesh.vertices {
1096 assert!(
1097 (v.z - 2.5).abs() < 1e-4,
1098 "vertex {v:?} should lie on plane world_z = 2.5"
1099 );
1100 }
1101 }
1102}