#[allow(dead_code)]
pub struct HeatField {
pub values: Vec<f32>,
pub vertex_count: usize,
}
#[allow(dead_code)]
pub struct HeatDiffuseConfig {
pub iterations: u32,
pub diffusion_rate: f32,
pub boundary_fixed: bool,
}
#[allow(dead_code)]
pub struct HeatSource {
pub vertex_index: usize,
pub temperature: f32,
}
#[allow(dead_code)]
pub fn default_heat_config() -> HeatDiffuseConfig {
HeatDiffuseConfig {
iterations: 100,
diffusion_rate: 0.25,
boundary_fixed: true,
}
}
#[allow(dead_code)]
pub fn new_heat_field(vertex_count: usize, initial: f32) -> HeatField {
HeatField {
values: vec![initial; vertex_count],
vertex_count,
}
}
#[allow(dead_code)]
pub fn set_heat_source(field: &mut HeatField, src: &HeatSource) {
if src.vertex_index < field.vertex_count {
field.values[src.vertex_index] = src.temperature;
}
}
#[allow(dead_code)]
pub fn diffuse_step(field: &mut HeatField, adjacency: &[Vec<usize>], cfg: &HeatDiffuseConfig) {
let old = field.values.clone();
for (i, neighbors) in adjacency.iter().enumerate() {
if !neighbors.is_empty() {
let avg: f32 = neighbors.iter().map(|&j| old[j]).sum::<f32>() / neighbors.len() as f32;
field.values[i] = old[i] + cfg.diffusion_rate * (avg - old[i]);
}
}
}
#[allow(dead_code)]
pub fn diffuse_heat(
field: &mut HeatField,
adjacency: &[Vec<usize>],
sources: &[HeatSource],
cfg: &HeatDiffuseConfig,
) {
for src in sources {
set_heat_source(field, src);
}
for _ in 0..cfg.iterations {
diffuse_step(field, adjacency, cfg);
if cfg.boundary_fixed {
for src in sources {
set_heat_source(field, src);
}
}
}
}
#[allow(dead_code)]
pub fn build_adjacency(vertex_count: usize, indices: &[u32]) -> Vec<Vec<usize>> {
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); vertex_count];
let triangles = indices.len() / 3;
for t in 0..triangles {
let a = indices[t * 3] as usize;
let b = indices[t * 3 + 1] as usize;
let c = indices[t * 3 + 2] as usize;
for &(u, v) in &[(a, b), (b, a), (b, c), (c, b), (a, c), (c, a)] {
if u < vertex_count && v < vertex_count && !adj[u].contains(&v) {
adj[u].push(v);
}
}
}
adj
}
#[allow(dead_code)]
pub fn heat_gradient(field: &HeatField, adjacency: &[Vec<usize>], vertex_idx: usize) -> f32 {
let neighbors = &adjacency[vertex_idx];
if neighbors.is_empty() {
return 0.0;
}
let sum: f32 = neighbors
.iter()
.map(|&j| (field.values[j] - field.values[vertex_idx]).abs())
.sum();
sum / neighbors.len() as f32
}
#[allow(dead_code)]
pub fn normalize_field(field: &mut HeatField) {
let mn = heat_field_min(field);
let mx = heat_field_max(field);
let range = mx - mn;
if range > 1e-10 {
for v in field.values.iter_mut() {
*v = (*v - mn) / range;
}
}
}
#[allow(dead_code)]
pub fn heat_field_min(field: &HeatField) -> f32 {
field.values.iter().cloned().fold(f32::INFINITY, f32::min)
}
#[allow(dead_code)]
pub fn heat_field_max(field: &HeatField) -> f32 {
field
.values
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max)
}
#[allow(dead_code)]
pub fn heat_to_color(value: f32) -> [f32; 3] {
let t = value.clamp(0.0, 1.0);
[t, 0.0, 1.0 - t]
}
fn dist3(a: [f32; 3], b: [f32; 3]) -> f32 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[allow(dead_code)]
pub fn geodesic_heat_distance(
positions: &[[f32; 3]],
indices: &[u32],
source_vertex: usize,
iterations: u32,
) -> Vec<f32> {
let n = positions.len();
let adj = build_adjacency(n, indices);
let cfg = HeatDiffuseConfig {
iterations,
diffusion_rate: 0.25,
boundary_fixed: false,
};
let mut field = new_heat_field(n, 0.0);
let src = HeatSource {
vertex_index: source_vertex,
temperature: 1.0,
};
diffuse_heat(&mut field, &adj, &[src], &cfg);
let heat_max = heat_field_max(&field).max(1e-10);
field
.values
.iter()
.enumerate()
.map(|(i, &h)| {
let heat_dist = (1.0 - h / heat_max).max(0.0);
let edge_len: f32 = if !adj[i].is_empty() {
adj[i]
.iter()
.map(|&j| dist3(positions[i], positions[j]))
.sum::<f32>()
/ adj[i].len() as f32
} else {
dist3(positions[i], positions[source_vertex])
};
heat_dist * edge_len * n as f32
})
.collect()
}
#[allow(dead_code)]
pub fn threshold_field(field: &HeatField, threshold: f32) -> Vec<bool> {
field.values.iter().map(|&v| v >= threshold).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_square_indices() -> Vec<u32> {
vec![0, 1, 2, 0, 2, 3]
}
#[test]
fn test_new_heat_field_count() {
let f = new_heat_field(10, 0.5);
assert_eq!(f.vertex_count, 10);
assert_eq!(f.values.len(), 10);
}
#[test]
fn test_new_heat_field_initial_value() {
let f = new_heat_field(5, 2.5);
assert!(f.values.iter().all(|&v| (v - 2.5).abs() < 1e-6));
}
#[test]
fn test_build_adjacency_basic() {
let adj = build_adjacency(4, &simple_square_indices());
assert_eq!(adj.len(), 4);
assert!(adj[0].contains(&1));
assert!(adj[0].contains(&2));
assert!(adj[0].contains(&3));
}
#[test]
fn test_build_adjacency_symmetry() {
let adj = build_adjacency(4, &simple_square_indices());
for (i, neighbors) in adj.iter().enumerate() {
for &j in neighbors {
assert!(
adj[j].contains(&i),
"Adjacency should be symmetric: {i} -> {j} but not {j} -> {i}"
);
}
}
}
#[test]
fn test_set_heat_source() {
let mut f = new_heat_field(5, 0.0);
set_heat_source(
&mut f,
&HeatSource {
vertex_index: 2,
temperature: 1.0,
},
);
assert!((f.values[2] - 1.0).abs() < 1e-6);
assert!((f.values[0]).abs() < 1e-6);
}
#[test]
fn test_set_heat_source_out_of_bounds() {
let mut f = new_heat_field(3, 0.0);
set_heat_source(
&mut f,
&HeatSource {
vertex_index: 100,
temperature: 999.0,
},
);
assert!(f.values.iter().all(|&v| v.abs() < 1e-6));
}
#[test]
fn test_heat_field_min_max() {
let mut f = new_heat_field(4, 0.0);
f.values[0] = -1.0;
f.values[3] = 5.0;
assert!((heat_field_min(&f) - (-1.0)).abs() < 1e-6);
assert!((heat_field_max(&f) - 5.0).abs() < 1e-6);
}
#[test]
fn test_normalize_field() {
let mut f = new_heat_field(4, 0.0);
f.values = vec![0.0, 2.0, 4.0, 8.0];
normalize_field(&mut f);
assert!((f.values[0] - 0.0).abs() < 1e-6);
assert!((f.values[3] - 1.0).abs() < 1e-6);
assert!((f.values[2] - 0.5).abs() < 1e-6);
}
#[test]
fn test_normalize_field_uniform() {
let mut f = new_heat_field(3, 3.0);
normalize_field(&mut f); assert_eq!(f.values.len(), 3);
}
#[test]
fn test_heat_to_color_blue_at_zero() {
let c = heat_to_color(0.0);
assert!((c[0]).abs() < 1e-6); assert!((c[2] - 1.0).abs() < 1e-6); }
#[test]
fn test_heat_to_color_red_at_one() {
let c = heat_to_color(1.0);
assert!((c[0] - 1.0).abs() < 1e-6); assert!((c[2]).abs() < 1e-6); }
#[test]
fn test_threshold_field() {
let mut f = new_heat_field(4, 0.0);
f.values = vec![0.1, 0.5, 0.9, 0.3];
let mask = threshold_field(&f, 0.4);
assert!(!mask[0]);
assert!(mask[1]);
assert!(mask[2]);
assert!(!mask[3]);
}
#[test]
fn test_diffuse_heat_propagates() {
let idx = vec![0u32, 1, 2, 0, 2, 3];
let adj = build_adjacency(4, &idx);
let cfg = HeatDiffuseConfig {
iterations: 50,
diffusion_rate: 0.25,
boundary_fixed: true,
};
let mut field = new_heat_field(4, 0.0);
let src = HeatSource {
vertex_index: 0,
temperature: 1.0,
};
diffuse_heat(&mut field, &adj, &[src], &cfg);
assert!(field.values[0] > 0.5, "Source should stay hot");
assert!(field.values[1] > 0.0, "Neighbor should receive heat");
assert!(field.values[3] > 0.0, "Far neighbor should receive heat");
}
#[test]
fn test_default_heat_config() {
let cfg = default_heat_config();
assert!(cfg.iterations > 0);
assert!(cfg.diffusion_rate > 0.0);
}
#[test]
fn test_heat_gradient_uniform() {
let f = new_heat_field(4, 1.0);
let adj = build_adjacency(4, &simple_square_indices());
let grad = heat_gradient(&f, &adj, 0);
assert!((grad).abs() < 1e-6, "Uniform field has zero gradient");
}
#[test]
fn test_geodesic_heat_distance_source_small() {
let positions = vec![
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let indices = vec![0u32, 1, 2, 0, 2, 3];
let dists = geodesic_heat_distance(&positions, &indices, 0, 100);
assert_eq!(dists.len(), 4);
assert!(dists[0] <= dists[1] + 1e-3);
}
#[test]
fn test_build_adjacency_empty() {
let adj = build_adjacency(5, &[]);
assert_eq!(adj.len(), 5);
assert!(adj.iter().all(|n| n.is_empty()));
}
}