use std::f64::consts::PI;
#[inline]
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[inline]
fn mat3_mul_vec3(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ActivationFn {
Relu,
Tanh,
Sigmoid,
Linear,
}
impl ActivationFn {
pub fn apply(&self, x: f64) -> f64 {
match self {
ActivationFn::Relu => x.max(0.0),
ActivationFn::Tanh => x.tanh(),
ActivationFn::Sigmoid => 1.0 / (1.0 + (-x).exp()),
ActivationFn::Linear => x,
}
}
pub fn derivative(&self, x: f64) -> f64 {
match self {
ActivationFn::Relu => {
if x > 0.0 {
1.0
} else {
0.0
}
}
ActivationFn::Tanh => {
let t = x.tanh();
1.0 - t * t
}
ActivationFn::Sigmoid => {
let s = 1.0 / (1.0 + (-x).exp());
s * (1.0 - s)
}
ActivationFn::Linear => 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct NeuralLayer {
pub weights: Vec<Vec<f64>>,
pub bias: Vec<f64>,
pub activation: ActivationFn,
}
impl NeuralLayer {
pub fn new(in_size: usize, out_size: usize) -> Self {
Self {
weights: vec![vec![0.0; in_size]; out_size],
bias: vec![0.0; out_size],
activation: ActivationFn::Relu,
}
}
pub fn with_activation(in_size: usize, out_size: usize, activation: ActivationFn) -> Self {
Self {
weights: vec![vec![0.0; in_size]; out_size],
bias: vec![0.0; out_size],
activation,
}
}
pub fn forward(&self, x: &[f64]) -> Vec<f64> {
let out_size = self.weights.len();
let mut out = Vec::with_capacity(out_size);
for i in 0..out_size {
let mut sum = self.bias[i];
let row = &self.weights[i];
for (j, &xj) in x.iter().enumerate() {
if j < row.len() {
sum += row[j] * xj;
}
}
out.push(self.activation.apply(sum));
}
out
}
pub fn out_size(&self) -> usize {
self.weights.len()
}
pub fn in_size(&self) -> usize {
self.weights.first().map(|r| r.len()).unwrap_or(0)
}
}
#[derive(Debug, Clone)]
pub struct NeuralDeformNet {
pub layers: Vec<NeuralLayer>,
}
impl NeuralDeformNet {
pub fn new(layer_sizes: &[usize]) -> Self {
assert!(layer_sizes.len() >= 2, "need at least input + output layer");
let n = layer_sizes.len();
let layers = (0..n - 1)
.map(|i| {
let activation = if i + 2 == n {
ActivationFn::Linear
} else {
ActivationFn::Relu
};
NeuralLayer::with_activation(layer_sizes[i], layer_sizes[i + 1], activation)
})
.collect();
Self { layers }
}
pub fn forward_flat(&self, x: &[f64]) -> Vec<f64> {
let mut current = x.to_vec();
for layer in &self.layers {
current = layer.forward(¤t);
}
current
}
pub fn forward(&self, rest_pos: &[[f64; 3]]) -> Vec<[f64; 3]> {
rest_pos
.iter()
.map(|p| {
let disp = self.forward_flat(&[p[0], p[1], p[2]]);
let dx = disp.first().copied().unwrap_or(0.0);
let dy = disp.get(1).copied().unwrap_or(0.0);
let dz = disp.get(2).copied().unwrap_or(0.0);
[p[0] + dx, p[1] + dy, p[2] + dz]
})
.collect()
}
pub fn set_weights_layer(&mut self, layer: usize, weights: Vec<Vec<f64>>, bias: Vec<f64>) {
self.layers[layer].weights = weights;
self.layers[layer].bias = bias;
}
}
#[derive(Debug, Clone)]
pub struct PositionalEncoding {
pub n_freqs: usize,
}
impl PositionalEncoding {
pub fn new(n_freqs: usize) -> Self {
Self { n_freqs }
}
pub fn encode(&self, x: &[f64; 3]) -> Vec<f64> {
let mut out = Vec::with_capacity(self.output_size());
for coord in x.iter() {
for k in 0..self.n_freqs {
let freq = (2_u64.pow(k as u32) as f64) * PI;
out.push((freq * coord).sin());
out.push((freq * coord).cos());
}
}
out
}
pub fn output_size(&self) -> usize {
3 * 2 * self.n_freqs
}
}
#[derive(Debug, Clone)]
pub struct ShapeDeformationBasis {
pub modes: Vec<Vec<[f64; 3]>>,
pub mean_shape: Vec<[f64; 3]>,
}
impl ShapeDeformationBasis {
pub fn new(mean: Vec<[f64; 3]>, modes: Vec<Vec<[f64; 3]>>) -> Self {
Self {
modes,
mean_shape: mean,
}
}
pub fn reconstruct(&self, coeffs: &[f64]) -> Vec<[f64; 3]> {
let n = self.mean_shape.len();
let mut out = self.mean_shape.clone();
for (i, coeff) in coeffs.iter().enumerate() {
if i >= self.modes.len() {
break;
}
let mode = &self.modes[i];
for (j, p) in out.iter_mut().enumerate() {
if j < mode.len() {
p[0] += coeff * mode[j][0];
p[1] += coeff * mode[j][1];
p[2] += coeff * mode[j][2];
}
}
}
let _ = n;
out
}
pub fn project(&self, shape: &[[f64; 3]]) -> Vec<f64> {
self.modes
.iter()
.map(|mode| {
let mut dot = 0.0;
for (j, p) in shape.iter().enumerate() {
if j < self.mean_shape.len() && j < mode.len() {
let d = sub3(*p, self.mean_shape[j]);
dot += d[0] * mode[j][0] + d[1] * mode[j][1] + d[2] * mode[j][2];
}
}
dot
})
.collect()
}
pub fn from_sample_shapes(shapes: &[Vec<[f64; 3]>], n_modes: usize) -> Self {
assert!(!shapes.is_empty(), "need at least one sample shape");
let n_verts = shapes[0].len();
let n_shapes = shapes.len();
let mut mean = vec![[0.0_f64; 3]; n_verts];
for shape in shapes {
for (j, p) in shape.iter().enumerate() {
mean[j][0] += p[0];
mean[j][1] += p[1];
mean[j][2] += p[2];
}
}
let inv_n = 1.0 / n_shapes as f64;
for p in mean.iter_mut() {
p[0] *= inv_n;
p[1] *= inv_n;
p[2] *= inv_n;
}
let dim = 3 * n_verts;
let mut x_data: Vec<Vec<f64>> = Vec::with_capacity(n_shapes);
for shape in shapes {
let mut row = Vec::with_capacity(dim);
for (j, p) in shape.iter().enumerate() {
row.push(p[0] - mean[j][0]);
row.push(p[1] - mean[j][1]);
row.push(p[2] - mean[j][2]);
}
x_data.push(row);
}
let actual_modes = n_modes.min(n_shapes).min(dim);
let mut modes: Vec<Vec<[f64; 3]>> = Vec::with_capacity(actual_modes);
let mut residuals = x_data.clone();
for _ in 0..actual_modes {
let mut v: Vec<f64> = residuals[0].clone();
let len: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if len < 1e-30 {
break;
}
for vi in v.iter_mut() {
*vi /= len;
}
for _ in 0..30 {
let u: Vec<f64> = residuals
.iter()
.map(|row| row.iter().zip(v.iter()).map(|(a, b)| a * b).sum())
.collect();
let mut v_new = vec![0.0_f64; dim];
for (row, &ui) in residuals.iter().zip(u.iter()) {
for (k, &rk) in row.iter().enumerate() {
v_new[k] += ui * rk;
}
}
let nrm: f64 = v_new.iter().map(|x| x * x).sum::<f64>().sqrt();
if nrm < 1e-30 {
break;
}
for vi in v_new.iter_mut() {
*vi /= nrm;
}
v = v_new;
}
let mode_verts: Vec<[f64; 3]> = (0..n_verts)
.map(|j| [v[3 * j], v[3 * j + 1], v[3 * j + 2]])
.collect();
modes.push(mode_verts);
for row in residuals.iter_mut() {
let proj: f64 = row.iter().zip(v.iter()).map(|(a, b)| a * b).sum();
for (k, rk) in row.iter_mut().enumerate() {
*rk -= proj * v[k];
}
}
}
Self {
modes,
mean_shape: mean,
}
}
}
#[derive(Debug, Clone)]
pub struct LatentSpaceDeformer {
pub basis: ShapeDeformationBasis,
pub decoder: NeuralDeformNet,
}
impl LatentSpaceDeformer {
pub fn new(basis: ShapeDeformationBasis, decoder: NeuralDeformNet) -> Self {
Self { basis, decoder }
}
pub fn decode(&self, latent: &[f64]) -> Vec<[f64; 3]> {
let coeffs = self.decoder.forward_flat(latent);
self.basis.reconstruct(&coeffs)
}
}
pub fn blend_shapes(shapes: &[Vec<[f64; 3]>], weights: &[f64]) -> Vec<[f64; 3]> {
assert!(!shapes.is_empty(), "need at least one shape");
let n = shapes[0].len();
let mut out = vec![[0.0_f64; 3]; n];
for (shape, &w) in shapes.iter().zip(weights.iter()) {
for (j, p) in shape.iter().enumerate() {
if j < n {
out[j] = add3(out[j], scale3(*p, w));
}
}
}
out
}
pub fn delta_mush_smooth(
positions: &[[f64; 3]],
adjacency: &[Vec<usize>],
lambda: f64,
iterations: usize,
) -> Vec<[f64; 3]> {
let n = positions.len();
let mut current = positions.to_vec();
let mut next = current.clone();
for _ in 0..iterations {
for i in 0..n {
let neighbours = &adjacency[i];
if neighbours.is_empty() {
next[i] = current[i];
continue;
}
let inv_k = 1.0 / neighbours.len() as f64;
let mut avg = [0.0_f64; 3];
for &j in neighbours {
avg = add3(avg, current[j]);
}
avg = scale3(avg, inv_k);
let blended = add3(scale3(current[i], 1.0 - lambda), scale3(avg, lambda));
next[i] = blended;
}
current.clone_from(&next);
}
current
}
pub fn linear_blend_skinning(
rest: &[[f64; 3]],
bones: &[([f64; 3], [[f64; 3]; 3])],
weights: &[Vec<(usize, f64)>],
) -> Vec<[f64; 3]> {
rest.iter()
.zip(weights.iter())
.map(|(p, vw)| {
let mut deformed = [0.0_f64; 3];
for &(bone_idx, w) in vw {
if bone_idx >= bones.len() {
continue;
}
let (translation, rotation) = &bones[bone_idx];
let rotated = mat3_mul_vec3(rotation, *p);
let transformed = add3(rotated, *translation);
deformed = add3(deformed, scale3(transformed, w));
}
deformed
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relu_positive_input() {
assert!((ActivationFn::Relu.apply(2.0) - 2.0).abs() < 1e-12);
}
#[test]
fn relu_negative_input_is_zero() {
assert_eq!(ActivationFn::Relu.apply(-1.0), 0.0);
}
#[test]
fn relu_derivative_positive() {
assert!((ActivationFn::Relu.derivative(1.0) - 1.0).abs() < 1e-12);
}
#[test]
fn relu_derivative_negative_is_zero() {
assert_eq!(ActivationFn::Relu.derivative(-1.0), 0.0);
}
#[test]
fn tanh_at_zero_is_zero() {
assert!(ActivationFn::Tanh.apply(0.0).abs() < 1e-12);
}
#[test]
fn tanh_derivative_at_zero_is_one() {
assert!((ActivationFn::Tanh.derivative(0.0) - 1.0).abs() < 1e-12);
}
#[test]
fn sigmoid_at_zero_is_half() {
assert!((ActivationFn::Sigmoid.apply(0.0) - 0.5).abs() < 1e-12);
}
#[test]
fn sigmoid_derivative_at_zero() {
assert!((ActivationFn::Sigmoid.derivative(0.0) - 0.25).abs() < 1e-12);
}
#[test]
fn linear_apply_identity() {
assert!((ActivationFn::Linear.apply(3.7) - 3.7).abs() < 1e-12);
}
#[test]
fn linear_derivative_is_one() {
assert!((ActivationFn::Linear.derivative(99.0) - 1.0).abs() < 1e-12);
}
#[test]
fn neural_layer_zero_init_forward() {
let layer = NeuralLayer::new(3, 2);
let out = layer.forward(&[1.0, 2.0, 3.0]);
assert_eq!(out.len(), 2);
assert_eq!(out[0], 0.0);
assert_eq!(out[1], 0.0);
}
#[test]
fn neural_layer_size_accessors() {
let layer = NeuralLayer::new(4, 6);
assert_eq!(layer.in_size(), 4);
assert_eq!(layer.out_size(), 6);
}
#[test]
fn neural_layer_custom_weights() {
let mut layer = NeuralLayer::with_activation(2, 1, ActivationFn::Linear);
layer.weights[0] = vec![1.0, 2.0];
layer.bias[0] = 0.5;
let out = layer.forward(&[1.0, 1.0]);
assert!((out[0] - 3.5).abs() < 1e-12);
}
#[test]
fn neural_layer_relu_clamps_negative() {
let mut layer = NeuralLayer::new(1, 1);
layer.bias[0] = -5.0; let out = layer.forward(&[0.0]);
assert_eq!(out[0], 0.0, "ReLU should clamp to 0");
}
#[test]
fn neural_deform_net_zero_displacement() {
let net = NeuralDeformNet::new(&[3, 8, 3]);
let rest = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let deformed = net.forward(&rest);
for (d, r) in deformed.iter().zip(rest.iter()) {
for k in 0..3 {
assert!(
(d[k] - r[k]).abs() < 1e-12,
"zero net should not displace vertices"
);
}
}
}
#[test]
fn neural_deform_net_set_weights() {
let mut net = NeuralDeformNet::new(&[3, 3]);
net.set_weights_layer(0, vec![vec![1.0, 0.0, 0.0]; 3], vec![0.0; 3]);
let out = net.forward_flat(&[5.0, 0.0, 0.0]);
assert!((out[0] - 5.0).abs() < 1e-12, "out[0]={}", out[0]);
}
#[test]
fn neural_deform_net_forward_output_count() {
let net = NeuralDeformNet::new(&[3, 16, 16, 3]);
let rest: Vec<[f64; 3]> = (0..10).map(|i| [i as f64, 0.0, 0.0]).collect();
let deformed = net.forward(&rest);
assert_eq!(deformed.len(), rest.len());
}
#[test]
fn positional_encoding_output_size() {
let pe = PositionalEncoding::new(4);
assert_eq!(pe.output_size(), 3 * 2 * 4);
let encoded = pe.encode(&[0.1, 0.2, 0.3]);
assert_eq!(encoded.len(), pe.output_size());
}
#[test]
fn positional_encoding_zero_point() {
let pe = PositionalEncoding::new(3);
let enc = pe.encode(&[0.0, 0.0, 0.0]);
for i in 0..3 {
for k in 0..3 {
let sin_val = enc[i * 6 + 2 * k];
let cos_val = enc[i * 6 + 2 * k + 1];
assert!(sin_val.abs() < 1e-12, "sin at 0 should be 0, got {sin_val}");
assert!(
(cos_val - 1.0).abs() < 1e-12,
"cos at 0 should be 1, got {cos_val}"
);
}
}
}
#[test]
fn positional_encoding_different_freqs() {
let pe1 = PositionalEncoding::new(1);
let pe2 = PositionalEncoding::new(4);
let pt = [0.5, 0.5, 0.5];
assert_ne!(pe1.encode(&pt).len(), pe2.encode(&pt).len());
}
#[test]
fn basis_reconstruct_zero_coeffs_gives_mean() {
let mean = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let mode0 = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let basis = ShapeDeformationBasis::new(mean.clone(), vec![mode0]);
let rec = basis.reconstruct(&[0.0]);
for (r, m) in rec.iter().zip(mean.iter()) {
for k in 0..3 {
assert!((r[k] - m[k]).abs() < 1e-12);
}
}
}
#[test]
fn basis_reconstruct_unit_coeff_adds_mode() {
let mean = vec![[0.0, 0.0, 0.0]];
let mode0 = vec![[1.0, 2.0, 3.0]];
let basis = ShapeDeformationBasis::new(mean, vec![mode0]);
let rec = basis.reconstruct(&[2.0]);
assert!((rec[0][0] - 2.0).abs() < 1e-12);
assert!((rec[0][1] - 4.0).abs() < 1e-12);
assert!((rec[0][2] - 6.0).abs() < 1e-12);
}
#[test]
fn basis_project_recovers_coeff() {
let mean = vec![[0.0, 0.0, 0.0]];
let mode0 = vec![[1.0, 0.0, 0.0]];
let basis = ShapeDeformationBasis::new(mean.clone(), vec![mode0]);
let shape = vec![[3.0, 0.0, 0.0]];
let coeffs = basis.project(&shape);
assert!((coeffs[0] - 3.0).abs() < 1e-12, "coeff={}", coeffs[0]);
}
#[test]
fn basis_from_samples_returns_correct_n_modes() {
let shapes: Vec<Vec<[f64; 3]>> = (0..5)
.map(|i| vec![[i as f64, 0.0, 0.0], [0.0, i as f64, 0.0]])
.collect();
let basis = ShapeDeformationBasis::from_sample_shapes(&shapes, 2);
assert!(basis.modes.len() <= 2);
assert_eq!(basis.mean_shape.len(), 2);
}
#[test]
fn basis_from_samples_mean_is_average() {
let shape = vec![[2.0_f64, 0.0, 0.0]];
let shapes = vec![shape.clone(), shape.clone(), shape.clone()];
let basis = ShapeDeformationBasis::from_sample_shapes(&shapes, 1);
assert!((basis.mean_shape[0][0] - 2.0).abs() < 1e-10);
}
#[test]
fn latent_space_deformer_decode_returns_correct_count() {
let mean = vec![[0.0_f64; 3]; 4];
let modes = vec![vec![[1.0_f64, 0.0, 0.0]; 4]];
let basis = ShapeDeformationBasis::new(mean, modes);
let decoder = NeuralDeformNet::new(&[2, 1]);
let ld = LatentSpaceDeformer::new(basis, decoder);
let decoded = ld.decode(&[0.5, 0.5]);
assert_eq!(decoded.len(), 4);
}
#[test]
fn blend_shapes_equal_weights_is_average() {
let s0 = vec![[0.0_f64, 0.0, 0.0]];
let s1 = vec![[2.0_f64, 0.0, 0.0]];
let blended = blend_shapes(&[s0, s1], &[0.5, 0.5]);
assert!(
(blended[0][0] - 1.0).abs() < 1e-12,
"blended={}",
blended[0][0]
);
}
#[test]
fn blend_shapes_single_weight_one() {
let s0 = vec![[3.0_f64, 4.0, 5.0]];
let blended = blend_shapes(std::slice::from_ref(&s0), &[1.0]);
assert_eq!(blended[0], s0[0]);
}
#[test]
fn blend_shapes_zero_weights_gives_zero() {
let s0 = vec![[1.0_f64, 2.0, 3.0]];
let blended = blend_shapes(&[s0], &[0.0]);
assert_eq!(blended[0], [0.0, 0.0, 0.0]);
}
#[test]
fn delta_mush_smooth_isolated_vertex_unchanged() {
let positions = vec![[1.0, 2.0, 3.0], [5.0, 0.0, 0.0]];
let adjacency = vec![vec![], vec![0_usize]];
let smoothed = delta_mush_smooth(&positions, &adjacency, 0.5, 1);
assert_eq!(smoothed[0], positions[0]);
}
#[test]
fn delta_mush_smooth_chain_converges() {
let positions = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
let adjacency = vec![vec![1usize], vec![0usize, 2usize], vec![1usize]];
let smoothed = delta_mush_smooth(&positions, &adjacency, 0.5, 20);
assert!(
smoothed[1][0] < 9.0,
"vertex 1 should have moved, x={}",
smoothed[1][0]
);
}
#[test]
fn delta_mush_smooth_zero_lambda_no_change() {
let positions = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let adjacency = vec![vec![1_usize], vec![0_usize]];
let smoothed = delta_mush_smooth(&positions, &adjacency, 0.0, 5);
for (s, p) in smoothed.iter().zip(positions.iter()) {
for k in 0..3 {
assert!((s[k] - p[k]).abs() < 1e-12);
}
}
}
#[test]
fn lbs_identity_transform_no_change() {
let rest = vec![[1.0, 2.0, 3.0]];
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let bones = vec![([0.0_f64, 0.0, 0.0], identity)];
let weights = vec![vec![(0usize, 1.0)]];
let deformed = linear_blend_skinning(&rest, &bones, &weights);
for k in 0..3 {
assert!((deformed[0][k] - rest[0][k]).abs() < 1e-12);
}
}
#[test]
fn lbs_translation_only() {
let rest = vec![[0.0, 0.0, 0.0]];
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let bones = vec![([5.0_f64, 3.0, 1.0], identity)];
let weights = vec![vec![(0usize, 1.0)]];
let deformed = linear_blend_skinning(&rest, &bones, &weights);
assert!((deformed[0][0] - 5.0).abs() < 1e-12);
assert!((deformed[0][1] - 3.0).abs() < 1e-12);
assert!((deformed[0][2] - 1.0).abs() < 1e-12);
}
#[test]
fn lbs_two_bones_weighted() {
let rest = vec![[1.0, 0.0, 0.0]];
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let bones = vec![
([2.0_f64, 0.0, 0.0], identity),
([-2.0_f64, 0.0, 0.0], identity),
];
let weights = vec![vec![(0usize, 0.5), (1usize, 0.5)]];
let deformed = linear_blend_skinning(&rest, &bones, &weights);
assert!((deformed[0][0] - 1.0).abs() < 1e-12, "x={}", deformed[0][0]);
}
#[test]
fn lbs_rotation_90_deg_around_z() {
let rot90z = [[0.0_f64, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
let rest = vec![[1.0, 0.0, 0.0]];
let bones = vec![([0.0_f64, 0.0, 0.0], rot90z)];
let weights = vec![vec![(0usize, 1.0)]];
let deformed = linear_blend_skinning(&rest, &bones, &weights);
assert!((deformed[0][0] - 0.0).abs() < 1e-12, "x={}", deformed[0][0]);
assert!((deformed[0][1] - 1.0).abs() < 1e-12, "y={}", deformed[0][1]);
}
#[test]
fn lbs_out_of_range_bone_index_ignored() {
let rest = vec![[1.0, 1.0, 1.0]];
let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let bones = vec![([0.0_f64, 0.0, 0.0], identity)];
let weights = vec![vec![(99usize, 1.0)]];
let deformed = linear_blend_skinning(&rest, &bones, &weights);
assert_eq!(deformed[0], [0.0, 0.0, 0.0]);
}
#[test]
fn blend_shapes_multiple_verts() {
let s0 = vec![[0.0_f64, 0.0, 0.0], [1.0, 1.0, 1.0]];
let s1 = vec![[2.0_f64, 2.0, 2.0], [3.0, 3.0, 3.0]];
let blended = blend_shapes(&[s0, s1], &[0.5, 0.5]);
assert!((blended[0][0] - 1.0).abs() < 1e-12);
assert!((blended[1][0] - 2.0).abs() < 1e-12);
}
#[test]
fn positional_encoding_known_value() {
let pe = PositionalEncoding::new(1);
let enc = pe.encode(&[0.5, 0.0, 0.0]);
assert!((enc[0] - 1.0).abs() < 1e-10, "sin(π*0.5)≈1, got {}", enc[0]);
assert!(enc[1].abs() < 1e-10, "cos(π*0.5)≈0, got {}", enc[1]);
}
#[test]
fn neural_layer_tanh_output_bounded() {
let mut layer = NeuralLayer::with_activation(1, 3, ActivationFn::Tanh);
layer.weights[0][0] = 100.0;
layer.weights[1][0] = -100.0;
layer.weights[2][0] = 0.0;
let out = layer.forward(&[1.0]);
assert!((out[0] - 1.0).abs() < 1e-6, "tanh(100)≈1");
assert!((out[1] + 1.0).abs() < 1e-6, "tanh(-100)≈-1");
}
#[test]
fn basis_from_samples_reconstruct_close_to_sample() {
let shapes = vec![vec![[1.0_f64, 0.0, 0.0]], vec![[1.0_f64, 0.0, 0.0]]];
let basis = ShapeDeformationBasis::from_sample_shapes(&shapes, 2);
let rec = basis.reconstruct(&[]);
assert!((rec[0][0] - 1.0).abs() < 1e-10);
}
#[test]
fn delta_mush_iterations_zero_is_identity() {
let positions = vec![[3.0, 1.0, 2.0], [0.0, 0.0, 0.0]];
let adjacency = vec![vec![1_usize], vec![0_usize]];
let smoothed = delta_mush_smooth(&positions, &adjacency, 0.8, 0);
assert_eq!(smoothed, positions);
}
}