#![allow(dead_code)]
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BrushMode {
Paint,
Smooth,
Erase,
Blur,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BrushConfig {
pub radius: f32,
pub strength: f32,
pub falloff_exponent: f32,
pub use_symmetry: bool,
pub mode: BrushMode,
}
impl Default for BrushConfig {
fn default() -> Self {
Self {
radius: 0.1,
strength: 0.5,
falloff_exponent: 2.0,
use_symmetry: false,
mode: BrushMode::Paint,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct BrushStroke {
pub points: Vec<[f32; 3]>,
pub radius: f32,
pub strength: f32,
pub mode: BrushMode,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PaintBrush {
pub config: BrushConfig,
pub strokes: Vec<BrushStroke>,
pub active_stroke: Option<BrushStroke>,
}
pub type AffectedVertices = Vec<(usize, f32)>;
#[allow(dead_code)]
pub fn default_brush_config() -> BrushConfig {
BrushConfig::default()
}
#[allow(dead_code)]
pub fn new_paint_brush(config: BrushConfig) -> PaintBrush {
PaintBrush {
config,
strokes: Vec::new(),
active_stroke: None,
}
}
#[allow(dead_code)]
pub fn begin_stroke(brush: &mut PaintBrush, point: [f32; 3]) {
let stroke = BrushStroke {
points: vec![point],
radius: brush.config.radius,
strength: brush.config.strength,
mode: brush.config.mode,
};
brush.active_stroke = Some(stroke);
}
#[allow(dead_code)]
pub fn continue_stroke(brush: &mut PaintBrush, point: [f32; 3]) {
if let Some(stroke) = brush.active_stroke.as_mut() {
stroke.points.push(point);
}
}
#[allow(dead_code)]
pub fn end_stroke(brush: &mut PaintBrush) {
if let Some(stroke) = brush.active_stroke.take() {
brush.strokes.push(stroke);
}
}
#[allow(dead_code)]
pub fn brush_affected_vertices(
positions: &[f32],
center: [f32; 3],
radius: f32,
falloff_exponent: f32,
) -> AffectedVertices {
let vc = positions.len() / 3;
let mut result = Vec::new();
for i in 0..vc {
let vx = positions[i * 3];
let vy = positions[i * 3 + 1];
let vz = positions[i * 3 + 2];
let dx = vx - center[0];
let dy = vy - center[1];
let dz = vz - center[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist <= radius {
let w = brush_falloff(dist, radius, falloff_exponent);
result.push((i, w));
}
}
result
}
#[allow(dead_code)]
pub fn brush_falloff(dist: f32, radius: f32, exponent: f32) -> f32 {
if radius <= 0.0 {
return 0.0;
}
let t = (dist / radius).clamp(0.0, 1.0);
(1.0 - t).powf(exponent)
}
#[allow(dead_code)]
pub fn apply_brush_stroke(
values: &mut [f32],
affected: &AffectedVertices,
strength: f32,
mode: BrushMode,
) {
for &(idx, weight) in affected {
if idx >= values.len() {
continue;
}
let delta = strength * weight;
match mode {
BrushMode::Paint => values[idx] = (values[idx] + delta).clamp(0.0, 1.0),
BrushMode::Erase => values[idx] = (values[idx] - delta).clamp(0.0, 1.0),
BrushMode::Smooth | BrushMode::Blur => {
values[idx] = (values[idx] * (1.0 - delta) + 0.5 * delta).clamp(0.0, 1.0)
}
}
}
}
#[allow(dead_code)]
pub fn stroke_count(brush: &PaintBrush) -> usize {
brush.strokes.len()
}
#[allow(dead_code)]
pub fn set_brush_mode(brush: &mut PaintBrush, mode: BrushMode) {
brush.config.mode = mode;
}
#[allow(dead_code)]
pub fn brush_radius(brush: &PaintBrush) -> f32 {
brush.config.radius
}
#[allow(dead_code)]
pub fn set_brush_radius(brush: &mut PaintBrush, radius: f32) {
brush.config.radius = radius.max(0.0);
}
#[allow(dead_code)]
pub fn brush_strength(brush: &PaintBrush) -> f32 {
brush.config.strength
}
#[allow(dead_code)]
pub fn paint_brush_to_json(brush: &PaintBrush) -> String {
format!(
r#"{{"radius":{:.4},"strength":{:.4},"falloff_exponent":{:.4},"mode":"{:?}","stroke_count":{}}}"#,
brush.config.radius,
brush.config.strength,
brush.config.falloff_exponent,
brush.config.mode,
brush.strokes.len()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_brush_config() {
let cfg = default_brush_config();
assert!((cfg.radius - 0.1).abs() < 1e-6);
assert!((cfg.strength - 0.5).abs() < 1e-6);
assert_eq!(cfg.mode, BrushMode::Paint);
assert!(!cfg.use_symmetry);
}
#[test]
fn test_new_paint_brush() {
let brush = new_paint_brush(default_brush_config());
assert!(brush.strokes.is_empty());
assert!(brush.active_stroke.is_none());
}
#[test]
fn test_begin_stroke() {
let mut brush = new_paint_brush(default_brush_config());
begin_stroke(&mut brush, [0.0, 0.0, 0.0]);
assert!(brush.active_stroke.is_some());
let pts = &brush.active_stroke.as_ref().expect("should succeed").points;
assert_eq!(pts.len(), 1);
}
#[test]
fn test_continue_stroke() {
let mut brush = new_paint_brush(default_brush_config());
begin_stroke(&mut brush, [0.0, 0.0, 0.0]);
continue_stroke(&mut brush, [1.0, 0.0, 0.0]);
continue_stroke(&mut brush, [2.0, 0.0, 0.0]);
assert_eq!(brush.active_stroke.as_ref().expect("should succeed").points.len(), 3);
}
#[test]
fn test_end_stroke_commits_to_history() {
let mut brush = new_paint_brush(default_brush_config());
begin_stroke(&mut brush, [0.0, 0.0, 0.0]);
end_stroke(&mut brush);
assert_eq!(stroke_count(&brush), 1);
assert!(brush.active_stroke.is_none());
}
#[test]
fn test_end_stroke_no_active_is_noop() {
let mut brush = new_paint_brush(default_brush_config());
end_stroke(&mut brush);
assert_eq!(stroke_count(&brush), 0);
}
#[test]
fn test_brush_falloff_center() {
let w = brush_falloff(0.0, 1.0, 2.0);
assert!((w - 1.0).abs() < 1e-6);
}
#[test]
fn test_brush_falloff_edge() {
let w = brush_falloff(1.0, 1.0, 2.0);
assert!(w < 1e-6);
}
#[test]
fn test_brush_falloff_zero_radius() {
let w = brush_falloff(0.0, 0.0, 2.0);
assert!((w).abs() < 1e-6);
}
#[test]
fn test_brush_affected_vertices() {
let pos = vec![0.0f32, 0.0, 0.0, 5.0, 0.0, 0.0];
let affected = brush_affected_vertices(&pos, [0.0, 0.0, 0.0], 1.0, 2.0);
assert_eq!(affected.len(), 1);
assert_eq!(affected[0].0, 0);
}
#[test]
fn test_apply_brush_stroke_paint() {
let mut values = vec![0.0f32; 3];
let affected = vec![(0, 1.0), (1, 0.5)];
apply_brush_stroke(&mut values, &affected, 0.5, BrushMode::Paint);
assert!((values[0] - 0.5).abs() < 1e-6);
assert!((values[1] - 0.25).abs() < 1e-6);
assert!((values[2]).abs() < 1e-6);
}
#[test]
fn test_apply_brush_stroke_erase() {
let mut values = vec![1.0f32; 2];
let affected = vec![(0, 1.0)];
apply_brush_stroke(&mut values, &affected, 0.5, BrushMode::Erase);
assert!((values[0] - 0.5).abs() < 1e-6);
}
#[test]
fn test_apply_brush_stroke_clamps() {
let mut values = vec![0.9f32];
let affected = vec![(0, 1.0)];
apply_brush_stroke(&mut values, &affected, 1.0, BrushMode::Paint);
assert!((values[0] - 1.0).abs() < 1e-6);
}
#[test]
fn test_set_brush_mode() {
let mut brush = new_paint_brush(default_brush_config());
set_brush_mode(&mut brush, BrushMode::Smooth);
assert_eq!(brush.config.mode, BrushMode::Smooth);
}
#[test]
fn test_set_brush_radius() {
let mut brush = new_paint_brush(default_brush_config());
set_brush_radius(&mut brush, 0.5);
assert!((brush_radius(&brush) - 0.5).abs() < 1e-6);
}
#[test]
fn test_set_brush_radius_clamp() {
let mut brush = new_paint_brush(default_brush_config());
set_brush_radius(&mut brush, -1.0);
assert!((brush_radius(&brush)).abs() < 1e-6);
}
#[test]
fn test_brush_strength_getter() {
let brush = new_paint_brush(default_brush_config());
assert!((brush_strength(&brush) - 0.5).abs() < 1e-6);
}
#[test]
fn test_paint_brush_to_json() {
let brush = new_paint_brush(default_brush_config());
let json = paint_brush_to_json(&brush);
assert!(json.contains("radius"));
assert!(json.contains("strength"));
assert!(json.contains("stroke_count"));
}
}