#![allow(dead_code)]
use std::f32::consts::PI;
#[derive(Debug, Clone)]
pub struct FilletToolConfig {
pub radius: f32,
pub segments: usize,
}
impl Default for FilletToolConfig {
fn default() -> Self {
FilletToolConfig {
radius: 0.1,
segments: 4,
}
}
}
#[derive(Debug, Clone)]
pub struct FilletToolResult {
pub new_positions: Vec<[f32; 3]>,
pub new_indices: Vec<u32>,
pub filleted_edges: usize,
}
fn normalize3(v: [f32; 3]) -> [f32; 3] {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if l < 1e-10 {
return [0.0, 0.0, 0.0];
}
[v[0] / l, v[1] / l, v[2] / l]
}
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
[
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
]
}
pub fn arc_points(
centre: [f32; 3],
from_dir: [f32; 3],
to_dir: [f32; 3],
radius: f32,
segments: usize,
) -> Vec<[f32; 3]> {
let from = normalize3(from_dir);
let to = normalize3(to_dir);
let dot = dot3(from, to).clamp(-1.0, 1.0);
let angle = dot.acos();
let cross = cross3(from, to);
let cross_len = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
let mut pts = Vec::with_capacity(segments + 1);
for i in 0..=segments {
let t = i as f32 / segments as f32;
let a = angle * t;
if cross_len < 1e-8 {
pts.push([
centre[0] + from[0] * radius,
centre[1] + from[1] * radius,
centre[2] + from[2] * radius,
]);
} else {
let axis = [
cross[0] / cross_len,
cross[1] / cross_len,
cross[2] / cross_len,
];
let cos_a = a.cos();
let sin_a = a.sin();
let d = dot3(axis, from);
let rotated = [
from[0] * cos_a
+ (axis[1] * from[2] - axis[2] * from[1]) * sin_a
+ axis[0] * d * (1.0 - cos_a),
from[1] * cos_a
+ (axis[2] * from[0] - axis[0] * from[2]) * sin_a
+ axis[1] * d * (1.0 - cos_a),
from[2] * cos_a
+ (axis[0] * from[1] - axis[1] * from[0]) * sin_a
+ axis[2] * d * (1.0 - cos_a),
];
pts.push([
centre[0] + rotated[0] * radius,
centre[1] + rotated[1] * radius,
centre[2] + rotated[2] * radius,
]);
}
}
pts
}
pub fn fillet_edge_simple(
v0: [f32; 3],
v1: [f32; 3],
n0: [f32; 3],
n1: [f32; 3],
config: &FilletToolConfig,
) -> Vec<[f32; 3]> {
let mid = lerp3(v0, v1, 0.5);
let from_dir = normalize3(n0);
let to_dir = normalize3(n1);
arc_points(mid, from_dir, to_dir, config.radius, config.segments)
}
pub fn fillet_vertex_estimate(n_edges: usize, segments: usize) -> usize {
n_edges * (segments + 1)
}
pub fn fillet_arc_length(radius: f32, angle_rad: f32) -> f32 {
radius * angle_rad.abs()
}
pub fn fillet_radius_from_chamfer(chamfer: f32, dihedral_rad: f32) -> f32 {
let half = (PI - dihedral_rad) * 0.5;
chamfer / half.tan().max(1e-8)
}
pub fn validate_fillet_config(config: &FilletToolConfig) -> bool {
config.radius > 0.0 && config.segments > 0
}
pub fn apply_edge_fillet(
positions: &[[f32; 3]],
edge_pairs: &[(usize, usize)],
config: &FilletToolConfig,
) -> FilletToolResult {
let mut new_positions = positions.to_vec();
let mut new_indices: Vec<u32> = Vec::new();
let mut filleted_edges = 0usize;
for &(a, b) in edge_pairs {
if a >= positions.len() || b >= positions.len() {
continue;
}
let v0 = positions[a];
let v1 = positions[b];
let dir = normalize3([v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]]);
let perp = if dir[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
let n0 = normalize3(cross3(dir, perp));
let n1 = normalize3(cross3(perp, dir));
let arc = fillet_edge_simple(v0, v1, n0, n1, config);
let base = new_positions.len() as u32;
for (i, &ap) in arc.iter().enumerate() {
new_positions.push(ap);
if i + 1 < arc.len() {
new_indices.push(base + i as u32);
new_indices.push(base + i as u32 + 1);
}
}
filleted_edges += 1;
}
FilletToolResult {
new_positions,
new_indices,
filleted_edges,
}
}
pub fn default_fillet_tool_config() -> FilletToolConfig {
FilletToolConfig::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_arc_points_count() {
let pts = arc_points([0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.0, 4);
assert_eq!(pts.len(), 5);
}
#[test]
fn test_arc_points_first() {
let pts = arc_points([0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.0, 4);
assert!((pts[0][0] - 1.0).abs() < 1e-5);
}
#[test]
fn test_fillet_arc_length() {
let l = fillet_arc_length(1.0, PI / 2.0);
assert!((l - PI / 2.0).abs() < 1e-5);
}
#[test]
fn test_validate_fillet_config() {
let cfg = default_fillet_tool_config();
assert!(validate_fillet_config(&cfg));
let bad = FilletToolConfig {
radius: -1.0,
segments: 4,
};
assert!(!validate_fillet_config(&bad));
}
#[test]
fn test_fillet_vertex_estimate() {
assert_eq!(fillet_vertex_estimate(2, 4), 10);
}
#[test]
fn test_apply_edge_fillet() {
let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let cfg = default_fillet_tool_config();
let res = apply_edge_fillet(&pos, &[(0, 1)], &cfg);
assert_eq!(res.filleted_edges, 1);
assert!(res.new_positions.len() > 2);
}
#[test]
fn test_fillet_edge_simple() {
let cfg = default_fillet_tool_config();
let pts = fillet_edge_simple(
[0.0; 3],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
&cfg,
);
assert_eq!(pts.len(), cfg.segments + 1);
}
#[test]
fn test_fillet_radius_from_chamfer() {
let r = fillet_radius_from_chamfer(1.0, PI / 2.0);
assert!(r > 0.0);
}
}