use egui::Color32;
use crate::core::colormap::{AutoscaleMode, Colormap, ColormapName};
use crate::core::complex::ComplexMode;
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::render::gpu_scene3d::{
ImageInterpolation, PointMarker, Scene3dGeometry, Scene3dImageLayer, Scene3dTexturedMesh,
flat_normal,
};
pub const DEFAULT_SCATTER3D_SIZE: f32 = 6.0;
#[derive(Clone, Debug)]
pub struct Scatter3D {
x: Vec<f32>,
y: Vec<f32>,
z: Vec<f32>,
values: Vec<f64>,
colormap: Colormap,
marker: PointMarker,
size: f32,
}
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::new(ColormapName::Viridis, 0.0, 1.0),
marker: PointMarker::Circle,
size: DEFAULT_SCATTER3D_SIZE,
}
}
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();
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;
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.colormap = colormap;
self
}
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) = mode.range(&self.values, self.colormap.autoscale_percentiles);
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()
}
pub fn 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 append_to(&self, geometry: &mut Scene3dGeometry) {
for i in 0..self.len() {
let [r, g, b, a] = self.colormap.color_at(self.values[i]);
geometry.add_point(
[self.x[i], self.y[i], self.z[i]],
Color32::from_rgba_unmultiplied(r, g, b, a),
self.size,
self.marker,
);
}
}
}
#[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>>,
}
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,
}
}
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)> {
positions_bounds(&self.positions)
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
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(),
],
};
geometry.add_mesh_triangle_rgba(p, rgba, normals);
}
}
}
#[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,
}
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::new(ColormapName::Viridis, 0.0, 1.0),
}
}
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);
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;
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.colormap = colormap;
self
}
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) = mode.range(&self.values, self.colormap.autoscale_percentiles);
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)> {
positions_bounds(&self.positions)
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
let rgba_at = |i: usize| {
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()
};
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],
};
geometry.add_mesh_triangle_rgba(p, [rgba_at(i0), rgba_at(i1), rgba_at(i2)], normals);
}
}
}
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();
self.mesh = cylindrical_volume_mesh(
&self.positions,
diagonal / 2.0,
dz,
&angles,
&self.colors,
true,
rotation_matrix(rotation.0, rotation.1),
);
}
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);
}
}
#[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);
self.mesh = cylindrical_volume_mesh(
&self.positions,
self.radius,
self.height,
&angles,
&self.colors,
false,
rotation_matrix(rotation.0, rotation.1),
);
}
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);
}
}
#[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);
self.mesh = cylindrical_volume_mesh(
&self.positions,
self.radius,
self.height,
&angles,
&self.colors,
true,
rotation_matrix(rotation.0, rotation.1),
);
}
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);
}
}
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,
}
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::new(ColormapName::Viridis, 0.0, 1.0),
origin: [0.0, 0.0, 0.0],
scale: [1.0, 1.0],
interpolation: ImageInterpolation::Nearest,
}
}
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;
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;
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.colormap = colormap;
self
}
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) = mode.range(&self.data, self.colormap.autoscale_percentiles);
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)> {
image_bounds(self.width, self.height, self.origin, self.scale)
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
let mut pixels = Vec::with_capacity(self.data.len() * 4);
for &v in &self.data {
let [r, g, b, a] = self.colormap.color_at(v);
pixels.extend_from_slice(&premul_linear_rgba8(Color32::from_rgba_unmultiplied(
r, g, b, a,
)));
}
geometry.add_image_layer(Scene3dImageLayer {
pixels,
width: self.width as u32,
height: self.height as u32,
origin: self.origin,
scale: self.scale,
interpolation: self.interpolation,
});
}
}
#[derive(Clone, Debug)]
pub struct ImageRgba3D {
pixels: Vec<Color32>,
width: usize,
height: usize,
origin: [f32; 3],
scale: [f32; 2],
interpolation: ImageInterpolation,
}
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,
}
}
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)> {
image_bounds(self.width, self.height, self.origin, self.scale)
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
let mut pixels = Vec::with_capacity(self.pixels.len() * 4);
for &c in &self.pixels {
pixels.extend_from_slice(&premul_linear_rgba8(c));
}
geometry.add_image_layer(Scene3dImageLayer {
pixels,
width: self.width as u32,
height: self.height as u32,
origin: self.origin,
scale: self.scale,
interpolation: self.interpolation,
});
}
}
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,
}
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::new(ColormapName::Viridis, 0.0, 1.0),
}
}
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;
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;
}
pub fn with_colormap(mut self, colormap: Colormap) -> Self {
self.colormap = colormap;
self
}
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) = mode.range(&self.values, self.colormap.autoscale_percentiles);
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)> {
height_grid_bounds(&self.heights, self.h_width, self.h_height)
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
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, g, b, a] = self.colormap.color_at(self.values[vr * self.v_width + vc]);
geometry.add_point(
[col as f32, row as f32, z],
Color32::from_rgba_unmultiplied(r, g, b, a),
1.0,
PointMarker::Square,
);
}
}
}
}
#[derive(Clone, Debug)]
pub struct HeightMapRGBA {
heights: Vec<f32>,
h_width: usize,
h_height: usize,
colors: Vec<Color32>,
c_width: usize,
c_height: usize,
}
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,
}
}
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)> {
height_grid_bounds(&self.heights, self.h_width, self.h_height)
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.is_empty() {
return;
}
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];
geometry.add_point([col as f32, row as f32, z], color, 1.0, PointMarker::Square);
}
}
}
}
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,
}
impl Isosurface {
pub fn new(level: f32, color: Color32) -> Self {
Self {
level,
auto: None,
color,
}
}
pub fn new_auto(auto: fn(&[f32]) -> f32, color: Color32) -> Self {
Self {
level: f32::NAN,
auto: Some(auto),
color,
}
}
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;
}
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,
}
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::new(ColormapName::Viridis, 0.0, 1.0),
interpolation: ImageInterpolation::Linear,
resolution: DEFAULT_CUT_PLANE_RESOLUTION,
visible: false,
}
}
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;
}
}
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 [r, g, b, a] = cut_plane.colormap.color_at(value as f64);
pixels.extend_from_slice(&premul_linear_rgba8(Color32::from_rgba_unmultiplied(
r, g, b, a,
)));
}
}
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,
}
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(),
}
}
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;
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 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) = mode.range(&values, self.cut_plane.colormap.autoscale_percentiles);
self.cut_plane.colormap.vmin = vmin;
self.cut_plane.colormap.vmax = vmax;
(vmin, vmax)
}
pub fn 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 value_at(&self, world: Vec3) -> Option<f32> {
let (min, max) = self.bounds()?;
if world.x < min.x
|| world.y < min.y
|| world.z < min.z
|| world.x > max.x
|| world.y > max.y
|| world.z > max.z
{
return None;
}
Some(sample_field_value(
&self.data,
self.depth,
self.height,
self.width,
world,
self.cut_plane.interpolation(),
))
}
pub fn pick_cut_plane(&self, segment: (Vec3, Vec3)) -> Option<Vec3> {
if !self.cut_plane.is_visible() || self.data.is_empty() {
return None;
}
let plane = self.cut_plane.plane();
segment_plane_intersect(segment.0, segment.1, plane.normal(), plane.point())
.into_iter()
.find(|&hit| self.value_at(hit).is_some())
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
if self.data.is_empty() {
return;
}
let mut order: Vec<usize> = (0..self.isosurfaces.len()).collect();
order.sort_by(|&a, &b| {
self.isosurfaces[b]
.level
.total_cmp(&self.isosurfaces[a].level)
});
for i in order {
let iso = &self.isosurfaces[i];
if !iso.level.is_finite() {
continue;
}
let Some((vertices, normals, indices)) = marching_cubes_isosurface(
&self.data,
self.depth,
self.height,
self.width,
iso.level,
true,
) else {
continue;
};
for tri in indices.chunks_exact(3) {
let p = [0usize, 1, 2].map(|k| {
let v = vertices[tri[k] as usize];
[v[2] + 0.5, v[1] + 0.5, v[0] + 0.5]
});
let n = [0usize, 1, 2].map(|k| {
let nm = normals[tri[k] as usize];
[nm[2], nm[1], nm[0]]
});
geometry.add_mesh_triangle(p, iso.color, n);
}
}
if self.cut_plane.visible
&& let Some(mesh) = build_cut_plane_mesh(
&self.data,
self.depth,
self.height,
self.width,
&self.cut_plane,
)
{
geometry.add_textured_mesh(mesh);
}
}
}
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 ComplexField3D {
re: Vec<f32>,
im: Vec<f32>,
depth: usize,
height: usize,
width: usize,
mode: ComplexMode,
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,
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.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 bounds(&self) -> Option<(Vec3, Vec3)> {
self.field.bounds()
}
pub fn append_to(&self, geometry: &mut Scene3dGeometry) {
self.field.append_to(geometry);
}
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);
}
}
#[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 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 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));
}
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 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 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"
);
}
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)"
);
}
}