use std::collections::BTreeSet;
use egui::Color32;
use crate::core::colormap::{AutoscaleMode, Colormap, ColormapName};
use crate::core::complex::ComplexMode;
use crate::core::scatter_viz::delaunay;
use crate::core::scene3d::marching_cubes::isosurface as marching_cubes_isosurface;
use crate::core::scene3d::mat4::{Mat4, Vec3, mat4_rotate};
use crate::core::scene3d::plane::{Plane, box_plane_intersect, segment_plane_intersect};
use crate::core::scene3d::transform::Item3DTransform;
use crate::render::gpu_scene3d::{
ImageInterpolation, PointMarker, Scene3dGeometry, Scene3dImageLayer, Scene3dTexturedMesh,
flat_normal,
};
pub const DEFAULT_SCATTER3D_SIZE: f32 = 6.0;
const DEFAULT_COLORMAP_AUTOSCALE_MODE: AutoscaleMode = AutoscaleMode::MinMax;
fn append_with_transform(
transform: &Item3DTransform,
raw_bounds: Option<(Vec3, Vec3)>,
geometry: &mut Scene3dGeometry,
build: impl FnOnce(&mut Scene3dGeometry),
) {
if transform.is_identity() {
build(geometry);
} else {
let mut local = Scene3dGeometry::new();
build(&mut local);
local.apply_transform(&transform.composed_matrix(raw_bounds));
geometry.extend_from(&local);
}
}
macro_rules! impl_item3d_transform {
($ty:ty) => {
impl $ty {
pub fn transform(&self) -> &Item3DTransform {
&self.transform
}
pub fn transform_mut(&mut self) -> &mut Item3DTransform {
&mut self.transform
}
}
};
}
#[derive(Clone, Debug)]
pub struct Scatter3D {
x: Vec<f32>,
y: Vec<f32>,
z: Vec<f32>,
values: Vec<f64>,
colormap: Colormap,
marker: PointMarker,
size: f32,
transform: Item3DTransform,
}
impl Default for Scatter3D {
fn default() -> Self {
Self::new()
}
}
impl Scatter3D {
pub fn new() -> Self {
Self {
x: Vec::new(),
y: Vec::new(),
z: Vec::new(),
values: Vec::new(),
colormap: Colormap::autoscale(ColormapName::Gray),
marker: PointMarker::Circle,
size: DEFAULT_SCATTER3D_SIZE,
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, x: &[f32], y: &[f32], z: &[f32], values: &[f64]) -> bool {
let n = x.len();
if y.len() != n || z.len() != n || values.len() != n {
return false;
}
self.x = x.to_vec();
self.y = y.to_vec();
self.z = z.to_vec();
self.values = values.to_vec();
self.resolve_colormap();
true
}
pub fn with_data(mut self, x: &[f32], y: &[f32], z: &[f32], values: &[f64]) -> Self {
self.set_data(x, y, z, values);
self
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
self.resolve_colormap();
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.set_colormap(colormap);
self
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
fn resolve_colormap(&mut self) {
self.colormap = self
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &self.values);
}
pub fn autoscale_colormap(&mut self, mode: AutoscaleMode) -> (f64, f64) {
let (vmin, vmax) = self.colormap.autoscale_range(mode, &self.values);
self.colormap.vmin = vmin;
self.colormap.vmax = vmax;
(vmin, vmax)
}
pub fn set_marker(&mut self, marker: PointMarker) {
self.marker = marker;
}
pub fn with_marker(mut self, marker: PointMarker) -> Self {
self.marker = marker;
self
}
pub fn set_size(&mut self, size: f32) {
self.size = size.max(0.0);
}
pub fn with_size(mut self, size: f32) -> Self {
self.set_size(size);
self
}
pub fn len(&self) -> usize {
self.x.len()
}
pub fn is_empty(&self) -> bool {
self.x.is_empty()
}
fn raw_bounds(&self) -> Option<(Vec3, Vec3)> {
if self.is_empty() {
return None;
}
let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
for i in 0..self.len() {
let (px, py, pz) = (self.x[i], self.y[i], self.z[i]);
min.x = min.x.min(px);
min.y = min.y.min(py);
min.z = min.z.min(pz);
max.x = max.x.max(px);
max.y = max.y.max(py);
max.z = max.z.max(pz);
}
Some((min, max))
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(self.raw_bounds())
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
append_with_transform(&self.transform, self.raw_bounds(), geometry, |g| {
for i in 0..self.len() {
let [r, gr, b, a] = self.colormap.color_at(self.values[i]);
g.add_point(
[self.x[i], self.y[i], self.z[i]],
Color32::from_rgba_unmultiplied(r, gr, b, a),
self.size,
self.marker,
);
}
});
}
}
impl_item3d_transform!(Scatter3D);
pub const DEFAULT_SCATTER2D_LINE_WIDTH: f32 = 1.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Scatter2DVisualization {
#[default]
Points,
Lines,
Solid,
}
#[derive(Clone, Debug)]
pub struct Scatter2D {
x: Vec<f64>,
y: Vec<f64>,
values: Vec<f64>,
colormap: Colormap,
marker: PointMarker,
size: f32,
line_width: f32,
height_map: bool,
visualization: Scatter2DVisualization,
transform: Item3DTransform,
}
impl Default for Scatter2D {
fn default() -> Self {
Self::new()
}
}
impl Scatter2D {
pub fn new() -> Self {
Self {
x: Vec::new(),
y: Vec::new(),
values: Vec::new(),
colormap: Colormap::autoscale(ColormapName::Gray),
marker: PointMarker::Circle,
size: DEFAULT_SCATTER3D_SIZE,
line_width: DEFAULT_SCATTER2D_LINE_WIDTH,
height_map: false,
visualization: Scatter2DVisualization::Points,
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, x: &[f64], y: &[f64], values: &[f64]) -> bool {
let n = x.len();
if y.len() != n || values.len() != n {
return false;
}
self.x = x.to_vec();
self.y = y.to_vec();
self.values = values.to_vec();
self.resolve_colormap();
true
}
pub fn with_data(mut self, x: &[f64], y: &[f64], values: &[f64]) -> Self {
self.set_data(x, y, values);
self
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
self.resolve_colormap();
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.set_colormap(colormap);
self
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
fn resolve_colormap(&mut self) {
self.colormap = self
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &self.values);
}
pub fn autoscale_colormap(&mut self, mode: AutoscaleMode) -> (f64, f64) {
let (vmin, vmax) = self.colormap.autoscale_range(mode, &self.values);
self.colormap.vmin = vmin;
self.colormap.vmax = vmax;
(vmin, vmax)
}
pub fn set_visualization(&mut self, visualization: Scatter2DVisualization) {
self.visualization = visualization;
}
pub fn with_visualization(mut self, visualization: Scatter2DVisualization) -> Self {
self.visualization = visualization;
self
}
pub fn visualization(&self) -> Scatter2DVisualization {
self.visualization
}
pub fn set_height_map(&mut self, height_map: bool) {
self.height_map = height_map;
}
pub fn with_height_map(mut self, height_map: bool) -> Self {
self.height_map = height_map;
self
}
pub fn is_height_map(&self) -> bool {
self.height_map
}
pub fn set_marker(&mut self, marker: PointMarker) {
self.marker = marker;
}
pub fn with_marker(mut self, marker: PointMarker) -> Self {
self.marker = marker;
self
}
pub fn set_size(&mut self, size: f32) {
self.size = size.max(0.0);
}
pub fn with_size(mut self, size: f32) -> Self {
self.set_size(size);
self
}
pub fn set_line_width(&mut self, width: f32) {
self.line_width = width.max(1.0);
}
pub fn with_line_width(mut self, width: f32) -> Self {
self.set_line_width(width);
self
}
pub fn line_width(&self) -> f32 {
self.line_width
}
pub fn len(&self) -> usize {
self.x.len()
}
pub fn is_empty(&self) -> bool {
self.x.is_empty()
}
fn z(&self, i: usize) -> f32 {
if self.height_map {
self.values[i] as f32
} else {
0.0
}
}
fn position(&self, i: usize) -> [f32; 3] {
[self.x[i] as f32, self.y[i] as f32, self.z(i)]
}
fn color_rgba(&self, i: usize) -> [f32; 4] {
let [r, g, b, a] = self.colormap.color_at(self.values[i]);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, g, b, a)).to_array()
}
fn raw_bounds(&self) -> Option<(Vec3, Vec3)> {
if self.is_empty() {
return None;
}
let positions: Vec<[f32; 3]> = (0..self.len()).map(|i| self.position(i)).collect();
positions_bounds(&positions)
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(self.raw_bounds())
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
append_with_transform(&self.transform, self.raw_bounds(), geometry, |g| {
self.append_raw(g)
});
}
fn append_raw(&self, geometry: &mut Scene3dGeometry) {
match self.visualization {
Scatter2DVisualization::Points => {
for i in 0..self.len() {
let [r, g, b, a] = self.colormap.color_at(self.values[i]);
geometry.add_point(
self.position(i),
Color32::from_rgba_unmultiplied(r, g, b, a),
self.size,
self.marker,
);
}
}
Scatter2DVisualization::Lines => {
let tri = delaunay(&self.x, &self.y);
let mut edges: BTreeSet<(usize, usize)> = BTreeSet::new();
for &[i0, i1, i2] in &tri.triangles {
for (a, b) in [(i0, i1), (i1, i2), (i2, i0)] {
edges.insert((a.min(b), a.max(b)));
}
}
for (a, b) in edges {
geometry.add_line_gradient(
self.position(a),
self.position(b),
self.color_rgba(a),
self.color_rgba(b),
);
}
for i in 0..self.len() {
geometry.add_line_pick_anchor(self.position(i));
}
}
Scatter2DVisualization::Solid => {
let tri = delaunay(&self.x, &self.y);
for &[i0, i1, i2] in &tri.triangles {
let p = [self.position(i0), self.position(i1), self.position(i2)];
let rgba = [
self.color_rgba(i0),
self.color_rgba(i1),
self.color_rgba(i2),
];
let normal = flat_normal(p[0], p[1], p[2]);
geometry.add_mesh_triangle_rgba(p, rgba, [normal; 3]);
}
}
}
}
}
impl_item3d_transform!(Scatter2D);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MeshDrawMode {
#[default]
Triangles,
TriangleStrip,
Fan,
}
#[derive(Clone, Debug)]
pub enum MeshColor {
Uniform(Color32),
PerVertex(Vec<Color32>),
}
fn expand_triangles(
mode: MeshDrawMode,
n_vertices: usize,
indices: Option<&[u32]>,
) -> Vec<[usize; 3]> {
let stream: Vec<usize> = match indices {
Some(idx) => idx.iter().map(|&i| i as usize).collect(),
None => (0..n_vertices).collect(),
};
let n = stream.len();
let mut tris = Vec::new();
match mode {
MeshDrawMode::Triangles => {
for c in stream.chunks_exact(3) {
tris.push([c[0], c[1], c[2]]);
}
}
MeshDrawMode::TriangleStrip => {
for i in 0..n.saturating_sub(2) {
tris.push([stream[i], stream[i + 1], stream[i + 2]]);
}
}
MeshDrawMode::Fan => {
for i in 1..n.saturating_sub(1) {
tris.push([stream[0], stream[i], stream[i + 1]]);
}
}
}
tris
}
fn mesh_attrs_valid(n: usize, normals: Option<&[[f32; 3]]>, indices: Option<&[u32]>) -> bool {
if let Some(ns) = normals
&& ns.len() != n
{
return false;
}
if let Some(idx) = indices
&& idx.iter().any(|&i| i as usize >= n)
{
return false;
}
true
}
fn positions_bounds(positions: &[[f32; 3]]) -> Option<(Vec3, Vec3)> {
if positions.is_empty() {
return None;
}
let mut min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
let mut max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
for &[px, py, pz] in positions {
min.x = min.x.min(px);
min.y = min.y.min(py);
min.z = min.z.min(pz);
max.x = max.x.max(px);
max.y = max.y.max(py);
max.z = max.z.max(pz);
}
Some((min, max))
}
#[derive(Clone, Debug)]
pub struct Mesh3D {
positions: Vec<[f32; 3]>,
colors: MeshColor,
normals: Option<Vec<[f32; 3]>>,
mode: MeshDrawMode,
indices: Option<Vec<u32>>,
transform: Item3DTransform,
}
impl Default for Mesh3D {
fn default() -> Self {
Self::new()
}
}
impl Mesh3D {
pub fn new() -> Self {
Self {
positions: Vec::new(),
colors: MeshColor::Uniform(Color32::WHITE),
normals: None,
mode: MeshDrawMode::Triangles,
indices: None,
transform: Item3DTransform::default(),
}
}
pub fn set_data(
&mut self,
positions: &[[f32; 3]],
colors: MeshColor,
normals: Option<&[[f32; 3]]>,
mode: MeshDrawMode,
indices: Option<&[u32]>,
) -> bool {
let n = positions.len();
if let MeshColor::PerVertex(cs) = &colors
&& cs.len() != n
{
return false;
}
if !mesh_attrs_valid(n, normals, indices) {
return false;
}
self.positions = positions.to_vec();
self.colors = colors;
self.normals = normals.map(<[[f32; 3]]>::to_vec);
self.mode = mode;
self.indices = indices.map(<[u32]>::to_vec);
true
}
pub fn with_data(
mut self,
positions: &[[f32; 3]],
colors: MeshColor,
normals: Option<&[[f32; 3]]>,
mode: MeshDrawMode,
indices: Option<&[u32]>,
) -> Self {
self.set_data(positions, colors, normals, mode, indices);
self
}
pub fn mode(&self) -> MeshDrawMode {
self.mode
}
pub fn len(&self) -> usize {
self.positions.len()
}
pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform
.transform_bounds(positions_bounds(&self.positions))
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
append_with_transform(
&self.transform,
positions_bounds(&self.positions),
geometry,
|g| {
for [i0, i1, i2] in
expand_triangles(self.mode, self.positions.len(), self.indices.as_deref())
{
let p = [self.positions[i0], self.positions[i1], self.positions[i2]];
let normals = match &self.normals {
Some(ns) => [ns[i0], ns[i1], ns[i2]],
None => [flat_normal(p[0], p[1], p[2]); 3],
};
let rgba = match &self.colors {
MeshColor::Uniform(c) => [egui::Rgba::from(*c).to_array(); 3],
MeshColor::PerVertex(cs) => [
egui::Rgba::from(cs[i0]).to_array(),
egui::Rgba::from(cs[i1]).to_array(),
egui::Rgba::from(cs[i2]).to_array(),
],
};
g.add_mesh_triangle_rgba(p, rgba, normals);
}
},
);
}
}
impl_item3d_transform!(Mesh3D);
#[derive(Clone, Debug)]
pub struct ColormapMesh3D {
positions: Vec<[f32; 3]>,
values: Vec<f64>,
normals: Option<Vec<[f32; 3]>>,
mode: MeshDrawMode,
indices: Option<Vec<u32>>,
colormap: Colormap,
transform: Item3DTransform,
}
impl Default for ColormapMesh3D {
fn default() -> Self {
Self::new()
}
}
impl ColormapMesh3D {
pub fn new() -> Self {
Self {
positions: Vec::new(),
values: Vec::new(),
normals: None,
mode: MeshDrawMode::Triangles,
indices: None,
colormap: Colormap::autoscale(ColormapName::Gray),
transform: Item3DTransform::default(),
}
}
pub fn set_data(
&mut self,
positions: &[[f32; 3]],
values: &[f64],
normals: Option<&[[f32; 3]]>,
mode: MeshDrawMode,
indices: Option<&[u32]>,
) -> bool {
let n = positions.len();
if values.len() != n {
return false;
}
if !mesh_attrs_valid(n, normals, indices) {
return false;
}
self.positions = positions.to_vec();
self.values = values.to_vec();
self.normals = normals.map(<[[f32; 3]]>::to_vec);
self.mode = mode;
self.indices = indices.map(<[u32]>::to_vec);
self.resolve_colormap();
true
}
pub fn with_data(
mut self,
positions: &[[f32; 3]],
values: &[f64],
normals: Option<&[[f32; 3]]>,
mode: MeshDrawMode,
indices: Option<&[u32]>,
) -> Self {
self.set_data(positions, values, normals, mode, indices);
self
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
self.resolve_colormap();
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.set_colormap(colormap);
self
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
fn resolve_colormap(&mut self) {
self.colormap = self
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &self.values);
}
pub fn autoscale_colormap(&mut self, mode: AutoscaleMode) -> (f64, f64) {
let (vmin, vmax) = self.colormap.autoscale_range(mode, &self.values);
self.colormap.vmin = vmin;
self.colormap.vmax = vmax;
(vmin, vmax)
}
pub fn mode(&self) -> MeshDrawMode {
self.mode
}
pub fn len(&self) -> usize {
self.positions.len()
}
pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform
.transform_bounds(positions_bounds(&self.positions))
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
append_with_transform(
&self.transform,
positions_bounds(&self.positions),
geometry,
|g| {
let rgba_at = |i: usize| {
let [r, gr, b, a] = self.colormap.color_at(self.values[i]);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, gr, b, a)).to_array()
};
for [i0, i1, i2] in
expand_triangles(self.mode, self.positions.len(), self.indices.as_deref())
{
let p = [self.positions[i0], self.positions[i1], self.positions[i2]];
let normals = match &self.normals {
Some(ns) => [ns[i0], ns[i1], ns[i2]],
None => [flat_normal(p[0], p[1], p[2]); 3],
};
g.add_mesh_triangle_rgba(p, [rgba_at(i0), rgba_at(i1), rgba_at(i2)], normals);
}
},
);
}
}
impl_item3d_transform!(ColormapMesh3D);
fn rotation_matrix(angle_deg: f32, axis: [f32; 3]) -> Mat4 {
let a = Vec3::from_array(axis);
let len = a.length();
if angle_deg == 0.0 || len == 0.0 {
return Mat4::IDENTITY;
}
let n = a * (1.0 / len);
mat4_rotate(angle_deg.to_radians(), n.x, n.y, n.z)
}
fn linspace_angles(n_seg: usize) -> Vec<f32> {
(0..=n_seg)
.map(|i| std::f32::consts::TAU * i as f32 / n_seg as f32)
.collect()
}
fn cylindrical_volume_mesh(
positions: &[[f32; 3]],
radius: f32,
height: f32,
angles: &[f32],
color: &[Color32],
flat_faces: bool,
rotation: Mat4,
) -> Mesh3D {
if positions.is_empty() || angles.len() < 2 {
return Mesh3D::new();
}
let n_seg = angles.len() - 1;
let hz = height / 2.0;
let edge = |r: f32, a: f32, z: f32| {
rotation.transform_point(Vec3::new(r * a.cos(), r * a.sin(), z), false)
};
let mut wedge_verts: Vec<Vec3> = Vec::with_capacity(n_seg * 12);
let mut wedge_normals: Vec<Vec3> = Vec::with_capacity(n_seg * 12);
for i in 0..n_seg {
let (a0, a1) = (angles[i], angles[i + 1]);
let c1 = rotation.transform_point(Vec3::new(0.0, 0.0, -hz), false);
let c2 = edge(radius, a0, -hz);
let c3 = edge(radius, a1, -hz);
let c4 = edge(radius, a0, hz);
let c5 = edge(radius, a1, hz);
let c6 = rotation.transform_point(Vec3::new(0.0, 0.0, hz), false);
wedge_verts.extend_from_slice(&[c1, c3, c2, c2, c3, c4, c3, c5, c4, c4, c5, c6]);
if flat_faces {
wedge_normals.extend_from_slice(&[
(c3 - c1).cross(c2 - c1),
(c2 - c3).cross(c1 - c3),
(c1 - c2).cross(c3 - c2),
(c3 - c2).cross(c4 - c2),
(c4 - c3).cross(c2 - c3),
(c2 - c4).cross(c3 - c4),
(c5 - c3).cross(c4 - c3),
(c4 - c5).cross(c3 - c5),
(c3 - c4).cross(c5 - c4),
(c5 - c4).cross(c6 - c4),
Vec3::new(0.0, 0.0, 0.0), (c4 - c6).cross(c5 - c6),
]);
} else {
wedge_normals.extend_from_slice(&[
(c3 - c1).cross(c2 - c1),
(c2 - c3).cross(c1 - c3),
(c1 - c2).cross(c3 - c2),
c2 - c1,
c3 - c1,
c4 - c6,
c3 - c1,
c5 - c6,
c4 - c6,
(c5 - c4).cross(c6 - c4),
Vec3::new(0.0, 0.0, 0.0), (c4 - c6).cross(c5 - c6),
]);
}
}
let total = wedge_verts.len() * positions.len();
let mut out_pos = Vec::with_capacity(total);
let mut out_norm = Vec::with_capacity(total);
let mut out_color = Vec::with_capacity(total);
for (k, &p) in positions.iter().enumerate() {
let pv = Vec3::from_array(p);
let color_k = if color.len() == 1 { color[0] } else { color[k] };
for (v, n) in wedge_verts.iter().zip(&wedge_normals) {
out_pos.push((*v + pv).to_array());
out_norm.push(n.to_array());
out_color.push(color_k);
}
}
Mesh3D::new().with_data(
&out_pos,
MeshColor::PerVertex(out_color),
Some(&out_norm),
MeshDrawMode::Triangles,
None,
)
}
fn volume_color_valid(color: &[Color32], n_positions: usize) -> bool {
color.len() == 1 || color.len() == n_positions
}
#[derive(Clone, Debug)]
pub struct Box3D {
size: [f32; 3],
colors: Vec<Color32>,
positions: Vec<[f32; 3]>,
mesh: Mesh3D,
}
impl Default for Box3D {
fn default() -> Self {
Self::new()
}
}
impl Box3D {
pub fn new() -> Self {
let mut b = Self {
size: [1.0, 1.0, 1.0],
colors: vec![Color32::WHITE],
positions: vec![[0.0, 0.0, 0.0]],
mesh: Mesh3D::new(),
};
b.rebuild((0.0, [0.0, 0.0, 0.0]));
b
}
pub fn set_data(
&mut self,
size: [f32; 3],
color: &[Color32],
positions: &[[f32; 3]],
rotation: (f32, [f32; 3]),
) -> bool {
if !volume_color_valid(color, positions.len()) {
return false;
}
self.size = size;
self.colors = color.to_vec();
self.positions = positions.to_vec();
self.rebuild(rotation);
true
}
fn rebuild(&mut self, rotation: (f32, [f32; 3])) {
let [dx, dy, dz] = self.size;
let diagonal = (dx * dx + dy * dy).sqrt();
let alpha = 2.0 * (dy / diagonal).asin();
let beta = 2.0 * (dx / diagonal).asin();
let angles: Vec<f32> = [
0.0,
alpha,
alpha + beta,
alpha + beta + alpha,
std::f32::consts::TAU,
]
.iter()
.map(|a| a - 0.5 * alpha)
.collect();
let transform = *self.mesh.transform();
self.mesh = cylindrical_volume_mesh(
&self.positions,
diagonal / 2.0,
dz,
&angles,
&self.colors,
true,
rotation_matrix(rotation.0, rotation.1),
);
*self.mesh.transform_mut() = transform;
}
pub fn positions(&self) -> &[[f32; 3]] {
&self.positions
}
pub fn size(&self) -> [f32; 3] {
self.size
}
pub fn colors(&self) -> &[Color32] {
&self.colors
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.mesh.bounds()
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
self.mesh.append_to(geometry);
}
pub fn transform(&self) -> &Item3DTransform {
self.mesh.transform()
}
pub fn transform_mut(&mut self) -> &mut Item3DTransform {
self.mesh.transform_mut()
}
}
#[derive(Clone, Debug)]
pub struct Cylinder3D {
radius: f32,
height: f32,
nb_faces: usize,
colors: Vec<Color32>,
positions: Vec<[f32; 3]>,
mesh: Mesh3D,
}
impl Default for Cylinder3D {
fn default() -> Self {
Self::new()
}
}
impl Cylinder3D {
pub fn new() -> Self {
let mut c = Self {
radius: 1.0,
height: 1.0,
nb_faces: 20,
colors: vec![Color32::WHITE],
positions: vec![[0.0, 0.0, 0.0]],
mesh: Mesh3D::new(),
};
c.rebuild((0.0, [0.0, 0.0, 0.0]));
c
}
pub fn set_data(
&mut self,
radius: f32,
height: f32,
color: &[Color32],
nb_faces: usize,
positions: &[[f32; 3]],
rotation: (f32, [f32; 3]),
) -> bool {
if !volume_color_valid(color, positions.len()) {
return false;
}
self.radius = radius;
self.height = height;
self.nb_faces = nb_faces;
self.colors = color.to_vec();
self.positions = positions.to_vec();
self.rebuild(rotation);
true
}
fn rebuild(&mut self, rotation: (f32, [f32; 3])) {
let angles = linspace_angles(self.nb_faces);
let transform = *self.mesh.transform();
self.mesh = cylindrical_volume_mesh(
&self.positions,
self.radius,
self.height,
&angles,
&self.colors,
false,
rotation_matrix(rotation.0, rotation.1),
);
*self.mesh.transform_mut() = transform;
}
pub fn positions(&self) -> &[[f32; 3]] {
&self.positions
}
pub fn radius(&self) -> f32 {
self.radius
}
pub fn height(&self) -> f32 {
self.height
}
pub fn colors(&self) -> &[Color32] {
&self.colors
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.mesh.bounds()
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
self.mesh.append_to(geometry);
}
pub fn transform(&self) -> &Item3DTransform {
self.mesh.transform()
}
pub fn transform_mut(&mut self) -> &mut Item3DTransform {
self.mesh.transform_mut()
}
}
#[derive(Clone, Debug)]
pub struct Hexagon3D {
radius: f32,
height: f32,
colors: Vec<Color32>,
positions: Vec<[f32; 3]>,
mesh: Mesh3D,
}
impl Default for Hexagon3D {
fn default() -> Self {
Self::new()
}
}
impl Hexagon3D {
pub fn new() -> Self {
let mut h = Self {
radius: 1.0,
height: 1.0,
colors: vec![Color32::WHITE],
positions: vec![[0.0, 0.0, 0.0]],
mesh: Mesh3D::new(),
};
h.rebuild((0.0, [0.0, 0.0, 0.0]));
h
}
pub fn set_data(
&mut self,
radius: f32,
height: f32,
color: &[Color32],
positions: &[[f32; 3]],
rotation: (f32, [f32; 3]),
) -> bool {
if !volume_color_valid(color, positions.len()) {
return false;
}
self.radius = radius;
self.height = height;
self.colors = color.to_vec();
self.positions = positions.to_vec();
self.rebuild(rotation);
true
}
fn rebuild(&mut self, rotation: (f32, [f32; 3])) {
let angles = linspace_angles(6);
let transform = *self.mesh.transform();
self.mesh = cylindrical_volume_mesh(
&self.positions,
self.radius,
self.height,
&angles,
&self.colors,
true,
rotation_matrix(rotation.0, rotation.1),
);
*self.mesh.transform_mut() = transform;
}
pub fn positions(&self) -> &[[f32; 3]] {
&self.positions
}
pub fn radius(&self) -> f32 {
self.radius
}
pub fn height(&self) -> f32 {
self.height
}
pub fn colors(&self) -> &[Color32] {
&self.colors
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.mesh.bounds()
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
self.mesh.append_to(geometry);
}
pub fn transform(&self) -> &Item3DTransform {
self.mesh.transform()
}
pub fn transform_mut(&mut self) -> &mut Item3DTransform {
self.mesh.transform_mut()
}
}
fn premul_linear_rgba8(c: Color32) -> [u8; 4] {
let [r, g, b, a] = egui::Rgba::from(c).to_array();
[
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8,
(a * 255.0).round() as u8,
]
}
fn image_bounds(
width: usize,
height: usize,
origin: [f32; 3],
scale: [f32; 2],
) -> Option<(Vec3, Vec3)> {
if width == 0 || height == 0 {
return None;
}
let min = Vec3::from_array(origin);
let max = Vec3::new(
origin[0] + width as f32 * scale[0],
origin[1] + height as f32 * scale[1],
origin[2],
);
Some((min, max))
}
#[derive(Clone, Debug)]
pub struct ImageData3D {
data: Vec<f64>,
width: usize,
height: usize,
colormap: Colormap,
origin: [f32; 3],
scale: [f32; 2],
interpolation: ImageInterpolation,
transform: Item3DTransform,
}
impl Default for ImageData3D {
fn default() -> Self {
Self::new()
}
}
impl ImageData3D {
pub fn new() -> Self {
Self {
data: Vec::new(),
width: 0,
height: 0,
colormap: Colormap::autoscale(ColormapName::Gray),
origin: [0.0, 0.0, 0.0],
scale: [1.0, 1.0],
interpolation: ImageInterpolation::Nearest,
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, data: &[f64], width: usize, height: usize) -> bool {
if data.len() != width * height {
return false;
}
self.data = data.to_vec();
self.width = width;
self.height = height;
self.resolve_colormap();
true
}
pub fn with_data(mut self, data: &[f64], width: usize, height: usize) -> Self {
self.set_data(data, width, height);
self
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
self.resolve_colormap();
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.set_colormap(colormap);
self
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
fn resolve_colormap(&mut self) {
self.colormap = self
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &self.data);
}
pub fn autoscale_colormap(&mut self, mode: AutoscaleMode) -> (f64, f64) {
let (vmin, vmax) = self.colormap.autoscale_range(mode, &self.data);
self.colormap.vmin = vmin;
self.colormap.vmax = vmax;
(vmin, vmax)
}
pub fn set_origin(&mut self, origin: [f32; 3]) {
self.origin = origin;
}
pub fn with_origin(mut self, origin: [f32; 3]) -> Self {
self.origin = origin;
self
}
pub fn set_scale(&mut self, scale: [f32; 2]) {
self.scale = scale;
}
pub fn with_scale(mut self, scale: [f32; 2]) -> Self {
self.scale = scale;
self
}
pub fn set_interpolation(&mut self, interpolation: ImageInterpolation) {
self.interpolation = interpolation;
}
pub fn with_interpolation(mut self, interpolation: ImageInterpolation) -> Self {
self.interpolation = interpolation;
self
}
pub fn dimensions(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(image_bounds(
self.width,
self.height,
self.origin,
self.scale,
))
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
let raw_bounds = image_bounds(self.width, self.height, self.origin, self.scale);
append_with_transform(&self.transform, raw_bounds, geometry, |g| {
let mut pixels = Vec::with_capacity(self.data.len() * 4);
for &v in &self.data {
let [r, gr, b, a] = self.colormap.color_at(v);
pixels.extend_from_slice(&premul_linear_rgba8(Color32::from_rgba_unmultiplied(
r, gr, b, a,
)));
}
g.add_image_layer(Scene3dImageLayer {
pixels,
width: self.width as u32,
height: self.height as u32,
origin: self.origin,
scale: self.scale,
interpolation: self.interpolation,
});
});
}
}
impl_item3d_transform!(ImageData3D);
#[derive(Clone, Debug)]
pub struct ImageRgba3D {
pixels: Vec<Color32>,
width: usize,
height: usize,
origin: [f32; 3],
scale: [f32; 2],
interpolation: ImageInterpolation,
transform: Item3DTransform,
}
impl Default for ImageRgba3D {
fn default() -> Self {
Self::new()
}
}
impl ImageRgba3D {
pub fn new() -> Self {
Self {
pixels: Vec::new(),
width: 0,
height: 0,
origin: [0.0, 0.0, 0.0],
scale: [1.0, 1.0],
interpolation: ImageInterpolation::Nearest,
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, pixels: &[Color32], width: usize, height: usize) -> bool {
if pixels.len() != width * height {
return false;
}
self.pixels = pixels.to_vec();
self.width = width;
self.height = height;
true
}
pub fn with_data(mut self, pixels: &[Color32], width: usize, height: usize) -> Self {
self.set_data(pixels, width, height);
self
}
pub fn set_origin(&mut self, origin: [f32; 3]) {
self.origin = origin;
}
pub fn with_origin(mut self, origin: [f32; 3]) -> Self {
self.origin = origin;
self
}
pub fn set_scale(&mut self, scale: [f32; 2]) {
self.scale = scale;
}
pub fn with_scale(mut self, scale: [f32; 2]) -> Self {
self.scale = scale;
self
}
pub fn set_interpolation(&mut self, interpolation: ImageInterpolation) {
self.interpolation = interpolation;
}
pub fn with_interpolation(mut self, interpolation: ImageInterpolation) -> Self {
self.interpolation = interpolation;
self
}
pub fn dimensions(&self) -> (usize, usize) {
(self.width, self.height)
}
pub fn is_empty(&self) -> bool {
self.pixels.is_empty()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(image_bounds(
self.width,
self.height,
self.origin,
self.scale,
))
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
let raw_bounds = image_bounds(self.width, self.height, self.origin, self.scale);
append_with_transform(&self.transform, raw_bounds, geometry, |g| {
let mut pixels = Vec::with_capacity(self.pixels.len() * 4);
for &c in &self.pixels {
pixels.extend_from_slice(&premul_linear_rgba8(c));
}
g.add_image_layer(Scene3dImageLayer {
pixels,
width: self.width as u32,
height: self.height as u32,
origin: self.origin,
scale: self.scale,
interpolation: self.interpolation,
});
});
}
}
impl_item3d_transform!(ImageRgba3D);
fn nearest_src_index(i: usize, dst_len: usize, src_len: usize) -> usize {
((i as f64 * src_len as f64 / dst_len as f64).floor() as usize).min(src_len.saturating_sub(1))
}
fn height_grid_bounds(heights: &[f32], width: usize, height: usize) -> Option<(Vec3, Vec3)> {
if heights.is_empty() || width == 0 || height == 0 {
return None;
}
let mut zmin = f32::INFINITY;
let mut zmax = f32::NEG_INFINITY;
for &z in heights {
zmin = zmin.min(z);
zmax = zmax.max(z);
}
Some((
Vec3::new(0.0, 0.0, zmin),
Vec3::new((width - 1) as f32, (height - 1) as f32, zmax),
))
}
#[derive(Clone, Debug)]
pub struct HeightMapData {
heights: Vec<f32>,
h_width: usize,
h_height: usize,
values: Vec<f64>,
v_width: usize,
v_height: usize,
colormap: Colormap,
transform: Item3DTransform,
}
impl Default for HeightMapData {
fn default() -> Self {
Self::new()
}
}
impl HeightMapData {
pub fn new() -> Self {
Self {
heights: Vec::new(),
h_width: 0,
h_height: 0,
values: Vec::new(),
v_width: 0,
v_height: 0,
colormap: Colormap::autoscale(ColormapName::Gray),
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, heights: &[f32], width: usize, height: usize) -> bool {
if heights.len() != width * height {
return false;
}
self.heights = heights.to_vec();
self.h_width = width;
self.h_height = height;
true
}
pub fn with_data(mut self, heights: &[f32], width: usize, height: usize) -> Self {
self.set_data(heights, width, height);
self
}
pub fn set_colormapped_data(&mut self, data: &[f64], width: usize, height: usize) -> bool {
if data.len() != width * height {
return false;
}
self.values = data.to_vec();
self.v_width = width;
self.v_height = height;
self.resolve_colormap();
true
}
pub fn with_colormapped_data(mut self, data: &[f64], width: usize, height: usize) -> Self {
self.set_colormapped_data(data, width, height);
self
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
self.resolve_colormap();
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.set_colormap(colormap);
self
}
fn resolve_colormap(&mut self) {
self.colormap = self
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &self.values);
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
pub fn autoscale_colormap(&mut self, mode: AutoscaleMode) -> (f64, f64) {
let (vmin, vmax) = self.colormap.autoscale_range(mode, &self.values);
self.colormap.vmin = vmin;
self.colormap.vmax = vmax;
(vmin, vmax)
}
pub fn dimensions(&self) -> (usize, usize) {
(self.h_width, self.h_height)
}
pub fn is_empty(&self) -> bool {
self.heights.is_empty() || self.values.is_empty()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(height_grid_bounds(
&self.heights,
self.h_width,
self.h_height,
))
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
let raw_bounds = height_grid_bounds(&self.heights, self.h_width, self.h_height);
append_with_transform(&self.transform, raw_bounds, geometry, |g| {
for row in 0..self.h_height {
let vr = nearest_src_index(row, self.h_height, self.v_height);
for col in 0..self.h_width {
let vc = nearest_src_index(col, self.h_width, self.v_width);
let z = self.heights[row * self.h_width + col];
let [r, gr, b, a] = self.colormap.color_at(self.values[vr * self.v_width + vc]);
g.add_point(
[col as f32, row as f32, z],
Color32::from_rgba_unmultiplied(r, gr, b, a),
1.0,
PointMarker::Square,
);
}
}
});
}
}
impl_item3d_transform!(HeightMapData);
#[derive(Clone, Debug)]
pub struct HeightMapRGBA {
heights: Vec<f32>,
h_width: usize,
h_height: usize,
colors: Vec<Color32>,
c_width: usize,
c_height: usize,
transform: Item3DTransform,
}
impl Default for HeightMapRGBA {
fn default() -> Self {
Self::new()
}
}
impl HeightMapRGBA {
pub fn new() -> Self {
Self {
heights: Vec::new(),
h_width: 0,
h_height: 0,
colors: Vec::new(),
c_width: 0,
c_height: 0,
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, heights: &[f32], width: usize, height: usize) -> bool {
if heights.len() != width * height {
return false;
}
self.heights = heights.to_vec();
self.h_width = width;
self.h_height = height;
true
}
pub fn with_data(mut self, heights: &[f32], width: usize, height: usize) -> Self {
self.set_data(heights, width, height);
self
}
pub fn set_color_data(&mut self, colors: &[Color32], width: usize, height: usize) -> bool {
if colors.len() != width * height {
return false;
}
self.colors = colors.to_vec();
self.c_width = width;
self.c_height = height;
true
}
pub fn with_color_data(mut self, colors: &[Color32], width: usize, height: usize) -> Self {
self.set_color_data(colors, width, height);
self
}
pub fn dimensions(&self) -> (usize, usize) {
(self.h_width, self.h_height)
}
pub fn is_empty(&self) -> bool {
self.heights.is_empty() || self.colors.is_empty()
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(height_grid_bounds(
&self.heights,
self.h_width,
self.h_height,
))
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
let raw_bounds = height_grid_bounds(&self.heights, self.h_width, self.h_height);
append_with_transform(&self.transform, raw_bounds, geometry, |g| {
for row in 0..self.h_height {
let cr = nearest_src_index(row, self.h_height, self.c_height);
for col in 0..self.h_width {
let cc = nearest_src_index(col, self.h_width, self.c_width);
let z = self.heights[row * self.h_width + col];
let color = self.colors[cr * self.c_width + cc];
g.add_point([col as f32, row as f32, z], color, 1.0, PointMarker::Square);
}
}
});
}
}
impl_item3d_transform!(HeightMapRGBA);
pub const DEFAULT_ISOSURFACE_COLOR: Color32 = Color32::from_rgb(0xFF, 0xD7, 0x00);
pub fn mean_plus_std(data: &[f32]) -> f32 {
let finite: Vec<f64> = data
.iter()
.filter(|v| v.is_finite())
.map(|&v| v as f64)
.collect();
if finite.is_empty() {
return f32::NAN;
}
let n = finite.len() as f64;
let mean = finite.iter().sum::<f64>() / n;
let var = finite.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / n;
(mean + var.sqrt()) as f32
}
#[derive(Clone, Debug)]
pub struct Isosurface {
level: f32,
auto: Option<fn(&[f32]) -> f32>,
color: Color32,
visible: bool,
}
impl Isosurface {
pub fn new(level: f32, color: Color32) -> Self {
Self {
level,
auto: None,
color,
visible: true,
}
}
pub fn new_auto(auto: fn(&[f32]) -> f32, color: Color32) -> Self {
Self {
level: f32::NAN,
auto: Some(auto),
color,
visible: true,
}
}
pub fn level(&self) -> f32 {
self.level
}
pub fn set_level(&mut self, level: f32) {
self.level = level;
self.auto = None;
}
pub fn set_auto_level(&mut self, auto: fn(&[f32]) -> f32) {
self.auto = Some(auto);
}
pub fn is_auto_level(&self) -> bool {
self.auto.is_some()
}
pub fn color(&self) -> Color32 {
self.color
}
pub fn set_color(&mut self, color: Color32) {
self.color = color;
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
fn resolve(&mut self, data: &[f32]) {
if let Some(f) = self.auto {
self.level = f(data);
}
}
}
pub const DEFAULT_CUT_PLANE_RESOLUTION: usize = 256;
#[derive(Clone, Debug)]
pub struct CutPlane {
plane: Plane,
colormap: Colormap,
interpolation: ImageInterpolation,
resolution: usize,
visible: bool,
stroke_color: Color32,
stroke_visible: bool,
display_values_below_min: bool,
}
impl Default for CutPlane {
fn default() -> Self {
Self::new()
}
}
impl CutPlane {
pub fn new() -> Self {
Self {
plane: Plane::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)),
colormap: Colormap::autoscale(ColormapName::Gray),
interpolation: ImageInterpolation::Linear,
resolution: DEFAULT_CUT_PLANE_RESOLUTION,
visible: false,
stroke_color: Color32::WHITE,
stroke_visible: true,
display_values_below_min: true,
}
}
pub fn plane(&self) -> &Plane {
&self.plane
}
pub fn plane_mut(&mut self) -> &mut Plane {
&mut self.plane
}
pub fn set_point(&mut self, point: Vec3) {
self.plane.set_point(point);
}
pub fn set_normal(&mut self, normal: Vec3) {
self.plane.set_normal(normal);
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.colormap = colormap;
self
}
pub fn interpolation(&self) -> ImageInterpolation {
self.interpolation
}
pub fn set_interpolation(&mut self, interpolation: ImageInterpolation) {
self.interpolation = interpolation;
}
pub fn resolution(&self) -> usize {
self.resolution
}
pub fn set_resolution(&mut self, resolution: usize) {
self.resolution = resolution.max(1);
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
pub fn stroke_color(&self) -> Color32 {
self.stroke_color
}
pub fn set_stroke_color(&mut self, color: Color32) {
self.stroke_color = color;
}
pub fn is_stroke_visible(&self) -> bool {
self.stroke_visible
}
pub fn set_stroke_visible(&mut self, visible: bool) {
self.stroke_visible = visible;
}
pub fn display_values_below_min(&self) -> bool {
self.display_values_below_min
}
pub fn set_display_values_below_min(&mut self, display: bool) {
self.display_values_below_min = display;
}
}
fn plane_basis(normal: Vec3) -> (Vec3, Vec3) {
let seed = if normal.x.abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let e1 = normal.cross(seed).normalized();
let e2 = normal.cross(e1).normalized();
(e1, e2)
}
fn sample_field_value(
data: &[f32],
depth: usize,
height: usize,
width: usize,
p: Vec3,
interpolation: ImageInterpolation,
) -> f32 {
let idx = |ix: usize, iy: usize, iz: usize| data[(iz * height + iy) * width + ix];
let (fx, fy, fz) = (p.x - 0.5, p.y - 0.5, p.z - 0.5);
match interpolation {
ImageInterpolation::Nearest => {
let clamp = |f: f32, n: usize| (f.round().max(0.0) as usize).min(n - 1);
idx(clamp(fx, width), clamp(fy, height), clamp(fz, depth))
}
ImageInterpolation::Linear => {
let lo = |f: f32, n: usize| -> (usize, usize, f32) {
let c = f.clamp(0.0, (n - 1) as f32);
let i0 = c.floor() as usize;
let i1 = (i0 + 1).min(n - 1);
(i0, i1, c - i0 as f32)
};
let (x0, x1, dx) = lo(fx, width);
let (y0, y1, dy) = lo(fy, height);
let (z0, z1, dz) = lo(fz, depth);
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
let c00 = lerp(idx(x0, y0, z0), idx(x1, y0, z0), dx);
let c10 = lerp(idx(x0, y1, z0), idx(x1, y1, z0), dx);
let c01 = lerp(idx(x0, y0, z1), idx(x1, y0, z1), dx);
let c11 = lerp(idx(x0, y1, z1), idx(x1, y1, z1), dx);
lerp(lerp(c00, c10, dy), lerp(c01, c11, dy), dz)
}
}
}
fn build_cut_plane_mesh(
data: &[f32],
depth: usize,
height: usize,
width: usize,
cut_plane: &CutPlane,
) -> Option<Scene3dTexturedMesh> {
if data.is_empty() {
return None;
}
let normal = cut_plane.plane.normal();
let bounds = (
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(width as f32, height as f32, depth as f32),
);
let contour = box_plane_intersect(bounds, normal, cut_plane.plane.point());
if contour.len() < 3 {
return None;
}
let (e1, e2) = plane_basis(normal);
let origin = contour[0];
let st: Vec<(f32, f32)> = contour
.iter()
.map(|&v| {
let d = v - origin;
(d.dot(e1), d.dot(e2))
})
.collect();
let (mut smin, mut smax) = (f32::INFINITY, f32::NEG_INFINITY);
let (mut tmin, mut tmax) = (f32::INFINITY, f32::NEG_INFINITY);
for &(s, t) in &st {
smin = smin.min(s);
smax = smax.max(s);
tmin = tmin.min(t);
tmax = tmax.max(t);
}
let sspan = (smax - smin).max(f32::MIN_POSITIVE);
let tspan = (tmax - tmin).max(f32::MIN_POSITIVE);
let res = cut_plane.resolution.max(1);
let mut pixels = Vec::with_capacity(res * res * 4);
for j in 0..res {
let t = tmin + (j as f32 + 0.5) / res as f32 * tspan;
for i in 0..res {
let s = smin + (i as f32 + 0.5) / res as f32 * sspan;
let p = origin + e1 * s + e2 * t;
let value = sample_field_value(data, depth, height, width, p, cut_plane.interpolation);
let color = if !cut_plane.display_values_below_min
&& !(value as f64).is_nan()
&& cut_plane.colormap.normalize(value as f64) == 0.0
{
Color32::TRANSPARENT
} else {
let [r, g, b, a] = cut_plane.colormap.color_at(value as f64);
Color32::from_rgba_unmultiplied(r, g, b, a)
};
pixels.extend_from_slice(&premul_linear_rgba8(color));
}
}
let uv = |k: usize| [(st[k].0 - smin) / sspan, (st[k].1 - tmin) / tspan];
let mut vertices = Vec::with_capacity((contour.len() - 2) * 3);
let mut uvs = Vec::with_capacity((contour.len() - 2) * 3);
for k in 1..contour.len() - 1 {
for &idx in &[0usize, k, k + 1] {
vertices.push(contour[idx].to_array());
uvs.push(uv(idx));
}
}
Some(Scene3dTexturedMesh {
pixels,
width: res as u32,
height: res as u32,
vertices,
uvs,
interpolation: cut_plane.interpolation,
})
}
#[derive(Clone, Debug)]
pub struct ScalarField3D {
data: Vec<f32>,
depth: usize,
height: usize,
width: usize,
data_range: Option<(f32, f32, f32)>,
isosurfaces: Vec<Isosurface>,
cut_plane: CutPlane,
transform: Item3DTransform,
}
impl Default for ScalarField3D {
fn default() -> Self {
Self::new()
}
}
impl ScalarField3D {
pub fn new() -> Self {
Self {
data: Vec::new(),
depth: 0,
height: 0,
width: 0,
data_range: None,
isosurfaces: Vec::new(),
cut_plane: CutPlane::new(),
transform: Item3DTransform::default(),
}
}
pub fn set_data(&mut self, data: &[f32], depth: usize, height: usize, width: usize) -> bool {
if depth < 2 || height < 2 || width < 2 || data.len() != depth * height * width {
return false;
}
self.data = data.to_vec();
self.depth = depth;
self.height = height;
self.width = width;
self.data_range = compute_data_range(&self.data);
let data = std::mem::take(&mut self.data);
for iso in &mut self.isosurfaces {
iso.resolve(&data);
}
self.data = data;
self.resolve_cut_plane_colormap();
true
}
pub fn with_data(mut self, data: &[f32], depth: usize, height: usize, width: usize) -> Self {
self.set_data(data, depth, height, width);
self
}
pub fn dimensions(&self) -> (usize, usize, usize) {
(self.depth, self.height, self.width)
}
pub fn data(&self) -> &[f32] {
&self.data
}
pub fn data_range(&self) -> Option<(f32, f32, f32)> {
self.data_range
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn add_isosurface(&mut self, level: f32, color: Color32) -> usize {
self.isosurfaces.push(Isosurface::new(level, color));
self.isosurfaces.len() - 1
}
pub fn add_auto_isosurface(&mut self, auto: fn(&[f32]) -> f32, color: Color32) -> usize {
let mut iso = Isosurface::new_auto(auto, color);
if !self.data.is_empty() {
iso.resolve(&self.data);
}
self.isosurfaces.push(iso);
self.isosurfaces.len() - 1
}
pub fn isosurfaces(&self) -> &[Isosurface] {
&self.isosurfaces
}
pub fn isosurface_mut(&mut self, index: usize) -> Option<&mut Isosurface> {
self.isosurfaces.get_mut(index)
}
pub fn remove_isosurface(&mut self, index: usize) -> bool {
if index < self.isosurfaces.len() {
self.isosurfaces.remove(index);
true
} else {
false
}
}
pub fn clear_isosurfaces(&mut self) {
self.isosurfaces.clear();
}
pub fn cut_plane(&self) -> &CutPlane {
&self.cut_plane
}
pub fn cut_plane_mut(&mut self) -> &mut CutPlane {
&mut self.cut_plane
}
pub fn set_cut_plane_colormap(&mut self, colormap: Colormap) {
self.cut_plane.colormap = colormap;
self.resolve_cut_plane_colormap();
}
fn resolve_cut_plane_colormap(&mut self) {
if self.data.is_empty() || !self.cut_plane.colormap.is_autoscale() {
return;
}
let values: Vec<f64> = self.data.iter().map(|&v| v as f64).collect();
self.cut_plane.colormap = self
.cut_plane
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &values);
}
pub fn autoscale_cut_plane_colormap(&mut self, mode: AutoscaleMode) -> (f64, f64) {
if self.data.is_empty() {
let cm = &self.cut_plane.colormap;
return (cm.vmin, cm.vmax);
}
let values: Vec<f64> = self.data.iter().map(|&v| v as f64).collect();
let (vmin, vmax) = self.cut_plane.colormap.autoscale_range(mode, &values);
self.cut_plane.colormap.vmin = vmin;
self.cut_plane.colormap.vmax = vmax;
(vmin, vmax)
}
fn raw_bounds(&self) -> Option<(Vec3, Vec3)> {
if self.data.is_empty() {
return None;
}
Some((
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(self.width as f32, self.height as f32, self.depth as f32),
))
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.transform.transform_bounds(self.raw_bounds())
}
fn scene_to_object(&self, p: Vec3) -> Option<Vec3> {
if self.transform.is_identity() {
return Some(p);
}
let inv = self
.transform
.composed_matrix(self.raw_bounds())
.inverse()?;
Some(inv.transform_point(p, false))
}
fn value_at_object(&self, obj: Vec3) -> Option<f32> {
let (min, max) = self.raw_bounds()?;
if obj.x < min.x
|| obj.y < min.y
|| obj.z < min.z
|| obj.x > max.x
|| obj.y > max.y
|| obj.z > max.z
{
return None;
}
Some(sample_field_value(
&self.data,
self.depth,
self.height,
self.width,
obj,
self.cut_plane.interpolation(),
))
}
pub fn value_at(&self, world: Vec3) -> Option<f32> {
self.value_at_object(self.scene_to_object(world)?)
}
pub fn pick_cut_plane(&self, segment: (Vec3, Vec3)) -> Option<Vec3> {
if !self.cut_plane.is_visible() || self.data.is_empty() {
return None;
}
let a = self.scene_to_object(segment.0)?;
let b = self.scene_to_object(segment.1)?;
let plane = self.cut_plane.plane();
let hit = segment_plane_intersect(a, b, plane.normal(), plane.point())
.into_iter()
.find(|&hit| self.value_at_object(hit).is_some())?;
Some(if self.transform.is_identity() {
hit
} else {
self.transform
.composed_matrix(self.raw_bounds())
.transform_point(hit, false)
})
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.data.is_empty() {
return;
}
append_with_transform(&self.transform, self.raw_bounds(), geometry, |g| {
self.append_raw(g)
});
}
fn append_raw(&self, geometry: &mut Scene3dGeometry) {
append_solid_isosurfaces(
geometry,
&self.data,
self.depth,
self.height,
self.width,
&self.isosurfaces,
);
append_cut_plane(
geometry,
&self.data,
self.depth,
self.height,
self.width,
&self.cut_plane,
self.raw_bounds(),
);
}
}
fn iso_triangle_positions(vertices: &[[f32; 3]], tri: &[u32]) -> [[f32; 3]; 3] {
[0usize, 1, 2].map(|k| {
let v = vertices[tri[k] as usize];
[v[2] + 0.5, v[1] + 0.5, v[0] + 0.5]
})
}
fn iso_triangle_normals(normals: &[[f32; 3]], tri: &[u32]) -> [[f32; 3]; 3] {
[0usize, 1, 2].map(|k| {
let nm = normals[tri[k] as usize];
[nm[2], nm[1], nm[0]]
})
}
fn append_solid_isosurfaces(
geometry: &mut Scene3dGeometry,
data: &[f32],
depth: usize,
height: usize,
width: usize,
isosurfaces: &[Isosurface],
) {
let mut order: Vec<usize> = (0..isosurfaces.len()).collect();
order.sort_by(|&a, &b| isosurfaces[b].level.total_cmp(&isosurfaces[a].level));
for i in order {
let iso = &isosurfaces[i];
if !iso.visible || !iso.level.is_finite() {
continue;
}
let Some((vertices, normals, indices)) =
marching_cubes_isosurface(data, depth, height, width, iso.level, true)
else {
continue;
};
for tri in indices.chunks_exact(3) {
let p = iso_triangle_positions(&vertices, tri);
let n = iso_triangle_normals(&normals, tri);
geometry.add_mesh_triangle(p, iso.color, n);
}
}
}
fn append_colormapped_isosurface(
geometry: &mut Scene3dGeometry,
geom_data: &[f32],
color_data: &[f32],
dims: (usize, usize, usize),
iso: &ComplexIsosurface,
) {
let level = iso.level();
if !iso.is_visible() || !level.is_finite() {
return;
}
let (depth, height, width) = dims;
let Some((vertices, normals, indices)) =
marching_cubes_isosurface(geom_data, depth, height, width, level, true)
else {
return;
};
let (colormap, alpha) = (iso.colormap(), iso.alpha());
for tri in indices.chunks_exact(3) {
let p = iso_triangle_positions(&vertices, tri);
let n = iso_triangle_normals(&normals, tri);
let rgba = [0usize, 1, 2].map(|k| {
let v = vertices[tri[k] as usize];
let world = Vec3::new(v[2] + 0.5, v[1] + 0.5, v[0] + 0.5);
let value = sample_field_value(
color_data,
depth,
height,
width,
world,
ImageInterpolation::Linear,
) as f64;
let [r, g, b, _a] = colormap.color_at(value);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, g, b, alpha)).to_array()
});
geometry.add_mesh_triangle_rgba(p, rgba, n);
}
}
fn append_cut_plane(
geometry: &mut Scene3dGeometry,
data: &[f32],
depth: usize,
height: usize,
width: usize,
cut_plane: &CutPlane,
raw_bounds: Option<(Vec3, Vec3)>,
) {
if !cut_plane.visible {
return;
}
if let Some(mesh) = build_cut_plane_mesh(data, depth, height, width, cut_plane) {
geometry.add_textured_mesh(mesh);
}
if cut_plane.stroke_visible
&& let Some(bounds) = raw_bounds
{
let contour =
box_plane_intersect(bounds, cut_plane.plane.normal(), cut_plane.plane.point());
if contour.len() >= 3 {
for (i, &a) in contour.iter().enumerate() {
let b = contour[(i + 1) % contour.len()];
geometry.add_line(a.to_array(), b.to_array(), cut_plane.stroke_color);
}
}
}
}
impl_item3d_transform!(ScalarField3D);
fn compute_data_range(data: &[f32]) -> Option<(f32, f32, f32)> {
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
let mut min_pos = f32::INFINITY;
let mut any = false;
for &v in data {
if !v.is_finite() {
continue;
}
any = true;
min = min.min(v);
max = max.max(v);
if v > 0.0 {
min_pos = min_pos.min(v);
}
}
if !any {
return None;
}
let min_pos = if min_pos.is_finite() {
min_pos
} else {
f32::NAN
};
Some((min, min_pos, max))
}
#[derive(Clone, Debug)]
pub struct ComplexIsosurface {
iso: Isosurface,
color_mode: ComplexMode,
colormap: Colormap,
}
impl ComplexIsosurface {
pub fn new(level: f32, color_mode: ComplexMode, colormap: Colormap, color: Color32) -> Self {
Self {
iso: Isosurface::new(level, color),
color_mode,
colormap,
}
}
pub fn new_auto(
auto: fn(&[f32]) -> f32,
color_mode: ComplexMode,
colormap: Colormap,
color: Color32,
) -> Self {
Self {
iso: Isosurface::new_auto(auto, color),
color_mode,
colormap,
}
}
pub fn level(&self) -> f32 {
self.iso.level()
}
pub fn set_level(&mut self, level: f32) {
self.iso.set_level(level);
}
pub fn set_auto_level(&mut self, auto: fn(&[f32]) -> f32) {
self.iso.set_auto_level(auto);
}
pub fn is_auto_level(&self) -> bool {
self.iso.is_auto_level()
}
pub fn is_visible(&self) -> bool {
self.iso.is_visible()
}
pub fn set_visible(&mut self, visible: bool) {
self.iso.set_visible(visible);
}
pub fn color_mode(&self) -> ComplexMode {
self.color_mode
}
pub fn set_color_mode(&mut self, mode: ComplexMode) {
self.color_mode = mode;
}
pub fn alpha(&self) -> u8 {
self.iso.color().a()
}
pub fn set_alpha(&mut self, alpha: u8) {
let c = self.iso.color();
self.iso
.set_color(Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), alpha));
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn colormap_mut(&mut self) -> &mut Colormap {
&mut self.colormap
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
}
fn resolve(&mut self, geom_data: &[f32], color_data: &[f32]) {
self.iso.resolve(geom_data);
let values: Vec<f64> = color_data.iter().map(|&v| f64::from(v)).collect();
self.colormap = self
.colormap
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &values);
}
}
#[derive(Clone, Debug)]
pub struct ComplexField3D {
re: Vec<f32>,
im: Vec<f32>,
depth: usize,
height: usize,
width: usize,
mode: ComplexMode,
cut_plane_mode: Option<ComplexMode>,
colormapped_isosurfaces: Vec<ComplexIsosurface>,
field: ScalarField3D,
}
impl Default for ComplexField3D {
fn default() -> Self {
Self::new()
}
}
impl ComplexField3D {
pub fn new() -> Self {
Self {
re: Vec::new(),
im: Vec::new(),
mode: ComplexMode::Absolute,
depth: 0,
height: 0,
width: 0,
cut_plane_mode: None,
colormapped_isosurfaces: Vec::new(),
field: ScalarField3D::new(),
}
}
pub fn set_data(
&mut self,
re: &[f32],
im: &[f32],
depth: usize,
height: usize,
width: usize,
) -> bool {
if depth < 2 || height < 2 || width < 2 {
return false;
}
let n = depth * height * width;
if re.len() != n || im.len() != n {
return false;
}
self.re = re.to_vec();
self.im = im.to_vec();
self.depth = depth;
self.height = height;
self.width = width;
self.reproject();
true
}
pub fn with_data(
mut self,
re: &[f32],
im: &[f32],
depth: usize,
height: usize,
width: usize,
) -> Self {
self.set_data(re, im, depth, height, width);
self
}
pub fn complex_mode(&self) -> ComplexMode {
self.mode
}
pub fn set_complex_mode(&mut self, mode: ComplexMode) {
if mode == self.mode {
return;
}
self.mode = mode;
self.field.clear_isosurfaces();
self.colormapped_isosurfaces.clear();
self.reproject();
}
pub fn projected_data(&self, mode: ComplexMode) -> Option<Vec<f32>> {
if self.re.is_empty() {
return None;
}
Some(
self.re
.iter()
.zip(&self.im)
.map(|(&r, &i)| mode.to_scalar(r, i))
.collect(),
)
}
pub fn data_range_for(&self, mode: ComplexMode) -> Option<(f32, f32, f32)> {
let data = self.projected_data(mode)?;
compute_data_range(&data)
}
pub fn dimensions(&self) -> (usize, usize, usize) {
(self.depth, self.height, self.width)
}
pub fn is_empty(&self) -> bool {
self.re.is_empty()
}
pub fn field(&self) -> &ScalarField3D {
&self.field
}
pub fn field_mut(&mut self) -> &mut ScalarField3D {
&mut self.field
}
pub fn cut_plane_mode(&self) -> Option<ComplexMode> {
self.cut_plane_mode
}
pub fn set_cut_plane_mode(&mut self, mode: Option<ComplexMode>) {
self.cut_plane_mode = mode;
}
pub fn add_colormapped_isosurface(&mut self, mut iso: ComplexIsosurface) -> usize {
if !self.re.is_empty() {
let color_data = self.projected_data(iso.color_mode).unwrap_or_default();
iso.resolve(self.field.data(), &color_data);
}
self.colormapped_isosurfaces.push(iso);
self.colormapped_isosurfaces.len() - 1
}
pub fn colormapped_isosurfaces(&self) -> &[ComplexIsosurface] {
&self.colormapped_isosurfaces
}
pub fn colormapped_isosurface_mut(&mut self, index: usize) -> Option<&mut ComplexIsosurface> {
self.colormapped_isosurfaces.get_mut(index)
}
pub fn remove_colormapped_isosurface(&mut self, index: usize) {
if index < self.colormapped_isosurfaces.len() {
self.colormapped_isosurfaces.remove(index);
}
}
pub fn clear_colormapped_isosurfaces(&mut self) {
self.colormapped_isosurfaces.clear();
}
pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
self.field.bounds()
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.field.data().is_empty() {
return;
}
append_with_transform(
self.field.transform(),
self.field.raw_bounds(),
geometry,
|g| self.append_raw(g),
);
}
fn append_raw(&self, geometry: &mut Scene3dGeometry) {
let (depth, height, width) = (self.depth, self.height, self.width);
append_solid_isosurfaces(
geometry,
self.field.data(),
depth,
height,
width,
&self.field.isosurfaces,
);
for iso in &self.colormapped_isosurfaces {
let Some(color_data) = self.projected_data(iso.color_mode) else {
continue;
};
append_colormapped_isosurface(
geometry,
self.field.data(),
&color_data,
(depth, height, width),
iso,
);
}
match self.cut_plane_mode {
Some(mode) if mode != self.mode => {
if let Some(data) = self.projected_data(mode) {
let mut cut_plane = self.field.cut_plane().clone();
let values: Vec<f64> = data.iter().map(|&v| f64::from(v)).collect();
*cut_plane.colormap_mut() = self
.field
.cut_plane()
.colormap()
.resolved(DEFAULT_COLORMAP_AUTOSCALE_MODE, &values);
append_cut_plane(
geometry,
&data,
depth,
height,
width,
&cut_plane,
self.field.raw_bounds(),
);
}
}
_ => append_cut_plane(
geometry,
self.field.data(),
depth,
height,
width,
self.field.cut_plane(),
self.field.raw_bounds(),
),
}
}
pub fn transform(&self) -> &Item3DTransform {
self.field.transform()
}
pub fn transform_mut(&mut self) -> &mut Item3DTransform {
self.field.transform_mut()
}
fn reproject(&mut self) {
let data: Vec<f32> = self
.re
.iter()
.zip(&self.im)
.map(|(&r, &i)| self.mode.to_scalar(r, i))
.collect();
self.field
.set_data(&data, self.depth, self.height, self.width);
for i in 0..self.colormapped_isosurfaces.len() {
let color_mode = self.colormapped_isosurfaces[i].color_mode;
let color_data = self.projected_data(color_mode).unwrap_or_default();
self.colormapped_isosurfaces[i].resolve(&data, &color_data);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ramp_field() -> ScalarField3D {
let (d, h, w) = (3usize, 3usize, 3usize);
let mut data = vec![0.0f32; d * h * w];
for z in 0..d {
for y in 0..h {
for x in 0..w {
data[(z * h + y) * w + x] = z as f32;
}
}
}
ScalarField3D::new().with_data(&data, d, h, w)
}
#[test]
fn colormapped_items_default_to_gray_autoscale_like_silx() {
assert!(Scatter3D::new().colormap().is_autoscale());
assert!(Scatter2D::new().colormap().is_autoscale());
assert!(ColormapMesh3D::new().colormap().is_autoscale());
assert!(ImageData3D::new().colormap().is_autoscale());
assert!(HeightMapData::new().colormap().is_autoscale());
assert!(CutPlane::new().colormap().is_autoscale());
assert_eq!(
Scatter3D::new().colormap().lut,
Colormap::new(ColormapName::Gray, 0.0, 1.0).lut
);
}
#[test]
fn set_data_resolves_the_default_colormap_to_the_data_range() {
let mut s = Scatter3D::new();
s.set_data(&[0.0, 1.0, 2.0], &[0.0; 3], &[0.0; 3], &[10.0, 30.0, 20.0]);
assert_eq!((s.colormap().vmin, s.colormap().vmax), (10.0, 30.0));
let mut img = ImageData3D::new();
img.set_data(&[-5.0, 0.0, 5.0, 15.0], 2, 2);
assert_eq!((img.colormap().vmin, img.colormap().vmax), (-5.0, 15.0));
}
#[test]
fn set_data_leaves_a_pinned_colormap_untouched() {
let mut s = Scatter3D::new();
s.set_colormap(Colormap::new(ColormapName::Viridis, 0.0, 1.0));
s.set_data(&[0.0, 1.0], &[0.0; 2], &[0.0; 2], &[100.0, 200.0]);
assert_eq!((s.colormap().vmin, s.colormap().vmax), (0.0, 1.0));
}
#[test]
fn cut_plane_autoscales_to_the_volume_on_set_data() {
let field = ramp_field();
let cm = field.cut_plane().colormap();
assert_eq!((cm.vmin, cm.vmax), (0.0, 2.0));
}
#[test]
fn set_cut_plane_colormap_resolves_against_the_volume() {
let mut field = ramp_field();
field.set_cut_plane_colormap(Colormap::autoscale(ColormapName::Viridis));
let cm = field.cut_plane().colormap();
assert_eq!(cm.lut, Colormap::new(ColormapName::Viridis, 0.0, 1.0).lut);
assert_eq!((cm.vmin, cm.vmax), (0.0, 2.0));
}
#[test]
fn value_at_samples_inside_and_rejects_outside_the_box() {
let field = ramp_field();
let v = field
.value_at(Vec3::new(1.5, 1.5, 1.5))
.expect("inside the box");
assert!((v - 1.0).abs() < 1e-5, "sampled {v}");
let mid = field.value_at(Vec3::new(1.5, 1.5, 2.0)).expect("inside");
assert!((mid - 1.5).abs() < 1e-5, "sampled {mid}");
let top = field.value_at(Vec3::new(1.5, 1.5, 2.5)).expect("inside");
assert!((top - 2.0).abs() < 1e-5, "sampled {top}");
assert!(field.value_at(Vec3::new(3.5, 1.0, 1.0)).is_none());
assert!(field.value_at(Vec3::new(-0.1, 1.0, 1.0)).is_none());
assert!(
ScalarField3D::new()
.value_at(Vec3::new(1.0, 1.0, 1.0))
.is_none()
);
}
#[test]
fn pick_cut_plane_hidden_is_none_visible_hits_inside_box() {
let mut field = ramp_field();
field.cut_plane_mut().set_point(Vec3::new(0.0, 1.5, 0.0));
let seg = (Vec3::new(1.5, 3.0, 1.5), Vec3::new(1.5, 0.0, 1.5));
assert!(field.pick_cut_plane(seg).is_none());
field.cut_plane_mut().set_visible(true);
let hit = field
.pick_cut_plane(seg)
.expect("visible plane is crossed inside the box");
assert!((hit.y - 1.5).abs() < 1e-5, "hit on the plane: {hit:?}");
let value = field.value_at(hit).expect("hit is inside the box");
assert!((value - 1.0).abs() < 1e-5, "value {value}");
}
#[test]
fn pick_cut_plane_outside_box_is_none() {
let mut field = ramp_field();
field.cut_plane_mut().set_visible(true);
field.cut_plane_mut().set_point(Vec3::new(0.0, 1.5, 0.0));
let seg = (Vec3::new(9.0, 3.0, 9.0), Vec3::new(9.0, 0.0, 9.0));
assert!(field.pick_cut_plane(seg).is_none());
}
#[test]
fn field_transform_moves_bounds_value_and_cut_plane_pick_together() {
let mut field = ramp_field();
field.transform_mut().set_translation(10.0, 0.0, 0.0);
let (lo, hi) = field.bounds().expect("has data");
assert_eq!(lo, Vec3::new(10.0, 0.0, 0.0));
assert_eq!(hi, Vec3::new(13.0, 3.0, 3.0));
let v = field
.value_at(Vec3::new(11.5, 1.5, 1.5))
.expect("inside the translated box");
assert!((v - 1.0).abs() < 1e-5, "sampled {v}");
assert!(field.value_at(Vec3::new(1.5, 1.5, 1.5)).is_none());
field.cut_plane_mut().set_visible(true);
field.cut_plane_mut().set_point(Vec3::new(0.0, 1.5, 0.0));
let seg = (Vec3::new(11.5, 3.0, 1.5), Vec3::new(11.5, 0.0, 1.5));
let hit = field.pick_cut_plane(seg).expect("crosses inside the box");
assert!(
(hit.x - 11.5).abs() < 1e-4 && (hit.y - 1.5).abs() < 1e-4,
"scene-frame hit: {hit:?}"
);
let value = field.value_at(hit).expect("hit inside the box");
assert!((value - 1.0).abs() < 1e-5, "value {value}");
let raw_seg = (Vec3::new(1.5, 3.0, 1.5), Vec3::new(1.5, 0.0, 1.5));
assert!(field.pick_cut_plane(raw_seg).is_none());
}
#[test]
fn set_data_rejects_length_mismatch() {
let mut s = Scatter3D::new();
assert!(!s.set_data(&[0.0, 1.0], &[0.0], &[0.0, 1.0], &[0.0, 1.0]));
assert!(s.is_empty(), "rejected data must not be partially stored");
assert!(s.set_data(&[0.0, 1.0], &[2.0, 3.0], &[4.0, 5.0], &[6.0, 7.0]));
assert_eq!(s.len(), 2);
}
#[test]
fn append_to_colours_each_point_through_the_colormap() {
let cmap = Colormap::new(ColormapName::Viridis, 0.0, 4.0);
let s = Scatter3D::new()
.with_colormap(cmap.clone())
.with_marker(PointMarker::Square)
.with_size(8.0)
.with_data(
&[0.0, 1.0, 2.0],
&[0.0, 0.0, 0.0],
&[0.0, 0.0, 0.0],
&[0.0, 2.0, 4.0],
);
let mut g = Scene3dGeometry::new();
s.append_to(&mut g);
assert_eq!(g.points.len(), 3);
assert_eq!(g.points[1].pos, [1.0, 0.0, 0.0]);
for p in &g.points {
assert_eq!(p.size, 8.0);
assert_eq!(p.marker, PointMarker::Square.id());
}
let expect = |v: f64| {
let [r, gg, b, a] = cmap.color_at(v);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, gg, b, a)).to_array()
};
assert_eq!(g.points[0].color, expect(0.0));
assert_eq!(g.points[2].color, expect(4.0));
assert_ne!(g.points[0].color, g.points[2].color);
}
#[test]
fn autoscale_colormap_fits_value_range() {
let mut s =
Scatter3D::new().with_data(&[0.0, 1.0, 2.0], &[0.0; 3], &[0.0; 3], &[-5.0, 0.0, 10.0]);
let (vmin, vmax) = s.autoscale_colormap(AutoscaleMode::MinMax);
assert_eq!((vmin, vmax), (-5.0, 10.0));
assert_eq!(s.colormap().vmin, -5.0);
assert_eq!(s.colormap().vmax, 10.0);
}
#[test]
fn bounds_brackets_the_points() {
assert!(Scatter3D::new().bounds().is_none());
let s = Scatter3D::new().with_data(
&[-1.0, 2.0, 0.5],
&[3.0, -2.0, 1.0],
&[0.0, 4.0, -1.0],
&[0.0; 3],
);
let (min, max) = s.bounds().expect("non-empty bounds");
assert_eq!((min.x, min.y, min.z), (-1.0, -2.0, -1.0));
assert_eq!((max.x, max.y, max.z), (2.0, 3.0, 4.0));
}
#[test]
fn scatter2d_set_data_rejects_length_mismatch() {
let mut s = Scatter2D::new();
assert!(!s.set_data(&[0.0, 1.0], &[0.0], &[0.0, 1.0]));
assert!(s.is_empty(), "rejected data must not be partially stored");
assert!(s.set_data(&[0.0, 1.0], &[2.0, 3.0], &[4.0, 5.0]));
assert_eq!(s.len(), 2);
}
#[test]
fn scatter2d_points_mode_lies_on_z0_plane_or_lifts_to_value() {
let cmap = Colormap::new(ColormapName::Viridis, 0.0, 4.0);
let flat = Scatter2D::new()
.with_colormap(cmap.clone())
.with_marker(PointMarker::Square)
.with_size(8.0)
.with_data(&[0.0, 1.0, 2.0], &[0.0, 0.0, 0.0], &[0.0, 2.0, 4.0]);
let mut g = Scene3dGeometry::new();
flat.append_to(&mut g);
assert_eq!(g.points.len(), 3);
for p in &g.points {
assert_eq!(p.pos[2], 0.0, "flat scatter sits on z=0");
assert_eq!(p.size, 8.0);
assert_eq!(p.marker, PointMarker::Square.id());
}
let expect = |v: f64| {
let [r, gg, b, a] = cmap.color_at(v);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, gg, b, a)).to_array()
};
assert_eq!(g.points[0].color, expect(0.0));
assert_eq!(g.points[2].color, expect(4.0));
assert_ne!(g.points[0].color, g.points[2].color);
let hm = Scatter2D::new()
.with_data(&[0.0, 1.0, 2.0], &[0.0, 0.0, 0.0], &[0.0, 2.0, 4.0])
.with_height_map(true);
let mut g2 = Scene3dGeometry::new();
hm.append_to(&mut g2);
assert_eq!(g2.points[0].pos[2], 0.0);
assert_eq!(g2.points[1].pos[2], 2.0);
assert_eq!(g2.points[2].pos[2], 4.0);
}
#[test]
fn scatter2d_lines_mode_emits_unique_triangulation_edges() {
let s = Scatter2D::new()
.with_data(
&[0.0, 1.0, 0.0, 1.0],
&[0.0, 0.0, 1.0, 1.0],
&[0.0, 1.0, 2.0, 3.0],
)
.with_visualization(Scatter2DVisualization::Lines);
let mut g = Scene3dGeometry::new();
s.append_to(&mut g);
assert_eq!(g.lines.len(), 10);
for v in &g.lines {
assert_eq!(v.pos[2], 0.0);
}
assert_ne!(g.lines[0].color, g.lines[1].color);
assert!(g.points.is_empty());
assert!(g.meshes.is_empty());
}
#[test]
fn scatter2d_solid_mode_fills_triangles_coloured_by_value() {
let s = Scatter2D::new()
.with_data(
&[0.0, 1.0, 0.0, 1.0],
&[0.0, 0.0, 1.0, 1.0],
&[0.0, 1.0, 2.0, 3.0],
)
.with_visualization(Scatter2DVisualization::Solid);
let mut g = Scene3dGeometry::new();
s.append_to(&mut g);
assert_eq!(g.meshes.len(), 6);
for v in &g.meshes {
assert_eq!(v.pos[2], 0.0, "flat solid sits on z=0");
assert!(
(v.normal[2].abs() - 1.0).abs() < 1e-5,
"expected plane normal, got {:?}",
v.normal
);
}
assert!(g.points.is_empty());
assert!(g.lines.is_empty());
}
#[test]
fn scatter2d_degenerate_input_draws_nothing_in_triangulated_modes() {
let collinear =
Scatter2D::new().with_data(&[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0]);
let mut g = Scene3dGeometry::new();
collinear
.clone()
.with_visualization(Scatter2DVisualization::Lines)
.append_to(&mut g);
assert!(g.lines.is_empty());
let mut g2 = Scene3dGeometry::new();
collinear
.with_visualization(Scatter2DVisualization::Solid)
.append_to(&mut g2);
assert!(g2.meshes.is_empty());
}
#[test]
fn scatter2d_bounds_flat_collapses_z_height_map_spans_value() {
assert!(Scatter2D::new().bounds().is_none());
let flat = Scatter2D::new().with_data(&[-1.0, 2.0], &[3.0, -2.0], &[5.0, 10.0]);
let (min, max) = flat.bounds().expect("non-empty");
assert_eq!((min.x, min.y, min.z), (-1.0, -2.0, 0.0));
assert_eq!((max.x, max.y, max.z), (2.0, 3.0, 0.0));
let (min, max) = flat.with_height_map(true).bounds().expect("non-empty");
assert_eq!((min.z, max.z), (5.0, 10.0));
}
fn flat_tri() -> [[f32; 3]; 3] {
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
}
#[test]
fn mesh_triangles_mode_emits_one_triangle_with_flat_normal() {
let [a, b, c] = flat_tri();
let mut m = Mesh3D::new();
assert!(m.set_data(
&[a, b, c],
MeshColor::Uniform(Color32::from_rgb(255, 0, 0)),
None,
MeshDrawMode::Triangles,
None,
));
let mut g = Scene3dGeometry::new();
m.append_to(&mut g);
assert_eq!(g.meshes.len(), 3);
for v in &g.meshes {
assert_eq!(v.normal, [0.0, 0.0, 1.0]);
assert_eq!(
v.color,
egui::Rgba::from(Color32::from_rgb(255, 0, 0)).to_array()
);
}
assert_eq!(g.meshes[1].pos, b);
}
#[test]
fn mesh_set_data_rejects_inconsistent_attributes() {
let [a, b, c] = flat_tri();
let mut m = Mesh3D::new();
assert!(!m.set_data(
&[a, b, c],
MeshColor::PerVertex(vec![Color32::RED, Color32::GREEN]),
None,
MeshDrawMode::Triangles,
None,
));
assert!(!m.set_data(
&[a, b, c],
MeshColor::Uniform(Color32::WHITE),
Some(&[[0.0, 0.0, 1.0]]),
MeshDrawMode::Triangles,
None,
));
assert!(!m.set_data(
&[a, b, c],
MeshColor::Uniform(Color32::WHITE),
None,
MeshDrawMode::Triangles,
Some(&[0, 1, 3]),
));
assert!(m.is_empty(), "rejected data must not be partially stored");
assert!(m.set_data(
&[a, b, c],
MeshColor::PerVertex(vec![Color32::RED, Color32::GREEN, Color32::BLUE]),
Some(&[[0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]]),
MeshDrawMode::Triangles,
Some(&[0, 1, 2]),
));
assert_eq!(m.len(), 3);
}
#[test]
fn mesh_strip_and_fan_expand_to_triangle_lists() {
let p = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let mut strip = Scene3dGeometry::new();
Mesh3D::new()
.with_data(
&p,
MeshColor::Uniform(Color32::WHITE),
None,
MeshDrawMode::TriangleStrip,
None,
)
.append_to(&mut strip);
assert_eq!(strip.meshes.len(), 6);
assert_eq!(strip.meshes[3].pos, p[1]);
assert_eq!(strip.meshes[4].pos, p[2]);
assert_eq!(strip.meshes[5].pos, p[3]);
let mut fan = Scene3dGeometry::new();
Mesh3D::new()
.with_data(
&p,
MeshColor::Uniform(Color32::WHITE),
None,
MeshDrawMode::Fan,
None,
)
.append_to(&mut fan);
assert_eq!(fan.meshes.len(), 6);
assert_eq!(fan.meshes[3].pos, p[0]); assert_eq!(fan.meshes[4].pos, p[2]);
assert_eq!(fan.meshes[5].pos, p[3]);
}
#[test]
fn mesh_indices_unindex_before_expansion() {
let p = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let mut g = Scene3dGeometry::new();
Mesh3D::new()
.with_data(
&p,
MeshColor::Uniform(Color32::WHITE),
None,
MeshDrawMode::Triangles,
Some(&[0, 1, 0]),
)
.append_to(&mut g);
assert_eq!(g.meshes.len(), 3);
assert_eq!(g.meshes[0].pos, p[0]);
assert_eq!(g.meshes[1].pos, p[1]);
assert_eq!(g.meshes[2].pos, p[0]);
}
#[test]
fn colormap_mesh_colours_vertices_through_the_colormap() {
let [a, b, c] = flat_tri();
let cmap = Colormap::new(ColormapName::Viridis, 0.0, 2.0);
let mut m = ColormapMesh3D::new().with_colormap(cmap.clone());
assert!(m.set_data(
&[a, b, c],
&[0.0, 1.0, 2.0],
None,
MeshDrawMode::Triangles,
None
));
let mut g = Scene3dGeometry::new();
m.append_to(&mut g);
assert_eq!(g.meshes.len(), 3);
let expect = |v: f64| {
let [r, gg, bb, al] = cmap.color_at(v);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, gg, bb, al)).to_array()
};
assert_eq!(g.meshes[0].color, expect(0.0));
assert_eq!(g.meshes[2].color, expect(2.0));
assert_ne!(g.meshes[0].color, g.meshes[2].color);
assert_eq!(g.meshes[0].normal, [0.0, 0.0, 1.0]);
}
#[test]
fn colormap_mesh_rejects_value_length_mismatch_and_autoscales() {
let [a, b, c] = flat_tri();
let mut m = ColormapMesh3D::new();
assert!(!m.set_data(&[a, b, c], &[0.0, 1.0], None, MeshDrawMode::Triangles, None));
assert!(m.is_empty());
assert!(m.set_data(
&[a, b, c],
&[-3.0, 0.0, 7.0],
None,
MeshDrawMode::Triangles,
None
));
let (vmin, vmax) = m.autoscale_colormap(AutoscaleMode::MinMax);
assert_eq!((vmin, vmax), (-3.0, 7.0));
}
fn bounds_close(got: (Vec3, Vec3), min: [f32; 3], max: [f32; 3]) {
let eps = 1e-4;
let (g_min, g_max) = got;
for (a, b) in [(g_min.x, min[0]), (g_min.y, min[1]), (g_min.z, min[2])] {
assert!((a - b).abs() < eps, "min {a} vs {b}");
}
for (a, b) in [(g_max.x, max[0]), (g_max.y, max[1]), (g_max.z, max[2])] {
assert!((a - b).abs() < eps, "max {a} vs {b}");
}
}
#[test]
fn box3d_default_is_a_centred_unit_cube() {
let b = Box3D::new();
let mut g = Scene3dGeometry::new();
b.append_to(&mut g);
assert_eq!(g.meshes.len(), 48);
bounds_close(
b.bounds().expect("box bounds"),
[-0.5, -0.5, -0.5],
[0.5, 0.5, 0.5],
);
assert_eq!(b.size(), [1.0, 1.0, 1.0]);
}
#[test]
fn box3d_rejects_bad_colour_count_and_tiles_per_position() {
let mut b = Box3D::new();
assert!(!b.set_data(
[1.0, 1.0, 1.0],
&[Color32::RED, Color32::GREEN, Color32::BLUE],
&[[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
(0.0, [0.0, 0.0, 0.0]),
));
assert!(b.set_data(
[1.0, 1.0, 1.0],
&[Color32::RED],
&[[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]],
(0.0, [0.0, 0.0, 0.0]),
));
let mut g = Scene3dGeometry::new();
b.append_to(&mut g);
assert_eq!(g.meshes.len(), 96);
bounds_close(
b.bounds().expect("bounds"),
[-0.5, -0.5, -0.5],
[3.5, 0.5, 0.5],
);
}
#[test]
fn cylinder3d_default_has_radial_side_normals() {
let c = Cylinder3D::new();
let mut g = Scene3dGeometry::new();
c.append_to(&mut g);
assert_eq!(g.meshes.len(), 240);
bounds_close(
c.bounds().expect("cyl bounds"),
[-1.0, -1.0, -0.5],
[1.0, 1.0, 0.5],
);
assert_eq!(g.meshes[3].normal, [1.0, 0.0, 0.0]);
}
#[test]
fn hexagon3d_default_spans_its_hexagonal_footprint() {
let h = Hexagon3D::new();
let mut g = Scene3dGeometry::new();
h.append_to(&mut g);
assert_eq!(g.meshes.len(), 72);
let s60 = (std::f32::consts::TAU / 6.0).sin();
bounds_close(
h.bounds().expect("hex bounds"),
[-1.0, -s60, -0.5],
[1.0, s60, 0.5],
);
assert_eq!((h.radius(), h.height()), (1.0, 1.0));
}
#[test]
fn cylinder3d_face_count_controls_resolution() {
let mut c = Cylinder3D::new();
assert!(c.set_data(
2.0,
4.0,
&[Color32::WHITE],
8,
&[[0.0, 0.0, 0.0]],
(0.0, [0.0, 0.0, 0.0]),
));
let mut g = Scene3dGeometry::new();
c.append_to(&mut g);
assert_eq!(g.meshes.len(), 8 * 12);
bounds_close(
c.bounds().expect("bounds"),
[-2.0, -2.0, -2.0],
[2.0, 2.0, 2.0],
);
}
#[test]
fn image_data3d_builds_a_colormapped_layer() {
let cmap = Colormap::new(ColormapName::Viridis, 0.0, 3.0);
let mut img = ImageData3D::new().with_colormap(cmap.clone());
assert!(img.set_data(&[0.0, 1.0, 2.0, 3.0], 2, 2));
assert_eq!(img.dimensions(), (2, 2));
let mut g = Scene3dGeometry::new();
img.append_to(&mut g);
assert_eq!(g.images.len(), 1);
let layer = &g.images[0];
assert_eq!((layer.width, layer.height), (2, 2));
assert_eq!(layer.pixels.len(), 2 * 2 * 4);
let expect = |v: f64| {
let [r, gg, b, a] = cmap.color_at(v);
premul_linear_rgba8(Color32::from_rgba_unmultiplied(r, gg, b, a))
};
assert_eq!(&layer.pixels[0..4], &expect(0.0)); assert_eq!(&layer.pixels[12..16], &expect(3.0)); assert_ne!(&layer.pixels[0..4], &layer.pixels[12..16]);
}
#[test]
fn image_data3d_rejects_size_mismatch_and_bounds_follow_origin_scale() {
let mut img = ImageData3D::new();
assert!(!img.set_data(&[0.0, 1.0, 2.0], 2, 2));
assert!(img.is_empty());
assert!(img.bounds().is_none());
let img = ImageData3D::new()
.with_data(&[0.0; 6], 3, 2)
.with_origin([10.0, 20.0, -1.0])
.with_scale([2.0, 5.0]);
let (min, max) = img.bounds().expect("bounds");
assert_eq!((min.x, min.y, min.z), (10.0, 20.0, -1.0));
assert_eq!(
(max.x, max.y, max.z),
(10.0 + 3.0 * 2.0, 20.0 + 2.0 * 5.0, -1.0)
);
}
#[test]
fn image_rgba3d_passes_pixels_through_premultiplied() {
let cols = [Color32::RED, Color32::GREEN, Color32::BLUE, Color32::WHITE];
let mut img = ImageRgba3D::new();
assert!(img.set_data(&cols, 2, 2));
let mut g = Scene3dGeometry::new();
img.append_to(&mut g);
assert_eq!(g.images.len(), 1);
let layer = &g.images[0];
assert_eq!((layer.width, layer.height), (2, 2));
for (i, &c) in cols.iter().enumerate() {
assert_eq!(&layer.pixels[i * 4..i * 4 + 4], &premul_linear_rgba8(c));
}
}
#[test]
fn image_rgba3d_rejects_size_mismatch() {
let mut img = ImageRgba3D::new();
assert!(!img.set_data(&[Color32::RED, Color32::GREEN], 2, 2));
assert!(img.is_empty());
assert!(img.set_data(&[Color32::RED; 4], 2, 2));
assert_eq!(img.dimensions(), (2, 2));
}
#[test]
fn height_map_data_emits_one_square_point_per_pixel() {
let cmap = Colormap::new(ColormapName::Viridis, 0.0, 3.0);
let heights = [0.0_f32, 1.0, 2.0, 3.0]; let mut hm = HeightMapData::new().with_colormap(cmap.clone());
assert!(hm.set_data(&heights, 2, 2));
assert!(hm.set_colormapped_data(&[0.0, 1.0, 2.0, 3.0], 2, 2));
let mut g = Scene3dGeometry::new();
hm.append_to(&mut g);
assert_eq!(g.points.len(), 4);
for p in &g.points {
assert_eq!(p.size, 1.0);
assert_eq!(p.marker, PointMarker::Square.id());
}
let p11 = &g.points[3];
assert_eq!(p11.pos, [1.0, 1.0, 3.0]);
let expect = |v: f64| {
let [r, gg, b, a] = cmap.color_at(v);
egui::Rgba::from(Color32::from_rgba_unmultiplied(r, gg, b, a)).to_array()
};
assert_eq!(g.points[0].color, expect(0.0));
assert_eq!(p11.color, expect(3.0));
}
#[test]
fn height_map_data_empty_without_both_fields_and_bounds_from_heights() {
let mut hm = HeightMapData::new();
assert!(hm.set_data(&[0.0, 5.0, 2.0, 1.0], 2, 2));
assert!(hm.is_empty());
let mut g = Scene3dGeometry::new();
hm.append_to(&mut g);
assert!(g.points.is_empty());
let (min, max) = hm.bounds().expect("bounds from heights");
assert_eq!((min.x, min.y, min.z), (0.0, 0.0, 0.0)); assert_eq!((max.x, max.y, max.z), (1.0, 1.0, 5.0)); }
#[test]
fn height_map_data_resamples_columns_by_width() {
let cmap = Colormap::new(ColormapName::Viridis, 0.0, 1.0);
let heights = [0.0_f32; 8]; let values = [0.0, 1.0, 0.0, 1.0];
let hm = HeightMapData::new()
.with_colormap(cmap.clone())
.with_data(&heights, 4, 2)
.with_colormapped_data(&values, 2, 2);
let mut g = Scene3dGeometry::new();
hm.append_to(&mut g);
assert_eq!(g.points.len(), 8);
let c0 = egui::Rgba::from({
let [r, gg, b, a] = cmap.color_at(0.0);
Color32::from_rgba_unmultiplied(r, gg, b, a)
})
.to_array();
assert_eq!(g.points[0].color, c0); assert_eq!(g.points[1].color, c0); assert_ne!(g.points[2].color, c0); }
#[test]
fn height_map_rgba_colours_points_directly() {
let heights = [0.0_f32, 1.0, 2.0, 3.0];
let cols = [Color32::RED, Color32::GREEN, Color32::BLUE, Color32::WHITE];
let mut hm = HeightMapRGBA::new();
assert!(hm.set_data(&heights, 2, 2));
assert!(hm.set_color_data(&cols, 2, 2));
let mut g = Scene3dGeometry::new();
hm.append_to(&mut g);
assert_eq!(g.points.len(), 4);
for (i, &c) in cols.iter().enumerate() {
assert_eq!(g.points[i].color, egui::Rgba::from(c).to_array());
assert_eq!(g.points[i].marker, PointMarker::Square.id());
}
assert_eq!(g.points[3].pos, [1.0, 1.0, 3.0]);
}
fn blob_field() -> (Vec<f32>, usize, usize, usize) {
let (d, h, w) = (5usize, 5usize, 5usize);
let mut data = vec![0.0f32; d * h * w];
for z in 1..4 {
for y in 1..4 {
for x in 1..4 {
data[(z * h + y) * w + x] = 1.0;
}
}
}
(data, d, h, w)
}
#[test]
fn scalar_field_rejects_bad_shape() {
let mut sf = ScalarField3D::new();
assert!(!sf.set_data(&[0.0; 7], 2, 2, 2));
assert!(!sf.set_data(&[0.0; 2], 1, 2, 1));
assert!(sf.is_empty());
assert!(sf.set_data(&[0.0; 8], 2, 2, 2));
assert_eq!(sf.dimensions(), (2, 2, 2));
}
#[test]
fn scalar_field_data_range_and_bounds() {
let (data, d, h, w) = blob_field();
let sf = ScalarField3D::new().with_data(&data, d, h, w);
let (min, min_pos, max) = sf.data_range().expect("range");
assert_eq!(min, 0.0);
assert_eq!(max, 1.0);
assert_eq!(min_pos, 1.0, "smallest positive sample is 1.0");
let (lo, hi) = sf.bounds().expect("bounds");
assert_eq!(lo.to_array(), [0.0, 0.0, 0.0]);
assert_eq!(hi.to_array(), [5.0, 5.0, 5.0]);
}
#[test]
fn data_range_min_positive_nan_when_no_positive() {
let sf = ScalarField3D::new().with_data(&[-1.0; 8], 2, 2, 2);
let (min, min_pos, max) = sf.data_range().unwrap();
assert_eq!(min, -1.0);
assert_eq!(max, -1.0);
assert!(min_pos.is_nan(), "no positive sample → NaN min positive");
}
#[test]
fn add_remove_clear_isosurfaces() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
let i0 = sf.add_isosurface(0.5, Color32::RED);
let i1 = sf.add_isosurface(0.25, DEFAULT_ISOSURFACE_COLOR);
assert_eq!((i0, i1), (0, 1));
assert_eq!(sf.isosurfaces().len(), 2);
assert_eq!(sf.isosurfaces()[0].level(), 0.5);
assert_eq!(sf.isosurfaces()[1].color(), DEFAULT_ISOSURFACE_COLOR);
sf.isosurface_mut(0).unwrap().set_level(0.75);
assert_eq!(sf.isosurfaces()[0].level(), 0.75);
assert!(sf.remove_isosurface(0));
assert!(!sf.remove_isosurface(5));
assert_eq!(sf.isosurfaces().len(), 1);
sf.clear_isosurfaces();
assert!(sf.isosurfaces().is_empty());
}
#[test]
fn auto_level_resolves_on_data_and_on_add() {
let (data, d, h, w) = blob_field();
let expect = mean_plus_std(&data);
assert!(expect.is_finite() && expect > 0.0);
let mut sf = ScalarField3D::new();
sf.add_auto_isosurface(mean_plus_std, DEFAULT_ISOSURFACE_COLOR);
assert!(sf.isosurfaces()[0].level().is_nan());
assert!(sf.set_data(&data, d, h, w));
assert!((sf.isosurfaces()[0].level() - expect).abs() < 1e-6);
let mut sf2 = ScalarField3D::new().with_data(&data, d, h, w);
sf2.add_auto_isosurface(mean_plus_std, DEFAULT_ISOSURFACE_COLOR);
assert!((sf2.isosurfaces()[0].level() - expect).abs() < 1e-6);
assert!(sf2.isosurfaces()[0].is_auto_level());
}
#[test]
fn mean_plus_std_ignores_non_finite_and_empty() {
assert!(mean_plus_std(&[]).is_nan());
assert!(mean_plus_std(&[f32::NAN, f32::INFINITY]).is_nan());
assert!((mean_plus_std(&[2.0, 2.0, 2.0]) - 2.0).abs() < 1e-6);
}
#[test]
fn isosurface_emits_swapped_offset_triangles() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
sf.add_isosurface(0.5, DEFAULT_ISOSURFACE_COLOR);
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
assert!(!g.meshes.is_empty(), "isosurface produced triangles");
assert_eq!(g.meshes.len() % 3, 0, "triangles");
let gold = egui::Rgba::from(DEFAULT_ISOSURFACE_COLOR).to_array();
for v in &g.meshes {
assert_eq!(v.color, gold);
for k in 0..3 {
assert!(
v.pos[k] >= 0.0 && v.pos[k] <= 5.0,
"inside box: {:?}",
v.pos
);
}
let n = v.normal;
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-4, "unit normal, got {len}");
}
let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
for v in &g.meshes {
for k in 0..3 {
lo = lo.min(v.pos[k]);
hi = hi.max(v.pos[k]);
}
}
assert!(
lo >= 1.0 - 1e-4 && hi <= 4.0 + 1e-4,
"surface in [1,4]: {lo}..{hi}"
);
}
#[test]
fn hidden_isosurface_emits_no_geometry_but_keeps_level_and_colour() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
let idx = sf.add_isosurface(0.5, DEFAULT_ISOSURFACE_COLOR);
let mut visible = Scene3dGeometry::new();
sf.append_to(&mut visible);
assert!(
!visible.meshes.is_empty(),
"visible surface emits triangles"
);
sf.isosurface_mut(idx).unwrap().set_visible(false);
assert!(!sf.isosurfaces()[idx].is_visible());
assert_eq!(sf.isosurfaces()[idx].level(), 0.5);
assert_eq!(sf.isosurfaces()[idx].color(), DEFAULT_ISOSURFACE_COLOR);
let mut hidden = Scene3dGeometry::new();
sf.append_to(&mut hidden);
assert!(hidden.meshes.is_empty(), "hidden surface emits no geometry");
}
#[test]
fn non_finite_level_emits_nothing() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
sf.add_isosurface(f32::NAN, DEFAULT_ISOSURFACE_COLOR);
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
assert!(g.meshes.is_empty(), "NaN level → no triangles");
}
#[test]
fn cut_plane_hidden_by_default_emits_nothing() {
let (data, d, h, w) = blob_field();
let sf = ScalarField3D::new().with_data(&data, d, h, w);
assert!(!sf.cut_plane().is_visible(), "cut plane hidden by default");
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
assert!(g.textured_meshes.is_empty(), "hidden cut plane → no mesh");
}
#[test]
fn cut_plane_config_setters() {
let mut sf = ScalarField3D::new();
let cp = sf.cut_plane_mut();
cp.set_visible(true);
cp.set_point(Vec3::new(1.0, 2.0, 3.0));
cp.set_normal(Vec3::new(0.0, 0.0, 2.0)); cp.set_interpolation(ImageInterpolation::Nearest);
cp.set_resolution(0); assert!(sf.cut_plane().is_visible());
assert_eq!(sf.cut_plane().plane().point().to_array(), [1.0, 2.0, 3.0]);
assert_eq!(sf.cut_plane().plane().normal().to_array(), [0.0, 0.0, 1.0]);
assert_eq!(sf.cut_plane().interpolation(), ImageInterpolation::Nearest);
assert_eq!(sf.cut_plane().resolution(), 1);
}
#[test]
fn plane_basis_is_orthonormal() {
for n in [
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(1.0, 2.0, 3.0).normalized(),
] {
let (e1, e2) = plane_basis(n);
assert!((e1.length() - 1.0).abs() < 1e-5, "e1 unit");
assert!((e2.length() - 1.0).abs() < 1e-5, "e2 unit");
assert!(e1.dot(n).abs() < 1e-5, "e1 ⟂ n");
assert!(e2.dot(n).abs() < 1e-5, "e2 ⟂ n");
assert!(e1.dot(e2).abs() < 1e-5, "e1 ⟂ e2");
}
}
#[test]
fn sample_field_value_nearest_and_linear() {
let data: Vec<f32> = (0..8).map(|i| i as f32).collect();
let (d, h, w) = (2usize, 2usize, 2usize);
let v = sample_field_value(
&data,
d,
h,
w,
Vec3::new(1.5, 0.5, 1.5),
ImageInterpolation::Nearest,
);
assert_eq!(v, data[h * w + 1]); let v = sample_field_value(
&data,
d,
h,
w,
Vec3::new(1.0, 0.5, 0.5),
ImageInterpolation::Linear,
);
assert!((v - 0.5).abs() < 1e-5, "midpoint trilinear, got {v}");
let v = sample_field_value(
&data,
d,
h,
w,
Vec3::new(99.0, 99.0, 99.0),
ImageInterpolation::Nearest,
);
assert_eq!(v, data[7], "clamps to far-corner voxel");
}
#[test]
fn visible_axis_cut_plane_emits_textured_mesh() {
let (data, d, h, w) = blob_field(); let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
sf.autoscale_cut_plane_colormap(AutoscaleMode::MinMax);
{
let cp = sf.cut_plane_mut();
cp.set_normal(Vec3::new(0.0, 0.0, 1.0));
cp.set_point(Vec3::new(2.5, 2.5, 2.5));
cp.set_resolution(16);
cp.set_visible(true);
}
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
assert_eq!(g.textured_meshes.len(), 1, "one cut-plane mesh");
let m = &g.textured_meshes[0];
assert_eq!(m.vertices.len(), 6);
assert_eq!(m.uvs.len(), 6);
assert_eq!((m.width, m.height), (16, 16));
assert_eq!(m.pixels.len(), 16 * 16 * 4, "res×res premultiplied RGBA8");
let (mut lo, mut hi) = ([f32::INFINITY; 3], [f32::NEG_INFINITY; 3]);
for v in &m.vertices {
assert!((v[2] - 2.5).abs() < 1e-4, "on the z=2.5 plane");
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
hi[k] = hi[k].max(v[k]);
}
}
assert_eq!([lo[0], lo[1]], [0.0, 0.0]);
assert_eq!([hi[0], hi[1]], [5.0, 5.0]);
}
#[test]
fn cut_plane_discards_below_min_texels_when_display_off() {
let (data, d, h, w) = blob_field(); let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
{
let cp = sf.cut_plane_mut();
cp.set_colormap(Colormap::new(ColormapName::Gray, 0.5, 1.0));
cp.set_normal(Vec3::new(0.0, 0.0, 1.0));
cp.set_point(Vec3::new(2.5, 2.5, 2.5)); cp.set_resolution(16);
cp.set_visible(true);
}
let transparent = |sf: &ScalarField3D| -> usize {
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
g.textured_meshes[0]
.pixels
.chunks_exact(4)
.filter(|c| c[3] == 0)
.count()
};
assert_eq!(transparent(&sf), 0, "default draws below-min texels opaque");
sf.cut_plane_mut().set_display_values_below_min(false);
let holes = transparent(&sf);
assert!(holes > 0, "below-min discard punches transparent texels");
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
let opaque = g.textured_meshes[0]
.pixels
.chunks_exact(4)
.filter(|c| c[3] == 255)
.count();
assert!(opaque > 0, "above-min block texels stay opaque");
}
#[test]
fn cut_plane_display_values_below_min_defaults_true() {
assert!(CutPlane::new().display_values_below_min());
}
#[test]
fn autoscale_cut_plane_colormap_fits_data_range() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
let (vmin, vmax) = sf.autoscale_cut_plane_colormap(AutoscaleMode::MinMax);
assert_eq!((vmin, vmax), (0.0, 1.0));
assert_eq!(sf.cut_plane().colormap().vmin, 0.0);
assert_eq!(sf.cut_plane().colormap().vmax, 1.0);
}
#[test]
fn cut_plane_not_slicing_the_volume_emits_nothing() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
{
let cp = sf.cut_plane_mut();
cp.set_normal(Vec3::new(0.0, 0.0, 1.0));
cp.set_point(Vec3::new(2.5, 2.5, 100.0)); cp.set_visible(true);
}
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
assert!(
g.textured_meshes.is_empty(),
"plane misses the volume → no mesh"
);
assert!(g.lines.is_empty(), "plane misses the volume → no stroke");
}
#[test]
fn visible_cut_plane_emits_white_contour_stroke() {
let (data, d, h, w) = blob_field(); let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
{
let cp = sf.cut_plane_mut();
cp.set_normal(Vec3::new(0.0, 0.0, 1.0));
cp.set_point(Vec3::new(2.5, 2.5, 2.5));
cp.set_visible(true);
}
assert!(sf.cut_plane().is_stroke_visible(), "stroke on by default");
assert_eq!(sf.cut_plane().stroke_color(), Color32::WHITE);
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
assert_eq!(g.lines.len(), 8, "4 closed-loop segments, 2 verts each");
let white = egui::Rgba::from(Color32::WHITE).to_array();
for v in &g.lines {
assert_eq!(v.color, white, "stroke is white by default");
assert!((v.pos[2] - 2.5).abs() < 1e-4, "stroke lies on the plane");
}
let mut counts: Vec<([i32; 3], usize)> = Vec::new();
for v in &g.lines {
let key = [0, 1, 2].map(|k| (v.pos[k] * 1024.0).round() as i32);
match counts.iter_mut().find(|(p, _)| *p == key) {
Some((_, n)) => *n += 1,
None => counts.push((key, 1)),
}
}
assert_eq!(counts.len(), 4, "four distinct corners");
assert!(
counts.iter().all(|&(_, n)| n == 2),
"each corner shared by two segments (closed loop): {counts:?}"
);
}
#[test]
fn cut_plane_stroke_color_and_visibility_api() {
let (data, d, h, w) = blob_field();
let mut sf = ScalarField3D::new().with_data(&data, d, h, w);
{
let cp = sf.cut_plane_mut();
cp.set_normal(Vec3::new(0.0, 0.0, 1.0));
cp.set_point(Vec3::new(2.5, 2.5, 2.5));
cp.set_visible(true);
cp.set_stroke_color(Color32::RED); }
let mut g = Scene3dGeometry::new();
sf.append_to(&mut g);
let red = egui::Rgba::from(Color32::RED).to_array();
assert!(!g.lines.is_empty());
assert!(g.lines.iter().all(|v| v.color == red));
sf.cut_plane_mut().set_stroke_visible(false);
let mut g2 = Scene3dGeometry::new();
sf.append_to(&mut g2);
assert!(g2.lines.is_empty(), "stroke hidden → no loop");
assert_eq!(g2.textured_meshes.len(), 1, "slice still drawn");
sf.cut_plane_mut().set_stroke_visible(true);
sf.cut_plane_mut().set_visible(false);
let mut g3 = Scene3dGeometry::new();
sf.append_to(&mut g3);
assert!(g3.lines.is_empty() && g3.textured_meshes.is_empty());
}
fn complex_field() -> (Vec<f32>, Vec<f32>, usize, usize, usize) {
let (d, h, w) = (2usize, 2usize, 2usize);
let mut re = vec![0.0f32; d * h * w];
let mut im = vec![0.0f32; d * h * w];
re[0] = 3.0;
im[0] = 4.0;
(re, im, d, h, w)
}
#[test]
fn complex_mode_projections() {
assert_eq!(ComplexMode::Absolute.to_scalar(3.0, 4.0), 5.0);
assert_eq!(ComplexMode::SquareAmplitude.to_scalar(3.0, 4.0), 25.0);
assert_eq!(ComplexMode::Real.to_scalar(3.0, 4.0), 3.0);
assert_eq!(ComplexMode::Imaginary.to_scalar(3.0, 4.0), 4.0);
assert!((ComplexMode::Phase.to_scalar(3.0, 4.0) - 4.0f32.atan2(3.0)).abs() < 1e-6);
assert_eq!(ComplexMode::AmplitudePhase.to_scalar(3.0, 4.0), 0.0);
}
#[test]
fn complex_field_rejects_bad_shape() {
let mut cf = ComplexField3D::new();
assert!(!cf.set_data(&[0.0; 8], &[0.0; 7], 2, 2, 2));
assert!(!cf.set_data(&[0.0; 7], &[0.0; 7], 2, 2, 2));
assert!(!cf.set_data(&[0.0; 2], &[0.0; 2], 1, 2, 1));
assert!(cf.is_empty());
assert!(cf.set_data(&[0.0; 8], &[0.0; 8], 2, 2, 2));
assert_eq!(cf.dimensions(), (2, 2, 2));
}
#[test]
fn complex_field_projects_into_inner_field_per_mode() {
let (re, im, d, h, w) = complex_field();
let cf = ComplexField3D::new().with_data(&re, &im, d, h, w);
assert_eq!(cf.complex_mode(), ComplexMode::Absolute);
assert_eq!(cf.field().data()[0], 5.0);
assert_eq!(
cf.data_range_for(ComplexMode::Absolute),
Some((0.0, 5.0, 5.0))
);
assert_eq!(
cf.data_range_for(ComplexMode::SquareAmplitude),
Some((0.0, 25.0, 25.0))
);
assert_eq!(cf.projected_data(ComplexMode::Real).unwrap()[0], 3.0);
assert_eq!(cf.projected_data(ComplexMode::Imaginary).unwrap()[0], 4.0);
}
#[test]
fn set_complex_mode_reprojects_and_clears_isosurfaces() {
let (re, im, d, h, w) = complex_field();
let mut cf = ComplexField3D::new().with_data(&re, &im, d, h, w);
cf.field_mut().add_isosurface(2.0, DEFAULT_ISOSURFACE_COLOR);
assert_eq!(cf.field().isosurfaces().len(), 1);
assert_eq!(cf.field().data()[0], 5.0);
cf.set_complex_mode(ComplexMode::SquareAmplitude);
assert_eq!(cf.complex_mode(), ComplexMode::SquareAmplitude);
assert_eq!(cf.field().data()[0], 25.0);
assert!(
cf.field().isosurfaces().is_empty(),
"mode change clears iso-surfaces (silx setComplexMode)"
);
cf.field_mut()
.add_isosurface(10.0, DEFAULT_ISOSURFACE_COLOR);
cf.set_complex_mode(ComplexMode::SquareAmplitude);
assert_eq!(
cf.field().isosurfaces().len(),
1,
"unchanged mode is a no-op"
);
}
#[test]
fn complex_field_cut_plane_persists_across_mode_change() {
let (re, im, d, h, w) = complex_field();
let mut cf = ComplexField3D::new().with_data(&re, &im, d, h, w);
cf.field_mut().cut_plane_mut().set_visible(true);
cf.set_complex_mode(ComplexMode::Phase);
assert!(
cf.field().cut_plane().is_visible(),
"the cut plane survives a mode change (only iso-surfaces are cleared)"
);
}
fn ramp_complex_3() -> (Vec<f32>, Vec<f32>) {
let (d, h, w) = (3usize, 3usize, 3usize);
let mut re = vec![0.0f32; d * h * w];
let mut im = vec![0.0f32; d * h * w];
for z in 0..d {
for y in 0..h {
for x in 0..w {
re[(z * h + y) * w + x] = z as f32;
im[(z * h + y) * w + x] = x as f32;
}
}
}
(re, im)
}
#[test]
fn colormapped_iso_colormap_tracks_its_own_colour_mode() {
let (re, im) = ramp_complex_3();
let mut cf = ComplexField3D::new().with_data(&re, &im, 3, 3, 3);
let i = cf.add_colormapped_isosurface(ComplexIsosurface::new(
1.5,
ComplexMode::Real,
Colormap::autoscale(ColormapName::Viridis),
Color32::from_rgba_unmultiplied(0, 0, 0, 128),
));
let cm = cf.colormapped_isosurfaces()[i].colormap();
assert_eq!((cm.vmin, cm.vmax), (0.0, 2.0));
}
#[test]
fn colormapped_iso_colormap_re_resolves_on_data_change() {
let mut cf = ComplexField3D::new();
cf.add_colormapped_isosurface(ComplexIsosurface::new(
1.5,
ComplexMode::Real,
Colormap::autoscale(ColormapName::Viridis),
Color32::from_rgba_unmultiplied(0, 0, 0, 128),
));
let (re, im) = ramp_complex_3();
cf.set_data(&re, &im, 3, 3, 3);
assert_eq!(
{
let cm = cf.colormapped_isosurfaces()[0].colormap();
(cm.vmin, cm.vmax)
},
(0.0, 2.0)
);
let re2: Vec<f32> = re.iter().map(|&v| v * 2.0).collect();
cf.set_data(&re2, &im, 3, 3, 3);
let cm = cf.colormapped_isosurfaces()[0].colormap();
assert_eq!((cm.vmin, cm.vmax), (0.0, 4.0));
}
#[test]
fn colormapped_iso_pinned_colormap_survives_data() {
let (re, im) = ramp_complex_3();
let mut cf = ComplexField3D::new().with_data(&re, &im, 3, 3, 3);
cf.add_colormapped_isosurface(ComplexIsosurface::new(
1.5,
ComplexMode::Real,
Colormap::new(ColormapName::Viridis, -10.0, 10.0),
Color32::from_rgba_unmultiplied(0, 0, 0, 128),
));
let cm = cf.colormapped_isosurfaces()[0].colormap();
assert_eq!((cm.vmin, cm.vmax), (-10.0, 10.0));
}
#[test]
fn set_complex_mode_clears_colormapped_isosurfaces() {
let (re, im) = ramp_complex_3();
let mut cf = ComplexField3D::new().with_data(&re, &im, 3, 3, 3);
cf.add_colormapped_isosurface(ComplexIsosurface::new(
1.5,
ComplexMode::Real,
Colormap::autoscale(ColormapName::Viridis),
Color32::from_rgba_unmultiplied(0, 0, 0, 128),
));
assert_eq!(cf.colormapped_isosurfaces().len(), 1);
cf.set_complex_mode(ComplexMode::Real);
assert!(cf.colormapped_isosurfaces().is_empty());
}
#[test]
fn cut_plane_mode_defaults_to_none_and_round_trips() {
let mut cf = ComplexField3D::new();
assert_eq!(cf.cut_plane_mode(), None);
cf.set_cut_plane_mode(Some(ComplexMode::Phase));
assert_eq!(cf.cut_plane_mode(), Some(ComplexMode::Phase));
cf.set_cut_plane_mode(None);
assert_eq!(cf.cut_plane_mode(), None);
}
#[test]
fn colormapped_iso_emits_a_lit_mesh_coloured_by_its_mode() {
let (re, im) = ramp_complex_3();
let mut cf = ComplexField3D::new().with_data(&re, &im, 3, 3, 3);
cf.add_colormapped_isosurface(ComplexIsosurface::new(
1.5,
ComplexMode::Imaginary,
Colormap::autoscale(ColormapName::Viridis),
Color32::from_rgba_unmultiplied(0, 0, 0, 128),
));
let mut g = Scene3dGeometry::new();
cf.append_to(&mut g);
assert!(
!g.meshes.is_empty(),
"colormapped iso-surface emits lit mesh triangles"
);
let colours: std::collections::HashSet<[u32; 4]> =
g.meshes.iter().map(|v| v.color.map(f32::to_bits)).collect();
assert!(
colours.len() > 1,
"a colormapped surface is not a single solid colour"
);
}
#[test]
fn hidden_colormapped_isosurface_emits_no_geometry() {
let (re, im) = ramp_complex_3();
let mut cf = ComplexField3D::new().with_data(&re, &im, 3, 3, 3);
let idx = cf.add_colormapped_isosurface(ComplexIsosurface::new(
1.5,
ComplexMode::Imaginary,
Colormap::autoscale(ColormapName::Viridis),
Color32::from_rgba_unmultiplied(0, 0, 0, 128),
));
let mut visible = Scene3dGeometry::new();
cf.append_to(&mut visible);
assert!(
!visible.meshes.is_empty(),
"visible surface emits triangles"
);
cf.colormapped_isosurface_mut(idx)
.unwrap()
.set_visible(false);
assert!(!cf.colormapped_isosurfaces()[idx].is_visible());
let mut hidden = Scene3dGeometry::new();
cf.append_to(&mut hidden);
assert!(hidden.meshes.is_empty(), "hidden surface emits no geometry");
}
#[test]
fn cut_plane_with_its_own_mode_reslices_and_reranges() {
let (re, im) = ramp_complex_3();
let mut cf = ComplexField3D::new().with_data(&re, &im, 3, 3, 3);
cf.field_mut().cut_plane_mut().set_visible(true);
cf.field_mut()
.cut_plane_mut()
.set_point(Vec3::new(0.0, 1.5, 0.0));
let mut g_default = Scene3dGeometry::new();
cf.append_to(&mut g_default);
assert_eq!(g_default.textured_meshes.len(), 1, "field-mode slice");
cf.set_cut_plane_mode(Some(ComplexMode::Real));
let mut g_real = Scene3dGeometry::new();
cf.append_to(&mut g_real);
assert_eq!(g_real.textured_meshes.len(), 1);
assert_ne!(
g_default.textured_meshes[0].pixels, g_real.textured_meshes[0].pixels,
"own-mode slice differs from the field-mode slice"
);
cf.set_cut_plane_mode(Some(ComplexMode::Absolute));
let mut g_same = Scene3dGeometry::new();
cf.append_to(&mut g_same);
assert_eq!(
g_same.textured_meshes[0].pixels, g_default.textured_meshes[0].pixels,
"own mode equal to the field mode uses the field slice"
);
}
}