pub fn sdf_segment(px: f32, py: f32, ax: f32, ay: f32, bx: f32, by: f32) -> f32 {
let pax = px - ax;
let pay = py - ay;
let bax = bx - ax;
let bay = by - ay;
let dot_ba = bax * bax + bay * bay;
if dot_ba < 1e-12 {
return pax.hypot(pay);
}
let t = ((pax * bax + pay * bay) / dot_ba).clamp(0.0, 1.0);
let dx = pax - bax * t;
let dy = pay - bay * t;
dx.hypot(dy)
}
pub fn sdf_circle(px: f32, py: f32, cx: f32, cy: f32, r: f32) -> f32 {
let dx = px - cx;
let dy = py - cy;
dx.hypot(dy) - r
}
pub fn sdf_rect(px: f32, py: f32, cx: f32, cy: f32, half_w: f32, half_h: f32) -> f32 {
let dx = (px - cx).abs() - half_w;
let dy = (py - cy).abs() - half_h;
let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
let inside = dx.max(dy).min(0.0);
outside + inside
}
pub fn sdf_rounded_rect(
px: f32,
py: f32,
cx: f32,
cy: f32,
half_w: f32,
half_h: f32,
corner_r: f32,
) -> f32 {
let hw = half_w - corner_r;
let hh = half_h - corner_r;
let dx = (px - cx).abs() - hw;
let dy = (py - cy).abs() - hh;
let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
let inside = dx.max(dy).min(0.0);
outside + inside - corner_r
}
pub fn sdf_ellipse(px: f32, py: f32, cx: f32, cy: f32, rx: f32, ry: f32) -> f32 {
let dx = px - cx;
let dy = py - cy;
let f = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) - 1.0;
let gx = 2.0 * dx / (rx * rx);
let gy = 2.0 * dy / (ry * ry);
let grad_len = (gx * gx + gy * gy).sqrt();
if grad_len < 1e-12 {
return -rx.min(ry);
}
f / grad_len
}
pub fn sdf_polygon(px: f32, py: f32, vertices: &[[f32; 2]]) -> f32 {
let n = vertices.len();
if n < 3 {
return f32::MAX;
}
let d0x = px - vertices[0][0];
let d0y = py - vertices[0][1];
let mut d_sq = d0x * d0x + d0y * d0y;
let mut s = 1.0_f32;
let mut j = n - 1;
for i in 0..n {
let ex = vertices[j][0] - vertices[i][0];
let ey = vertices[j][1] - vertices[i][1];
let wx = px - vertices[i][0];
let wy = py - vertices[i][1];
let dot_we = wx * ex + wy * ey;
let dot_ee = ex * ex + ey * ey;
let t = if dot_ee > 1e-12 {
(dot_we / dot_ee).clamp(0.0, 1.0)
} else {
0.0
};
let bx = wx - ex * t;
let by = wy - ey * t;
d_sq = d_sq.min(bx * bx + by * by);
let c1 = py >= vertices[i][1];
let c2 = py < vertices[j][1];
let cross = ex * wy - ey * wx;
if (c1 && c2 && cross > 0.0) || (!c1 && !c2 && cross < 0.0) {
s = -s;
}
j = i;
}
s * d_sq.sqrt()
}
fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
pub fn sdf_coverage(sdf: f32, antialias: bool, feather: f32) -> f32 {
if feather > 0.0 {
let half = feather * 0.5;
smoothstep(half, -half, sdf)
} else if antialias {
smoothstep(0.5, -0.5, sdf)
} else {
if sdf <= 0.0 {
1.0
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-4;
#[test]
fn segment_perpendicular_distance() {
let d = sdf_segment(5.0, 3.0, 0.0, 0.0, 10.0, 0.0);
assert!((d - 3.0).abs() < EPS);
}
#[test]
fn segment_endpoint_distance() {
let d = sdf_segment(12.0, 0.0, 0.0, 0.0, 10.0, 0.0);
assert!((d - 2.0).abs() < EPS);
}
#[test]
fn segment_on_segment() {
let d = sdf_segment(5.0, 0.0, 0.0, 0.0, 10.0, 0.0);
assert!(d < EPS);
}
#[test]
fn circle_inside() {
let d = sdf_circle(5.0, 5.0, 5.0, 5.0, 10.0);
assert!((d - (-10.0)).abs() < EPS); }
#[test]
fn circle_boundary() {
let d = sdf_circle(15.0, 5.0, 5.0, 5.0, 10.0);
assert!(d.abs() < EPS);
}
#[test]
fn circle_outside() {
let d = sdf_circle(20.0, 5.0, 5.0, 5.0, 10.0);
assert!((d - 5.0).abs() < EPS);
}
#[test]
fn rect_inside() {
let d = sdf_rect(5.0, 5.0, 5.0, 5.0, 10.0, 8.0);
assert!(d < 0.0); assert!((d - (-8.0)).abs() < EPS); }
#[test]
fn rect_edge() {
let d = sdf_rect(15.0, 5.0, 5.0, 5.0, 10.0, 8.0);
assert!(d.abs() < EPS);
}
#[test]
fn rect_outside() {
let d = sdf_rect(20.0, 5.0, 5.0, 5.0, 10.0, 8.0);
assert!((d - 5.0).abs() < EPS);
}
#[test]
fn rect_corner_outside() {
let d = sdf_rect(16.0, 14.0, 5.0, 5.0, 10.0, 8.0);
assert!((d - std::f32::consts::SQRT_2).abs() < EPS);
}
#[test]
fn ellipse_center() {
let d = sdf_ellipse(5.0, 5.0, 5.0, 5.0, 10.0, 6.0);
assert!(d < 0.0); }
#[test]
fn ellipse_on_major_axis() {
let d = sdf_ellipse(15.0, 5.0, 5.0, 5.0, 10.0, 6.0);
assert!(d.abs() < 0.1); }
#[test]
fn ellipse_on_minor_axis() {
let d = sdf_ellipse(5.0, 11.0, 5.0, 5.0, 10.0, 6.0);
assert!(d.abs() < 0.1); }
#[test]
fn ellipse_outside() {
let d = sdf_ellipse(20.0, 5.0, 5.0, 5.0, 10.0, 6.0);
assert!(d > 0.0);
}
#[test]
fn polygon_triangle_inside() {
let verts = [[0.0, 0.0], [10.0, 0.0], [5.0, 10.0]];
let d = sdf_polygon(5.0, 3.0, &verts);
assert!(
d < 0.0,
"point inside triangle should have negative SDF, got {d}"
);
}
#[test]
fn polygon_triangle_outside() {
let verts = [[0.0, 0.0], [10.0, 0.0], [5.0, 10.0]];
let d = sdf_polygon(0.0, 10.0, &verts);
assert!(
d > 0.0,
"point outside triangle should have positive SDF, got {d}"
);
}
#[test]
fn polygon_triangle_vertex() {
let verts = [[0.0, 0.0], [10.0, 0.0], [5.0, 10.0]];
let d = sdf_polygon(0.0, 0.0, &verts);
assert!(d.abs() < EPS, "distance at vertex should be ~0, got {d}");
}
#[test]
fn polygon_square_inside() {
let verts = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
let d = sdf_polygon(5.0, 5.0, &verts);
assert!(d < 0.0);
assert!(
(d - (-5.0)).abs() < EPS,
"center of 10x10 square: expected -5, got {d}"
);
}
#[test]
fn polygon_square_edge() {
let verts = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
let d = sdf_polygon(10.0, 5.0, &verts);
assert!(d.abs() < EPS, "on edge should be ~0, got {d}");
}
#[test]
fn coverage_hard_edge() {
assert_eq!(sdf_coverage(-1.0, false, 0.0), 1.0);
assert_eq!(sdf_coverage(0.0, false, 0.0), 1.0); assert_eq!(sdf_coverage(0.01, false, 0.0), 0.0);
}
#[test]
fn coverage_antialiased() {
assert_eq!(sdf_coverage(-1.0, true, 0.0), 1.0); assert_eq!(sdf_coverage(1.0, true, 0.0), 0.0); let c = sdf_coverage(0.0, true, 0.0);
assert!((c - 0.5).abs() < EPS, "on boundary should be ~0.5, got {c}");
}
#[test]
fn coverage_feathered() {
assert_eq!(sdf_coverage(-10.0, false, 4.0), 1.0); assert_eq!(sdf_coverage(10.0, false, 4.0), 0.0); let c = sdf_coverage(0.0, false, 4.0);
assert!((c - 0.5).abs() < EPS, "on boundary should be ~0.5, got {c}");
let c2 = sdf_coverage(2.0, false, 4.0);
assert!(c2 < EPS);
let c3 = sdf_coverage(-2.0, false, 4.0);
assert!((c3 - 1.0).abs() < EPS);
}
#[test]
fn rounded_rect_zero_radius_matches_rect() {
for &(px, py) in &[(5.0, 5.0), (15.0, 5.0), (20.0, 5.0), (16.0, 14.0)] {
let d_rect = sdf_rect(px, py, 5.0, 5.0, 10.0, 8.0);
let d_rr = sdf_rounded_rect(px, py, 5.0, 5.0, 10.0, 8.0, 0.0);
assert!(
(d_rect - d_rr).abs() < EPS,
"mismatch at ({px}, {py}): rect={d_rect}, rounded={d_rr}"
);
}
}
}