#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct IsolineItem {
pub positions: Vec<[f32; 3]>,
pub indices: Vec<u32>,
pub scalars: Vec<f32>,
pub isovalues: Vec<f32>,
pub color: [f32; 4],
pub line_width: f32,
pub model_matrix: glam::Mat4,
pub depth_bias: f32,
}
impl Default for IsolineItem {
fn default() -> Self {
Self {
positions: Vec::new(),
indices: Vec::new(),
scalars: Vec::new(),
isovalues: Vec::new(),
color: [1.0, 1.0, 1.0, 1.0],
line_width: 1.0,
model_matrix: glam::Mat4::IDENTITY,
depth_bias: 0.001,
}
}
}
pub fn extract_isolines(item: &IsolineItem) -> (Vec<[f32; 3]>, Vec<u32>) {
if item.positions.is_empty()
|| item.indices.is_empty()
|| item.scalars.is_empty()
|| item.isovalues.is_empty()
{
return (Vec::new(), Vec::new());
}
if item.scalars.len() != item.positions.len() {
return (Vec::new(), Vec::new());
}
let tri_count = item.indices.len() / 3;
let iso_count = item.isovalues.len();
let mut out_positions: Vec<[f32; 3]> = Vec::with_capacity(tri_count * iso_count * 2);
let mut strip_lengths: Vec<u32> = Vec::with_capacity(tri_count * iso_count);
for &iso in &item.isovalues {
extract_for_isovalue(item, iso, &mut out_positions, &mut strip_lengths);
}
(out_positions, strip_lengths)
}
fn extract_for_isovalue(
item: &IsolineItem,
iso: f32,
out_positions: &mut Vec<[f32; 3]>,
strip_lengths: &mut Vec<u32>,
) {
let positions = &item.positions;
let scalars = &item.scalars;
let model = item.model_matrix;
let bias = item.depth_bias;
let tri_count = item.indices.len() / 3;
for tri_idx in 0..tri_count {
let i0 = item.indices[tri_idx * 3] as usize;
let i1 = item.indices[tri_idx * 3 + 1] as usize;
let i2 = item.indices[tri_idx * 3 + 2] as usize;
if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
continue;
}
let p0 = glam::Vec3::from(positions[i0]);
let p1 = glam::Vec3::from(positions[i1]);
let p2 = glam::Vec3::from(positions[i2]);
let s0 = scalars[i0];
let s1 = scalars[i1];
let s2 = scalars[i2];
let edge_a = p1 - p0;
let edge_b = p2 - p0;
let cross = edge_a.cross(edge_b);
let len_sq = cross.length_squared();
if len_sq < f32::EPSILON {
continue; }
let face_normal = cross / len_sq.sqrt();
let edges = [(p0, p1, s0, s1), (p1, p2, s1, s2), (p2, p0, s2, s0)];
let mut crossings: [Option<glam::Vec3>; 3] = [None; 3];
for (edge_slot, &(ea, eb, sa, sb)) in edges.iter().enumerate() {
if let Some(p) = edge_crossing(ea, eb, sa, sb, iso) {
crossings[edge_slot] = Some(p);
}
}
let pts: Vec<glam::Vec3> = crossings.iter().filter_map(|&c| c).collect();
if pts.len() < 2 {
continue;
}
let a = pts[0];
let b = pts[1];
let bias_vec = face_normal * bias;
let wa = transform_point(model, a + bias_vec);
let wb = transform_point(model, b + bias_vec);
out_positions.push(wa);
out_positions.push(wb);
strip_lengths.push(2);
}
}
#[inline]
fn edge_crossing(pa: glam::Vec3, pb: glam::Vec3, sa: f32, sb: f32, iso: f32) -> Option<glam::Vec3> {
if (sb - sa).abs() < f32::EPSILON {
return None;
}
let crosses = (sa < iso && sb >= iso) || (sb < iso && sa >= iso);
if !crosses {
return None;
}
let t = (iso - sa) / (sb - sa);
Some(pa + t * (pb - pa))
}
#[inline]
fn transform_point(m: glam::Mat4, p: glam::Vec3) -> [f32; 3] {
m.transform_point3(p).to_array()
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_triangle_item(scalars: Vec<f32>, isovalues: Vec<f32>) -> IsolineItem {
IsolineItem {
positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
indices: vec![0, 1, 2],
scalars,
isovalues,
depth_bias: 0.0,
..IsolineItem::default()
}
}
#[test]
fn edge_crossing_midpoint() {
let a = glam::Vec3::ZERO;
let b = glam::Vec3::X;
let p = edge_crossing(a, b, 0.0, 1.0, 0.5).unwrap();
assert!((p.x - 0.5).abs() < 1e-5);
assert!(p.y.abs() < 1e-5);
}
#[test]
fn edge_crossing_same_side_returns_none() {
let a = glam::Vec3::ZERO;
let b = glam::Vec3::X;
assert!(edge_crossing(a, b, 0.0, 0.5, 1.0).is_none());
}
#[test]
fn edge_crossing_equal_scalars_returns_none() {
let a = glam::Vec3::ZERO;
let b = glam::Vec3::X;
assert!(edge_crossing(a, b, 0.5, 0.5, 0.5).is_none());
}
#[test]
fn edge_crossing_at_endpoint() {
let a = glam::Vec3::ZERO;
let b = glam::Vec3::X;
let p = edge_crossing(a, b, 0.0, 1.0, 1.0).unwrap();
assert!((p.x - 1.0).abs() < 1e-5);
}
#[test]
fn extract_empty_positions_returns_empty() {
let item = IsolineItem::default();
let (pos, strips) = extract_isolines(&item);
assert!(pos.is_empty());
assert!(strips.is_empty());
}
#[test]
fn extract_mismatched_scalars_returns_empty() {
let item = IsolineItem {
positions: vec![[0.0; 3]; 3],
indices: vec![0, 1, 2],
scalars: vec![0.0, 1.0], isovalues: vec![0.5],
..IsolineItem::default()
};
let (pos, _) = extract_isolines(&item);
assert!(pos.is_empty());
}
#[test]
fn extract_single_triangle_linear_ramp() {
let item = simple_triangle_item(vec![0.0, 1.0, 0.0], vec![0.5]);
let (pos, strips) = extract_isolines(&item);
assert_eq!(strips.len(), 1);
assert_eq!(strips[0], 2);
assert_eq!(pos.len(), 2);
}
#[test]
fn extract_isovalue_outside_range_no_segments() {
let item = simple_triangle_item(vec![0.0, 0.5, 0.25], vec![10.0]);
let (pos, strips) = extract_isolines(&item);
assert!(pos.is_empty());
assert!(strips.is_empty());
}
#[test]
fn extract_multiple_isovalues() {
let item = simple_triangle_item(vec![0.0, 1.0, 0.0], vec![0.25, 0.75]);
let (pos, strips) = extract_isolines(&item);
assert_eq!(strips.len(), 2);
assert_eq!(pos.len(), 4);
}
#[test]
fn extract_degenerate_triangle_skipped() {
let item = IsolineItem {
positions: vec![[0.0; 3]; 3],
indices: vec![0, 1, 2],
scalars: vec![0.0, 1.0, 0.0],
isovalues: vec![0.5],
depth_bias: 0.0,
..IsolineItem::default()
};
let (pos, _) = extract_isolines(&item);
assert!(pos.is_empty());
}
#[test]
fn extract_out_of_bounds_indices_skipped() {
let item = IsolineItem {
positions: vec![[0.0; 3]; 3],
indices: vec![0, 1, 99], scalars: vec![0.0, 1.0, 0.0],
isovalues: vec![0.5],
..IsolineItem::default()
};
let (pos, _) = extract_isolines(&item);
assert!(pos.is_empty());
}
#[test]
fn extract_model_matrix_transforms_output() {
let item = IsolineItem {
positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
indices: vec![0, 1, 2],
scalars: vec![0.0, 1.0, 0.0],
isovalues: vec![0.5],
model_matrix: glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
depth_bias: 0.0,
..IsolineItem::default()
};
let (pos, _) = extract_isolines(&item);
for p in &pos {
assert!(p[0] > 9.0, "expected translated X, got {}", p[0]);
}
}
#[test]
fn extract_two_triangles_sharing_edge() {
let item = IsolineItem {
positions: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
],
indices: vec![0, 1, 2, 0, 2, 3],
scalars: vec![0.0, 1.0, 1.0, 0.0],
isovalues: vec![0.5],
depth_bias: 0.0,
..IsolineItem::default()
};
let (pos, strips) = extract_isolines(&item);
assert_eq!(strips.len(), 2); assert_eq!(pos.len(), 4);
}
#[test]
fn extract_empty_isovalues_returns_empty() {
let item = simple_triangle_item(vec![0.0, 1.0, 0.0], vec![]);
let (pos, strips) = extract_isolines(&item);
assert!(pos.is_empty());
assert!(strips.is_empty());
}
}