use crate::atlas::SdfTile;
use crate::edt::SdfError;
use crate::msdf::{extract_glyph_shape, segment_signed_dist, GlyphShape, Point, Segment};
#[inline]
fn eval_quad(p0: (f32, f32), p1: (f32, f32), p2: (f32, f32), t: f32) -> (f32, f32) {
let u = 1.0 - t;
(
u * u * p0.0 + 2.0 * u * t * p1.0 + t * t * p2.0,
u * u * p0.1 + 2.0 * u * t * p1.1 + t * t * p2.1,
)
}
#[inline]
fn eval_cubic(
p0: (f32, f32),
p1: (f32, f32),
p2: (f32, f32),
p3: (f32, f32),
t: f32,
) -> (f32, f32) {
let u = 1.0 - t;
(
u * u * u * p0.0 + 3.0 * u * u * t * p1.0 + 3.0 * u * t * t * p2.0 + t * t * t * p3.0,
u * u * u * p0.1 + 3.0 * u * u * t * p1.1 + 3.0 * u * t * t * p2.1 + t * t * t * p3.1,
)
}
#[inline]
fn cross2d(ax: f32, ay: f32, bx: f32, by: f32, px: f32, py: f32) -> f32 {
(bx - ax) * (py - ay) - (by - ay) * (px - ax)
}
fn quad_winding(p0: Point, p1: Point, p2: Point, px: f32, py: f32) -> i32 {
const N: usize = 8;
let mut winding = 0i32;
let p0f = (p0.x as f32, p0.y as f32);
let p1f = (p1.x as f32, p1.y as f32);
let p2f = (p2.x as f32, p2.y as f32);
for i in 0..N {
let t0 = i as f32 / N as f32;
let t1 = (i + 1) as f32 / N as f32;
let a = eval_quad(p0f, p1f, p2f, t0);
let b = eval_quad(p0f, p1f, p2f, t1);
if a.1 <= py {
if b.1 > py && cross2d(a.0, a.1, b.0, b.1, px, py) > 0.0 {
winding += 1;
}
} else if b.1 <= py && cross2d(a.0, a.1, b.0, b.1, px, py) < 0.0 {
winding -= 1;
}
}
winding
}
fn cubic_winding(p0: Point, c1: Point, c2: Point, p1: Point, px: f32, py: f32) -> i32 {
const N: usize = 8;
let mut winding = 0i32;
let p0f = (p0.x as f32, p0.y as f32);
let c1f = (c1.x as f32, c1.y as f32);
let c2f = (c2.x as f32, c2.y as f32);
let p1f = (p1.x as f32, p1.y as f32);
for i in 0..N {
let t0 = i as f32 / N as f32;
let t1 = (i + 1) as f32 / N as f32;
let a = eval_cubic(p0f, c1f, c2f, p1f, t0);
let b = eval_cubic(p0f, c1f, c2f, p1f, t1);
if a.1 <= py {
if b.1 > py && cross2d(a.0, a.1, b.0, b.1, px, py) > 0.0 {
winding += 1;
}
} else if b.1 <= py && cross2d(a.0, a.1, b.0, b.1, px, py) < 0.0 {
winding -= 1;
}
}
winding
}
fn winding_number(point: (f32, f32), shape: &GlyphShape) -> i32 {
let mut winding = 0i32;
let (px, py) = point;
for contour in &shape.contours {
for seg in &contour.segments {
match seg.segment {
Segment::Line(a, b) => {
let ay = a.y as f32;
let by = b.y as f32;
if ay <= py {
if by > py && cross2d(a.x as f32, ay, b.x as f32, by, px, py) > 0.0 {
winding += 1;
}
} else if by <= py && cross2d(a.x as f32, ay, b.x as f32, by, px, py) < 0.0 {
winding -= 1;
}
}
Segment::Quad(p0, p1, p2) => {
winding += quad_winding(p0, p1, p2, px, py);
}
Segment::Cubic(p0, c1, c2, p1) => {
winding += cubic_winding(p0, c1, c2, p1, px, py);
}
}
}
}
winding
}
fn analytic_sdf_at_point(point: (f32, f32), shape: &GlyphShape) -> f64 {
let (px, py) = point;
let mut min_abs = f64::MAX;
for contour in &shape.contours {
for seg in &contour.segments {
let d = segment_signed_dist(&seg.segment, px as f64, py as f64);
if d.abs() < min_abs {
min_abs = d.abs();
}
}
}
let w = winding_number(point, shape);
if w != 0 {
min_abs
} else {
-min_abs
}
}
pub fn glyph_to_sdf_tile_analytic(
face_data: &[u8],
glyph_id: u16,
px_size: f32,
tile_size: u32,
spread: f32,
) -> Result<Option<SdfTile>, SdfError> {
if tile_size == 0 {
return Err(SdfError::ZeroSize);
}
let face = ttf_parser::Face::parse(face_data, 0).map_err(|_| SdfError::InvalidFont)?;
let gid = ttf_parser::GlyphId(glyph_id);
let Some(shape) = extract_glyph_shape(face_data, glyph_id) else {
return Ok(None);
};
if shape.contours.is_empty() {
return Ok(None);
}
let units_per_em = shape.units_per_em;
let scale = px_size / units_per_em;
let bearing_x = face.glyph_hor_side_bearing(gid).unwrap_or(0) as i32;
let bearing_y = face.ascender() as i32;
let advance_x = face.glyph_hor_advance(gid).unwrap_or(0) as f32 * scale;
let bbox = face.glyph_bounding_box(gid).ok_or(SdfError::ZeroSize)?;
let shape_w = (bbox.x_max - bbox.x_min) as f32;
let shape_h = (bbox.y_max - bbox.y_min) as f32;
let border_du = spread / scale; let fit_scale_x = tile_size as f32 / (shape_w + 2.0 * border_du);
let fit_scale_y = tile_size as f32 / (shape_h + 2.0 * border_du);
let fit_scale = fit_scale_x.min(fit_scale_y).max(1e-6);
let offset_x = bbox.x_min as f32 - border_du;
let offset_y = bbox.y_min as f32 - border_du;
let n = tile_size as usize;
let mut data = vec![0u8; n * n];
let spread_du = spread / fit_scale;
for py in 0..n {
for px in 0..n {
let sx = px as f32 / fit_scale + offset_x;
let sy = (n - 1 - py) as f32 / fit_scale + offset_y;
let signed_dist = analytic_sdf_at_point((sx, sy), &shape);
let normalized = 0.5 + signed_dist / (2.0 * spread_du as f64);
let clamped = normalized.clamp(0.0, 1.0);
data[py * n + px] = (clamped * 255.0).round() as u8;
}
}
Ok(Some(SdfTile {
glyph_id,
width: tile_size,
height: tile_size,
data,
bearing_x,
bearing_y,
advance_x,
}))
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_FONT: &[u8] = include_bytes!("../../../tests/fixtures/test-font.ttf");
#[test]
fn test_analytic_sdf_non_panic() {
let result = glyph_to_sdf_tile_analytic(TEST_FONT, 36, 32.0, 32, 4.0);
assert!(result.is_ok(), "unexpected error: {:?}", result.err());
}
#[test]
fn test_analytic_sdf_glyph_0_ok() {
let result = glyph_to_sdf_tile_analytic(TEST_FONT, 0, 16.0, 16, 4.0);
assert!(result.is_ok(), "unexpected error: {:?}", result.err());
}
#[test]
fn test_analytic_sdf_tile_dimensions() {
let tile = glyph_to_sdf_tile_analytic(TEST_FONT, 36, 32.0, 32, 4.0)
.expect("should not error")
.expect("glyph 36 should have an outline");
assert_eq!(tile.width, 32);
assert_eq!(tile.height, 32);
assert_eq!(tile.data.len(), 32 * 32);
}
#[test]
fn test_analytic_sdf_has_inside_and_outside() {
let tile = glyph_to_sdf_tile_analytic(TEST_FONT, 36, 64.0, 64, 8.0)
.expect("should not error")
.expect("glyph 36 should have an outline");
assert_eq!(tile.data.len(), 64 * 64);
let has_inside = tile.data.iter().any(|&v| v > 128);
let has_outside = tile.data.iter().any(|&v| v < 128);
assert!(
has_inside && has_outside,
"SDF tile should have both inside (>128) and outside (<128) pixels"
);
}
#[test]
fn test_analytic_sdf_zero_tile_size_errors() {
let result = glyph_to_sdf_tile_analytic(TEST_FONT, 36, 32.0, 0, 4.0);
assert!(
matches!(result, Err(SdfError::ZeroSize)),
"zero tile_size should return ZeroSize error"
);
}
#[test]
fn test_analytic_sdf_invalid_font_errors() {
let bad = b"not a font";
let result = glyph_to_sdf_tile_analytic(bad, 0, 16.0, 16, 4.0);
assert!(
matches!(result, Err(SdfError::InvalidFont)),
"bad font bytes should return InvalidFont error"
);
}
#[test]
fn test_analytic_sdf_tile_packable() {
let tile = glyph_to_sdf_tile_analytic(TEST_FONT, 36, 32.0, 32, 4.0)
.expect("should not error")
.expect("glyph 36 should have an outline");
let atlas = crate::SdfAtlas::pack(&[tile]);
assert!(
atlas.uv_map.contains_key(&36),
"packed atlas should contain glyph 36"
);
}
}