#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SoftFalloff {
Smooth,
Linear,
Sharp,
Constant,
}
#[derive(Debug, Clone)]
pub struct TweakParams {
pub radius: f32,
pub falloff: SoftFalloff,
pub delta: [f32; 3],
}
#[derive(Debug, Clone)]
pub struct TweakResult {
pub positions: Vec<[f32; 3]>,
pub affected_count: usize,
pub max_weight: f32,
}
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()
}
pub fn soft_weight(d: f32, radius: f32, falloff: SoftFalloff) -> f32 {
if d >= radius || radius < 1e-8 {
return 0.0;
}
let t = 1.0 - d / radius;
match falloff {
SoftFalloff::Smooth => t * t * (3.0 - 2.0 * t),
SoftFalloff::Linear => t,
SoftFalloff::Sharp => t * t,
SoftFalloff::Constant => 1.0,
}
}
pub fn apply_tweak(positions: &[[f32; 3]], centre: [f32; 3], params: &TweakParams) -> TweakResult {
let mut out = positions.to_vec();
let mut affected_count = 0usize;
let mut max_weight = 0.0f32;
for pos in out.iter_mut() {
let d = dist3(*pos, centre);
let w = soft_weight(d, params.radius, params.falloff);
if w > 1e-8 {
affected_count += 1;
if w > max_weight {
max_weight = w;
}
pos[0] += w * params.delta[0];
pos[1] += w * params.delta[1];
pos[2] += w * params.delta[2];
}
}
TweakResult {
positions: out,
affected_count,
max_weight,
}
}
pub fn compute_soft_weights(
positions: &[[f32; 3]],
centre: [f32; 3],
radius: f32,
falloff: SoftFalloff,
) -> Vec<f32> {
positions
.iter()
.map(|&p| soft_weight(dist3(p, centre), radius, falloff))
.collect()
}
pub fn default_tweak_params(radius: f32, dy: f32) -> TweakParams {
TweakParams {
radius,
falloff: SoftFalloff::Smooth,
delta: [0.0, dy, 0.0],
}
}
pub fn count_selected(positions: &[[f32; 3]], centre: [f32; 3], radius: f32) -> usize {
positions
.iter()
.filter(|&&p| dist3(p, centre) < radius)
.count()
}
pub fn average_displacement(result: &TweakResult, original: &[[f32; 3]]) -> f32 {
if result.affected_count == 0 {
return 0.0;
}
let total: f32 = result
.positions
.iter()
.zip(original.iter())
.map(|(&a, &b)| dist3(a, b))
.sum();
total / result.affected_count as f32
}
pub fn undo_tweak(positions: &[[f32; 3]], centre: [f32; 3], params: &TweakParams) -> TweakResult {
let inv_params = TweakParams {
radius: params.radius,
falloff: params.falloff,
delta: [-params.delta[0], -params.delta[1], -params.delta[2]],
};
apply_tweak(positions, centre, &inv_params)
}
pub fn scale_tweak_delta(params: &TweakParams, factor: f32) -> TweakParams {
TweakParams {
radius: params.radius,
falloff: params.falloff,
delta: [
params.delta[0] * factor,
params.delta[1] * factor,
params.delta[2] * factor,
],
}
}
pub fn clamp_tweak_result(result: &mut TweakResult, min_bound: [f32; 3], max_bound: [f32; 3]) {
for pos in result.positions.iter_mut() {
for i in 0..3 {
pos[i] = pos[i].clamp(min_bound[i], max_bound[i]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soft_weight_centre() {
let w = soft_weight(0.0, 1.0, SoftFalloff::Smooth);
assert!((w - 1.0).abs() < 1e-6);
}
#[test]
fn test_soft_weight_boundary() {
let w = soft_weight(1.0, 1.0, SoftFalloff::Linear);
assert!(w < 1e-6);
}
#[test]
fn test_apply_tweak_moves_inside() {
let pts = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
let params = default_tweak_params(2.0, 1.0);
let res = apply_tweak(&pts, [0.0, 0.0, 0.0], ¶ms);
assert!(res.positions[0][1] > 0.0);
assert!((res.positions[1][1]).abs() < 1e-6);
}
#[test]
fn test_affected_count() {
let pts: Vec<[f32; 3]> = (0..5).map(|i| [i as f32, 0.0, 0.0]).collect();
let params = default_tweak_params(2.5, 0.5);
let res = apply_tweak(&pts, [0.0, 0.0, 0.0], ¶ms);
assert!(res.affected_count > 0 && res.affected_count <= 5);
}
#[test]
fn test_undo_tweak() {
let pts = vec![[0.0, 0.0, 0.0]];
let params = default_tweak_params(1.0, 2.0);
let moved = apply_tweak(&pts, [0.0, 0.0, 0.0], ¶ms);
assert!((moved.positions[0][1] - 2.0).abs() < 1e-5);
let restored = undo_tweak(&moved.positions, moved.positions[0], ¶ms);
assert!((restored.positions[0][1]).abs() < 1e-5);
}
#[test]
fn test_compute_soft_weights_length() {
let pts: Vec<[f32; 3]> = (0..4).map(|i| [i as f32, 0.0, 0.0]).collect();
let w = compute_soft_weights(&pts, [0.0; 3], 3.0, SoftFalloff::Smooth);
assert_eq!(w.len(), 4);
}
#[test]
fn test_count_selected() {
let pts = vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0], [5.0, 0.0, 0.0]];
let c = count_selected(&pts, [0.0; 3], 1.0);
assert_eq!(c, 2);
}
#[test]
fn test_scale_tweak_delta() {
let p = default_tweak_params(1.0, 2.0);
let scaled = scale_tweak_delta(&p, 0.5);
assert!((scaled.delta[1] - 1.0).abs() < 1e-6);
}
#[test]
fn test_clamp_tweak_result() {
let pts = vec![[0.0, 0.0, 0.0]];
let params = default_tweak_params(2.0, 10.0);
let mut res = apply_tweak(&pts, [0.0, 0.0, 0.0], ¶ms);
clamp_tweak_result(&mut res, [-5.0; 3], [5.0; 3]);
assert!(res.positions[0][1] <= 5.0);
}
}