use rayon::prelude::*;
#[derive(Debug)]
pub enum SdfError {
InvalidInput(String),
ZeroSize,
InvalidFont,
InvalidData(String),
Io(String),
}
impl std::fmt::Display for SdfError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SdfError::InvalidInput(s) => write!(f, "invalid input: {s}"),
SdfError::ZeroSize => write!(f, "SDF dimensions must be non-zero"),
SdfError::InvalidFont => write!(f, "could not parse font data"),
SdfError::InvalidData(s) => write!(f, "invalid SDF binary data: {s}"),
SdfError::Io(s) => write!(f, "I/O error: {s}"),
}
}
}
impl std::error::Error for SdfError {}
#[cfg_attr(feature = "simd", allow(dead_code))]
pub(crate) fn edt_1d(f: &mut [f32]) {
let n = f.len();
if n == 0 {
return;
}
let inf = f32::INFINITY;
let mut d = vec![0.0f32; n];
let mut v = vec![0usize; n];
let mut z = vec![0.0f32; n + 1];
let mut k = 0usize;
v[0] = 0;
z[0] = -inf;
z[1] = inf;
for q in 1..n {
let fq = f[q];
loop {
let r = v[k];
let fr = f[r];
let s =
((fq + (q * q) as f32) - (fr + (r * r) as f32)) / (2.0 * q as f32 - 2.0 * r as f32);
if s <= z[k] {
if k == 0 {
break;
}
k -= 1;
} else {
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = inf;
break;
}
}
}
k = 0;
for (q, slot) in d.iter_mut().enumerate().take(n) {
while z[k + 1] < q as f32 {
k += 1;
}
let r = v[k];
let dr = q as f32 - r as f32;
*slot = dr * dr + f[r];
}
f.copy_from_slice(&d);
}
#[cfg(feature = "simd")]
pub(crate) fn edt_1d_simd(f: &mut [f32]) {
use wide::f32x4;
let n = f.len();
if n == 0 {
return;
}
let inf = f32::INFINITY;
let mut v = vec![0usize; n];
let mut z = vec![0.0f32; n + 1];
let mut k = 0usize;
v[0] = 0;
z[0] = -inf;
z[1] = inf;
for q in 1..n {
let fq = f[q];
loop {
let r = v[k];
let fr = f[r];
let s =
((fq + (q * q) as f32) - (fr + (r * r) as f32)) / (2.0 * q as f32 - 2.0 * r as f32);
if s <= z[k] {
if k == 0 {
break;
}
k -= 1;
} else {
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = inf;
break;
}
}
}
let mut out = vec![0.0f32; n];
let mut k_cursor = 0usize;
let chunks = n / 4;
let remainder_start = chunks * 4;
let mut q = 0usize;
for _c in 0..chunks {
while z[k_cursor + 1] < q as f32 {
k_cursor += 1;
}
let r0 = v[k_cursor];
let d0 = {
let dr = q as f32 - r0 as f32;
dr * dr + f[r0]
};
let mut kk = k_cursor;
let r_vals = [
r0,
{
while z[kk + 1] < (q + 1) as f32 {
kk += 1;
}
v[kk]
},
{
while z[kk + 1] < (q + 2) as f32 {
kk += 1;
}
v[kk]
},
{
while z[kk + 1] < (q + 3) as f32 {
kk += 1;
}
v[kk]
},
];
k_cursor = kk;
let q_vec = f32x4::from([q as f32, (q + 1) as f32, (q + 2) as f32, (q + 3) as f32]);
let r_vec = f32x4::from([
r_vals[0] as f32,
r_vals[1] as f32,
r_vals[2] as f32,
r_vals[3] as f32,
]);
let fr_vec = f32x4::from([f[r_vals[0]], f[r_vals[1]], f[r_vals[2]], f[r_vals[3]]]);
let diff = q_vec - r_vec;
let dist = diff * diff + fr_vec;
let arr: [f32; 4] = dist.into();
let _ = d0; out[q] = arr[0];
out[q + 1] = arr[1];
out[q + 2] = arr[2];
out[q + 3] = arr[3];
q += 4;
}
for (offset, slot) in out[remainder_start..n].iter_mut().enumerate() {
let qq = remainder_start + offset;
while z[k_cursor + 1] < qq as f32 {
k_cursor += 1;
}
let r = v[k_cursor];
let dr = qq as f32 - r as f32;
*slot = dr * dr + f[r];
}
f.copy_from_slice(&out);
}
#[inline]
fn edt_1d_dispatch(f: &mut [f32]) {
#[cfg(feature = "simd")]
edt_1d_simd(f);
#[cfg(not(feature = "simd"))]
edt_1d(f);
}
pub fn edt_2d(grid: &[f32], width: usize, height: usize) -> Vec<f32> {
let mut row_result: Vec<f32> = grid.to_vec();
row_result.par_chunks_mut(width).for_each(|row| {
edt_1d_dispatch(row);
});
let mut transposed: Vec<f32> = vec![0.0; width * height];
for y in 0..height {
for x in 0..width {
transposed[x * height + y] = row_result[y * width + x];
}
}
transposed.par_chunks_mut(height).for_each(|col| {
edt_1d_dispatch(col);
});
for x in 0..width {
for y in 0..height {
row_result[y * width + x] = transposed[x * height + y];
}
}
row_result
}
pub fn compute_sdf(
coverage: &[u8],
width: usize,
height: usize,
spread: f32,
padding: u32,
) -> Result<Vec<u8>, SdfError> {
if coverage.len() != width * height {
return Err(SdfError::InvalidInput(format!(
"coverage length {} != width({}) * height({})",
coverage.len(),
width,
height
)));
}
let pad = padding as usize;
let padded_w = width + 2 * pad;
let padded_h = height + 2 * pad;
let n_padded = padded_w * padded_h;
const LARGE: f32 = 1e10;
let mut inside_grid = vec![LARGE; n_padded];
let mut outside_grid = vec![LARGE; n_padded];
for y in 0..height {
for x in 0..width {
let src_idx = y * width + x;
let dst_idx = (y + pad) * padded_w + (x + pad);
if coverage[src_idx] > 127 {
inside_grid[dst_idx] = 0.0;
} else {
outside_grid[dst_idx] = 0.0;
}
}
}
for y in 0..padded_h {
for x in 0..padded_w {
let in_inner = x >= pad && x < pad + width && y >= pad && y < pad + height;
if !in_inner {
outside_grid[y * padded_w + x] = 0.0;
}
}
}
let dist_inside = edt_2d(&inside_grid, padded_w, padded_h);
let dist_outside = edt_2d(&outside_grid, padded_w, padded_h);
let out: Vec<u8> = (0..n_padded)
.map(|i| {
let d_in = dist_inside[i].sqrt();
let d_out = dist_outside[i].sqrt();
let signed = d_out - d_in;
let normalized = 0.5 + signed / (2.0 * spread);
let clamped = normalized.clamp(0.0, 1.0);
(clamped * 255.0).round() as u8
})
.collect();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solid_square_inside_everywhere() {
let w = 8usize;
let h = 8usize;
let coverage = vec![255u8; w * h];
let sdf = compute_sdf(&coverage, w, h, 4.0, 0).expect("compute_sdf solid square");
let center = sdf[(h / 2) * w + w / 2];
assert!(
center > 128,
"center of solid square should be inside (> 128), got {center}"
);
}
#[test]
fn empty_square_outside_everywhere() {
let w = 8usize;
let h = 8usize;
let coverage = vec![0u8; w * h];
let sdf = compute_sdf(&coverage, w, h, 4.0, 0).expect("compute_sdf empty square");
let center = sdf[(h / 2) * w + w / 2];
assert!(
center < 128,
"center of empty square should be outside (< 128), got {center}"
);
}
#[test]
fn ring_shape_inside_outside() {
let w = 8usize;
let h = 8usize;
let mut coverage = vec![0u8; w * h];
for x in 0..w {
coverage[x] = 255;
coverage[(h - 1) * w + x] = 255;
}
for y in 0..h {
coverage[y * w] = 255;
coverage[y * w + (w - 1)] = 255;
}
let sdf = compute_sdf(&coverage, w, h, 4.0, 0).expect("compute_sdf ring");
assert_eq!(sdf.len(), w * h, "sdf length should match w*h");
}
#[test]
fn padding_grows_output_size() {
let w = 4usize;
let h = 4usize;
let coverage = vec![128u8; w * h];
let sdf = compute_sdf(&coverage, w, h, 4.0, 2).expect("compute_sdf with padding");
assert_eq!(sdf.len(), (w + 4) * (h + 4));
}
#[test]
fn invalid_coverage_length_errors() {
let coverage = vec![128u8; 10];
assert!(compute_sdf(&coverage, 4, 4, 4.0, 0).is_err());
}
#[test]
fn test_edt_1d_simd_matches_scalar() {
#[cfg(feature = "simd")]
{
let input_scalar: Vec<f32> = (0..32)
.map(|i| if i % 8 == 0 { 0.0 } else { f32::INFINITY })
.collect();
let mut scalar = input_scalar.clone();
let mut simd_buf = input_scalar.clone();
edt_1d(&mut scalar);
edt_1d_simd(&mut simd_buf);
for (s, v) in scalar.iter().zip(simd_buf.iter()) {
assert!((s - v).abs() < 0.001, "scalar={s}, simd={v}");
}
}
}
fn filled_square_coverage(w: usize, h: usize, cx: usize, cy: usize, sq_side: usize) -> Vec<u8> {
let half = sq_side / 2;
let x_min = cx.saturating_sub(half);
let x_max = (cx + half + 1).min(w);
let y_min = cy.saturating_sub(half);
let y_max = (cy + half + 1).min(h);
let mut cov = vec![0u8; w * h];
for y in y_min..y_max {
for x in x_min..x_max {
cov[y * w + x] = 255;
}
}
cov
}
fn circle_coverage_edt(w: usize, h: usize, cx: f32, cy: f32, r: f32) -> Vec<u8> {
(0..w * h)
.map(|i| {
let x = (i % w) as f32;
let y = (i / w) as f32;
let d = ((x - cx).powi(2) + (y - cy).powi(2)).sqrt();
if d < r + 0.5 {
255u8
} else {
0u8
}
})
.collect()
}
#[test]
fn sdf_quality_filled_square_symmetry() {
let w = 32usize;
let h = 32usize;
let cx = 16usize;
let cy = 16usize;
let sq_side = 16usize;
let spread = 8.0f32;
let coverage = filled_square_coverage(w, h, cx, cy, sq_side);
let sdf = compute_sdf(&coverage, w, h, spread, 0).expect("compute_sdf filled square");
assert_eq!(sdf.len(), w * h);
let center_v = sdf[cy * w + cx];
assert!(
center_v > 128,
"center pixel should be inside (>128), got {center_v}"
);
let corner_v = sdf[0];
assert!(
corner_v < 128,
"corner pixel should be outside (<128), got {corner_v}"
);
let boundary_x = cx + sq_side / 2 + 1; let boundary_v = sdf[cy * w + boundary_x] as i32;
assert!(
(boundary_v - 128).abs() <= 20,
"boundary pixel at ({boundary_x},{cy}) should be within ±20 of 128 \
(first outside pixel on right edge), got {boundary_v}"
);
let offset = 4usize;
let sym_right = sdf[cy * w + (cx + offset)] as i32;
let sym_left = sdf[cy * w + (cx - offset)] as i32;
assert_eq!(
sym_right,
sym_left,
"4-fold SDF symmetry broken: sdf[{},{}]={sym_right} vs sdf[{},{}]={sym_left}",
cx + offset,
cy,
cx - offset,
cy
);
let sym_down = sdf[(cy + offset) * w + cx] as i32;
let sym_up = sdf[(cy - offset) * w + cx] as i32;
assert_eq!(
sym_down,
sym_up,
"4-fold SDF symmetry broken: sdf[{},{}]={sym_down} vs sdf[{},{}]={sym_up}",
cx,
cy + offset,
cx,
cy - offset
);
}
#[test]
fn sdf_quality_circular_coverage() {
let size = 64usize;
let cx = 32.0f32;
let cy = 32.0f32;
let r = 16.0f32;
let spread = 8.0f32;
let coverage = circle_coverage_edt(size, size, cx, cy, r);
let sdf = compute_sdf(&coverage, size, size, spread, 0).expect("compute_sdf circle");
assert_eq!(sdf.len(), size * size);
let center_v = sdf[32 * size + 32];
assert!(
center_v > 200,
"circle center should be far inside (>200), got {center_v}"
);
let y_inside = (cy + r - 1.0) as usize; let y_outside = (cy + r + 1.0) as usize; let inside_v = sdf[y_inside * size + 32] as i32;
let outside_v = sdf[y_outside * size + 32] as i32;
assert!(
inside_v >= 128,
"pixel just inside the circle boundary ({},32) should be >=128, got {inside_v}",
y_inside
);
assert!(
outside_v <= 128,
"pixel just outside the circle boundary ({},32) should be <=128, got {outside_v}",
y_outside
);
let y_far_out = (cy + r + 4.0) as usize; let far_out_v = sdf[y_far_out * size + 32] as i32;
assert!(
far_out_v < 128,
"pixel outside circle by ~4px ({y_far_out},32) should be <128, got {far_out_v}"
);
assert!(
(far_out_v - 64).abs() <= 25,
"pixel outside circle by ~4px should be near 64 (±25), got {far_out_v}"
);
}
}