use std::collections::HashMap;
use crate::edt::SdfError;
#[derive(Clone, Debug)]
pub struct SdfTile {
pub glyph_id: u16,
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
pub bearing_x: i32,
pub bearing_y: i32,
pub advance_x: f32,
}
impl SdfTile {
#[allow(clippy::too_many_arguments)]
pub fn from_coverage(
glyph_id: u16,
coverage: &[f32],
width: usize,
height: usize,
spread: f32,
bearing_x: i32,
bearing_y: i32,
advance_x: f32,
) -> Result<Self, SdfError> {
if width == 0 || height == 0 {
return Err(SdfError::ZeroSize);
}
if coverage.len() != width * height {
return Err(SdfError::InvalidInput(format!(
"coverage length {} != width({}) * height({})",
coverage.len(),
width,
height
)));
}
let coverage_u8: Vec<u8> = coverage
.iter()
.map(|&v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
.collect();
let sdf_data = crate::edt::compute_sdf(&coverage_u8, width, height, spread, 0)?;
Ok(Self {
glyph_id,
width: width as u32,
height: height as u32,
data: sdf_data,
bearing_x,
bearing_y,
advance_x,
})
}
}
#[derive(Clone, Debug)]
pub struct UvRect {
pub u_min: f32,
pub v_min: f32,
pub u_max: f32,
pub v_max: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PackingAlgorithm {
#[default]
Shelf,
MaxRects,
Skyline,
}
#[derive(Debug, Clone)]
pub struct AtlasOptions {
pub atlas_size: u32,
pub padding: u32,
pub max_size: Option<u32>,
pub algorithm: PackingAlgorithm,
}
impl Default for AtlasOptions {
fn default() -> Self {
Self {
atlas_size: 512,
padding: 1,
max_size: None,
algorithm: PackingAlgorithm::Shelf,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AtlasStats {
pub tiles_packed: usize,
pub tiles_dropped: usize,
pub utilization: f32,
pub wasted_pixels: u32,
}
struct PackResult {
texture: Vec<u8>,
uv_map: HashMap<u16, UvRect>,
dropped: usize,
used_pixels: u32,
cursor_x: u32,
shelf_y: u32,
shelf_max_h: u32,
}
fn pack_inner(tiles: &[SdfTile], atlas_w: u32, atlas_h: u32, padding: u32) -> PackResult {
let tex_len = atlas_w as usize * atlas_h as usize;
let mut texture = vec![0u8; tex_len];
let mut uv_map: HashMap<u16, UvRect> = HashMap::new();
let mut cx: u32 = padding;
let mut cy: u32 = padding;
let mut row_h: u32 = 0;
let mut dropped: usize = 0;
let mut used_pixels: u32 = 0;
for tile in tiles {
let needed_w = tile.width + padding;
if cx + tile.width > atlas_w.saturating_sub(padding) {
cx = padding;
cy += row_h + padding;
row_h = 0;
}
if cy + tile.height > atlas_h.saturating_sub(padding) {
dropped += 1;
continue;
}
for y in 0..tile.height {
for x in 0..tile.width {
let src_idx = (y * tile.width + x) as usize;
let dst_idx = ((cy + y) * atlas_w + (cx + x)) as usize;
if dst_idx < texture.len() && src_idx < tile.data.len() {
texture[dst_idx] = tile.data[src_idx];
}
}
}
uv_map.insert(
tile.glyph_id,
UvRect {
u_min: cx as f32 / atlas_w as f32,
v_min: cy as f32 / atlas_h as f32,
u_max: (cx + tile.width) as f32 / atlas_w as f32,
v_max: (cy + tile.height) as f32 / atlas_h as f32,
},
);
used_pixels += tile.width * tile.height;
cx += needed_w;
row_h = row_h.max(tile.height);
}
PackResult {
texture,
uv_map,
dropped,
used_pixels,
cursor_x: cx,
shelf_y: cy,
shelf_max_h: row_h,
}
}
#[derive(Clone, Copy, Debug)]
struct Rect {
x: u32,
y: u32,
w: u32,
h: u32,
}
impl Rect {
fn contains(&self, other: &Rect) -> bool {
other.x >= self.x
&& other.y >= self.y
&& other.x + other.w <= self.x + self.w
&& other.y + other.h <= self.y + self.h
}
}
struct MaxRectsState {
free_rects: Vec<Rect>,
}
impl MaxRectsState {
fn new(atlas_w: u32, atlas_h: u32) -> Self {
Self {
free_rects: vec![Rect {
x: 0,
y: 0,
w: atlas_w,
h: atlas_h,
}],
}
}
}
fn insert_maxrects(
state: &mut MaxRectsState,
tile_w: u32,
tile_h: u32,
padding: u32,
) -> Option<Rect> {
let needed_w = tile_w + padding;
let needed_h = tile_h + padding;
let best_idx = state
.free_rects
.iter()
.enumerate()
.filter(|(_, r)| r.w >= needed_w && r.h >= needed_h)
.min_by_key(|(_, r)| {
let leftover_w = r.w.saturating_sub(needed_w);
let leftover_h = r.h.saturating_sub(needed_h);
leftover_w.min(leftover_h)
})
.map(|(i, _)| i);
let idx = best_idx?;
let chosen = state.free_rects[idx];
let placed = Rect {
x: chosen.x,
y: chosen.y,
w: tile_w,
h: tile_h,
};
let mut new_rects: Vec<Rect> = Vec::with_capacity(4);
if chosen.x + chosen.w > placed.x + needed_w {
let nr = Rect {
x: placed.x + needed_w,
y: chosen.y,
w: chosen.x + chosen.w - (placed.x + needed_w),
h: chosen.h,
};
if nr.w > 0 && nr.h > 0 {
new_rects.push(nr);
}
}
if chosen.y + chosen.h > placed.y + needed_h {
let nr = Rect {
x: chosen.x,
y: placed.y + needed_h,
w: chosen.w,
h: chosen.y + chosen.h - (placed.y + needed_h),
};
if nr.w > 0 && nr.h > 0 {
new_rects.push(nr);
}
}
if placed.x > chosen.x {
let nr = Rect {
x: chosen.x,
y: chosen.y,
w: placed.x - chosen.x,
h: chosen.h,
};
if nr.w > 0 && nr.h > 0 {
new_rects.push(nr);
}
}
if placed.y > chosen.y {
let nr = Rect {
x: chosen.x,
y: chosen.y,
w: chosen.w,
h: placed.y - chosen.y,
};
if nr.w > 0 && nr.h > 0 {
new_rects.push(nr);
}
}
state.free_rects.remove(idx);
state.free_rects.extend_from_slice(&new_rects);
let mut to_remove: Vec<bool> = vec![false; state.free_rects.len()];
let len = state.free_rects.len();
for i in 0..len {
if to_remove[i] {
continue;
}
for j in 0..len {
if i == j || to_remove[j] {
continue;
}
if state.free_rects[j].contains(&state.free_rects[i]) {
to_remove[i] = true;
break;
}
}
}
let mut keep_idx = 0;
state.free_rects.retain(|_| {
let keep = !to_remove[keep_idx];
keep_idx += 1;
keep
});
Some(placed)
}
fn pack_inner_maxrects(tiles: &[SdfTile], atlas_w: u32, atlas_h: u32, padding: u32) -> PackResult {
let tex_len = atlas_w as usize * atlas_h as usize;
let mut texture = vec![0u8; tex_len];
let mut uv_map: HashMap<u16, UvRect> = HashMap::new();
let mut dropped: usize = 0;
let mut used_pixels: u32 = 0;
let mut state = MaxRectsState::new(atlas_w, atlas_h);
for tile in tiles {
match insert_maxrects(&mut state, tile.width, tile.height, padding) {
None => {
dropped += 1;
}
Some(placed) => {
for y in 0..tile.height {
for x in 0..tile.width {
let src_idx = (y * tile.width + x) as usize;
let dst_idx = ((placed.y + y) * atlas_w + (placed.x + x)) as usize;
if dst_idx < texture.len() && src_idx < tile.data.len() {
texture[dst_idx] = tile.data[src_idx];
}
}
}
uv_map.insert(
tile.glyph_id,
UvRect {
u_min: placed.x as f32 / atlas_w as f32,
v_min: placed.y as f32 / atlas_h as f32,
u_max: (placed.x + placed.w) as f32 / atlas_w as f32,
v_max: (placed.y + placed.h) as f32 / atlas_h as f32,
},
);
used_pixels += tile.width * tile.height;
}
}
}
PackResult {
texture,
uv_map,
dropped,
used_pixels,
cursor_x: 0,
shelf_y: 0,
shelf_max_h: 0,
}
}
struct SkylineSegment {
x: u32,
y_top: u32,
width: u32,
}
struct SkylineState {
segments: Vec<SkylineSegment>,
atlas_w: u32,
atlas_h: u32,
}
impl SkylineState {
fn new(atlas_w: u32, atlas_h: u32) -> Self {
Self {
segments: vec![SkylineSegment {
x: 0,
y_top: 0,
width: atlas_w,
}],
atlas_w,
atlas_h,
}
}
}
fn insert_skyline(
state: &mut SkylineState,
tile_w: u32,
tile_h: u32,
padding: u32,
) -> Option<(u32, u32)> {
let needed_w = tile_w + padding;
let n = state.segments.len();
let mut best_y = u32::MAX;
let mut best_x = 0u32;
'outer: for i in 0..n {
let x_start = state.segments[i].x;
if x_start + needed_w > state.atlas_w.saturating_sub(padding) {
continue;
}
let mut max_y = 0u32;
let mut covered_w = 0u32;
for j in i..n {
let seg = &state.segments[j];
max_y = max_y.max(seg.y_top);
covered_w += seg.width;
if covered_w >= needed_w {
if max_y + tile_h + padding <= state.atlas_h && max_y < best_y {
best_y = max_y;
best_x = x_start;
}
continue 'outer;
}
}
}
if best_y == u32::MAX {
return None; }
let place_x = best_x;
let place_y = best_y;
let new_top = best_y + tile_h + padding;
let tile_right = place_x + needed_w;
let mut new_segments: Vec<SkylineSegment> = Vec::with_capacity(state.segments.len() + 2);
for seg in &state.segments {
let seg_right = seg.x + seg.width;
if seg_right <= place_x || seg.x >= tile_right {
new_segments.push(SkylineSegment {
x: seg.x,
y_top: seg.y_top,
width: seg.width,
});
} else {
if seg.x < place_x {
new_segments.push(SkylineSegment {
x: seg.x,
y_top: seg.y_top,
width: place_x - seg.x,
});
}
}
}
new_segments.push(SkylineSegment {
x: place_x,
y_top: new_top,
width: needed_w,
});
for seg in &state.segments {
let seg_right = seg.x + seg.width;
if seg.x < tile_right && seg_right > tile_right {
new_segments.push(SkylineSegment {
x: tile_right,
y_top: seg.y_top,
width: seg_right - tile_right,
});
}
}
new_segments.sort_by_key(|s| s.x);
let mut merged: Vec<SkylineSegment> = Vec::with_capacity(new_segments.len());
for seg in new_segments {
if let Some(last) = merged.last_mut() {
if last.y_top == seg.y_top && last.x + last.width == seg.x {
last.width += seg.width;
continue;
}
}
merged.push(seg);
}
state.segments = merged;
Some((place_x, place_y))
}
fn pack_inner_skyline(tiles: &[SdfTile], atlas_w: u32, atlas_h: u32, padding: u32) -> PackResult {
let tex_len = atlas_w as usize * atlas_h as usize;
let mut texture = vec![0u8; tex_len];
let mut uv_map: HashMap<u16, UvRect> = HashMap::new();
let mut dropped: usize = 0;
let mut used_pixels: u32 = 0;
let mut state = SkylineState::new(atlas_w, atlas_h);
for tile in tiles {
match insert_skyline(&mut state, tile.width, tile.height, padding) {
None => {
dropped += 1;
}
Some((px, py)) => {
for y in 0..tile.height {
for x in 0..tile.width {
let src_idx = (y * tile.width + x) as usize;
let dst_idx = ((py + y) * atlas_w + (px + x)) as usize;
if dst_idx < texture.len() && src_idx < tile.data.len() {
texture[dst_idx] = tile.data[src_idx];
}
}
}
uv_map.insert(
tile.glyph_id,
UvRect {
u_min: px as f32 / atlas_w as f32,
v_min: py as f32 / atlas_h as f32,
u_max: (px + tile.width) as f32 / atlas_w as f32,
v_max: (py + tile.height) as f32 / atlas_h as f32,
},
);
used_pixels += tile.width * tile.height;
}
}
}
PackResult {
texture,
uv_map,
dropped,
used_pixels,
cursor_x: 0,
shelf_y: 0,
shelf_max_h: 0,
}
}
pub struct MultiPageAtlas {
pub pages: Vec<SdfAtlas>,
pub page_size: u32,
}
impl MultiPageAtlas {
pub fn pack(tiles: &[SdfTile], page_size: u32, padding: u32) -> Self {
let mut pages: Vec<SdfAtlas> = Vec::new();
let atlas_size = page_size.next_power_of_two().max(64);
let mut queue: Vec<SdfTile> = tiles.to_vec();
while !queue.is_empty() {
let tex_len = atlas_size as usize * atlas_size as usize;
let mut page = SdfAtlas {
width: atlas_size,
height: atlas_size,
texture: vec![0u8; tex_len],
uv_map: HashMap::new(),
cursor_x: padding,
shelf_y: padding,
shelf_max_h: 0,
padding,
};
let mut any_packed = false;
let mut leftover: Vec<SdfTile> = Vec::new();
for tile in &queue {
if tile.width + 2 * padding > atlas_size || tile.height + 2 * padding > atlas_size {
continue;
}
match page.add_tile(tile) {
Some(_) => {
any_packed = true;
}
None => {
leftover.push(tile.clone());
}
}
}
pages.push(page);
if !any_packed {
break;
}
queue = leftover;
}
Self { pages, page_size }
}
pub fn lookup(&self, glyph_id: u16) -> Option<(usize, &UvRect)> {
self.pages
.iter()
.enumerate()
.find_map(|(i, page)| page.uv_map.get(&glyph_id).map(|uv| (i, uv)))
}
}
pub fn pack_growing(
tiles: &[SdfTile],
initial_size: u32,
max_size: u32,
padding: u32,
algorithm: PackingAlgorithm,
) -> (SdfAtlas, AtlasStats) {
let mut current_size = initial_size.max(1);
loop {
let options = AtlasOptions {
atlas_size: current_size,
padding,
max_size: Some(max_size),
algorithm,
};
let (atlas, stats) = SdfAtlas::pack_with_options(tiles, &options);
if stats.tiles_dropped == 0 || current_size >= max_size {
return (atlas, stats);
}
current_size = (current_size.saturating_mul(2)).min(max_size);
}
}
pub struct SdfAtlas {
pub width: u32,
pub height: u32,
pub texture: Vec<u8>,
pub uv_map: HashMap<u16, UvRect>,
cursor_x: u32,
shelf_y: u32,
shelf_max_h: u32,
padding: u32,
}
const MAGIC: &[u8; 4] = b"SDFA";
const VERSION: u32 = 1;
const ENTRY_SIZE: usize = 28;
const ENTRIES_OFFSET: usize = 20;
impl SdfAtlas {
pub fn new(width: u32, height: u32) -> Self {
let capacity = width as usize * height as usize;
Self {
width,
height,
texture: vec![0u8; capacity],
uv_map: HashMap::new(),
cursor_x: 0,
shelf_y: 0,
shelf_max_h: 0,
padding: 0,
}
}
pub fn with_capacity(
width: u32,
height: u32,
tile_count_hint: usize,
average_tile_area: usize,
) -> Self {
let capacity = (tile_count_hint * average_tile_area).min((width * height) as usize);
let mut atlas = Self::new(width, height);
atlas
.texture
.reserve(capacity.saturating_sub(atlas.texture.len()));
atlas
}
pub fn pack(tiles: &[SdfTile]) -> Self {
if tiles.is_empty() {
return Self {
width: 1,
height: 1,
texture: vec![0],
uv_map: HashMap::new(),
cursor_x: 0,
shelf_y: 0,
shelf_max_h: 0,
padding: 0,
};
}
let tile_w = tiles[0].width;
let tile_h = tiles[0].height;
let count = tiles.len() as u32;
let cols = (count as f32).sqrt().ceil() as u32;
let rows = count.div_ceil(cols);
let atlas_w = (cols * tile_w).next_power_of_two().max(256);
let atlas_h = (rows * tile_h).next_power_of_two().max(256);
let res = pack_inner(tiles, atlas_w, atlas_h, 0);
Self {
width: atlas_w,
height: atlas_h,
texture: res.texture,
uv_map: res.uv_map,
cursor_x: res.cursor_x,
shelf_y: res.shelf_y,
shelf_max_h: res.shelf_max_h,
padding: 0,
}
}
pub fn pack_with_options(tiles: &[SdfTile], options: &AtlasOptions) -> (Self, AtlasStats) {
let atlas_size = options.atlas_size.next_power_of_two().max(64);
if tiles.is_empty() {
let atlas = Self {
width: atlas_size,
height: atlas_size,
texture: vec![0u8; (atlas_size as usize) * (atlas_size as usize)],
uv_map: HashMap::new(),
cursor_x: options.padding,
shelf_y: options.padding,
shelf_max_h: 0,
padding: options.padding,
};
return (
atlas,
AtlasStats {
tiles_packed: 0,
tiles_dropped: 0,
utilization: 0.0,
wasted_pixels: atlas_size * atlas_size,
},
);
}
let res = match options.algorithm {
PackingAlgorithm::Shelf => pack_inner(tiles, atlas_size, atlas_size, options.padding),
PackingAlgorithm::MaxRects => {
pack_inner_maxrects(tiles, atlas_size, atlas_size, options.padding)
}
PackingAlgorithm::Skyline => {
pack_inner_skyline(tiles, atlas_size, atlas_size, options.padding)
}
};
let total = atlas_size * atlas_size;
let tiles_packed = tiles.len() - res.dropped;
let utilization = res.used_pixels as f32 / total as f32;
let wasted_pixels = total.saturating_sub(res.used_pixels);
let stats = AtlasStats {
tiles_packed,
tiles_dropped: res.dropped,
utilization,
wasted_pixels,
};
let atlas = Self {
width: atlas_size,
height: atlas_size,
texture: res.texture,
uv_map: res.uv_map,
cursor_x: res.cursor_x,
shelf_y: res.shelf_y,
shelf_max_h: res.shelf_max_h,
padding: options.padding,
};
(atlas, stats)
}
pub fn pack_growing(tiles: &[SdfTile], initial_size: u32, max_size: u32) -> (Self, AtlasStats) {
pack_growing(tiles, initial_size, max_size, 1, PackingAlgorithm::Shelf)
}
pub fn add_tile(&mut self, tile: &SdfTile) -> Option<UvRect> {
let pad = self.padding;
let atlas_w = self.width;
let atlas_h = self.height;
if self.cursor_x + tile.width > atlas_w.saturating_sub(pad) {
self.cursor_x = pad;
self.shelf_y += self.shelf_max_h + pad;
self.shelf_max_h = 0;
}
if self.shelf_y + tile.height > atlas_h.saturating_sub(pad) {
return None;
}
let cx = self.cursor_x;
let cy = self.shelf_y;
for y in 0..tile.height {
for x in 0..tile.width {
let src_idx = (y * tile.width + x) as usize;
let dst_idx = ((cy + y) * atlas_w + (cx + x)) as usize;
if dst_idx < self.texture.len() && src_idx < tile.data.len() {
self.texture[dst_idx] = tile.data[src_idx];
}
}
}
let uv = UvRect {
u_min: cx as f32 / atlas_w as f32,
v_min: cy as f32 / atlas_h as f32,
u_max: (cx + tile.width) as f32 / atlas_w as f32,
v_max: (cy + tile.height) as f32 / atlas_h as f32,
};
self.uv_map.insert(tile.glyph_id, uv.clone());
self.cursor_x += tile.width + pad;
self.shelf_max_h = self.shelf_max_h.max(tile.height);
Some(uv)
}
pub fn remove_tile(&mut self, glyph_id: u16) -> bool {
self.uv_map.remove(&glyph_id).is_some()
}
pub fn to_bytes(&self) -> Vec<u8> {
let num_entries = self.uv_map.len() as u32;
let texture_len = self.width as usize * self.height as usize;
let total = ENTRIES_OFFSET + num_entries as usize * ENTRY_SIZE + texture_len;
let mut buf = Vec::with_capacity(total);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&VERSION.to_le_bytes());
buf.extend_from_slice(&self.width.to_le_bytes());
buf.extend_from_slice(&self.height.to_le_bytes());
buf.extend_from_slice(&num_entries.to_le_bytes());
let mut entries: Vec<(&u16, &UvRect)> = self.uv_map.iter().collect();
entries.sort_by_key(|(gid, _)| *gid);
for (glyph_id, uv) in entries {
buf.extend_from_slice(&glyph_id.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&uv.u_min.to_bits().to_le_bytes()); buf.extend_from_slice(&uv.v_min.to_bits().to_le_bytes()); buf.extend_from_slice(&uv.u_max.to_bits().to_le_bytes()); buf.extend_from_slice(&uv.v_max.to_bits().to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes()); }
buf.extend_from_slice(&self.texture);
buf
}
pub fn from_bytes(data: &[u8]) -> Result<Self, SdfError> {
if data.len() < ENTRIES_OFFSET {
return Err(SdfError::InvalidData(format!(
"buffer too short: need at least {ENTRIES_OFFSET} bytes, got {}",
data.len()
)));
}
if &data[0..4] != MAGIC {
return Err(SdfError::InvalidData(format!(
"bad magic: expected {:?}, got {:?}",
MAGIC,
&data[0..4]
)));
}
let version = u32::from_le_bytes(
data[4..8]
.try_into()
.map_err(|_| SdfError::InvalidData("cannot read version".into()))?,
);
if version != VERSION {
return Err(SdfError::InvalidData(format!(
"unsupported version {version}, expected {VERSION}"
)));
}
let atlas_w = u32::from_le_bytes(
data[8..12]
.try_into()
.map_err(|_| SdfError::InvalidData("cannot read atlas_w".into()))?,
);
let atlas_h = u32::from_le_bytes(
data[12..16]
.try_into()
.map_err(|_| SdfError::InvalidData("cannot read atlas_h".into()))?,
);
let num_entries = u32::from_le_bytes(
data[16..20]
.try_into()
.map_err(|_| SdfError::InvalidData("cannot read num_entries".into()))?,
) as usize;
let texture_len = atlas_w as usize * atlas_h as usize;
let expected_len = ENTRIES_OFFSET + num_entries * ENTRY_SIZE + texture_len;
if data.len() < expected_len {
return Err(SdfError::InvalidData(format!(
"buffer too short: expected {expected_len} bytes, got {}",
data.len()
)));
}
let mut uv_map: HashMap<u16, UvRect> = HashMap::with_capacity(num_entries);
for i in 0..num_entries {
let base = ENTRIES_OFFSET + i * ENTRY_SIZE;
let glyph_id = u16::from_le_bytes(
data[base..base + 2]
.try_into()
.map_err(|_| SdfError::InvalidData(format!("entry {i}: bad glyph_id")))?,
);
let u_min = f32::from_bits(u32::from_le_bytes(
data[base + 4..base + 8]
.try_into()
.map_err(|_| SdfError::InvalidData(format!("entry {i}: bad u_min")))?,
));
let v_min = f32::from_bits(u32::from_le_bytes(
data[base + 8..base + 12]
.try_into()
.map_err(|_| SdfError::InvalidData(format!("entry {i}: bad v_min")))?,
));
let u_max = f32::from_bits(u32::from_le_bytes(
data[base + 12..base + 16]
.try_into()
.map_err(|_| SdfError::InvalidData(format!("entry {i}: bad u_max")))?,
));
let v_max = f32::from_bits(u32::from_le_bytes(
data[base + 16..base + 20]
.try_into()
.map_err(|_| SdfError::InvalidData(format!("entry {i}: bad v_max")))?,
));
uv_map.insert(
glyph_id,
UvRect {
u_min,
v_min,
u_max,
v_max,
},
);
}
let tex_start = ENTRIES_OFFSET + num_entries * ENTRY_SIZE;
let texture = data[tex_start..tex_start + texture_len].to_vec();
Ok(Self {
width: atlas_w,
height: atlas_h,
texture,
uv_map,
cursor_x: 0,
shelf_y: 0,
shelf_max_h: 0,
padding: 0,
})
}
pub fn from_static(data: &'static [u8]) -> Result<Self, SdfError> {
Self::from_bytes(data)
}
pub fn export_png(&self, path: &std::path::Path) -> Result<(), SdfError> {
use std::io::BufWriter;
let file = std::fs::File::create(path).map_err(|e| SdfError::Io(e.to_string()))?;
let w = BufWriter::new(file);
let mut encoder = png::Encoder::new(w, self.width, self.height);
encoder.set_color(png::ColorType::Grayscale);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|e| SdfError::Io(e.to_string()))?;
writer
.write_image_data(&self.texture)
.map_err(|e| SdfError::Io(e.to_string()))
}
}
pub struct MsdfAtlas {
pub width: u32,
pub height: u32,
pub texture: Vec<u8>,
pub uv_map: HashMap<u16, UvRect>,
}
impl MsdfAtlas {
pub fn pack(tiles: &[crate::msdf::MsdfTile], atlas_size: u32) -> Self {
if tiles.is_empty() {
return Self {
width: 1,
height: 1,
texture: vec![0u8; 3],
uv_map: HashMap::new(),
};
}
let atlas_w = atlas_size.next_power_of_two().max(64);
let atlas_h = atlas_size.next_power_of_two().max(64);
let tex_len = atlas_w as usize * atlas_h as usize * 3;
let mut texture = vec![0u8; tex_len];
let mut uv_map = HashMap::new();
let mut cx = 0u32;
let mut cy = 0u32;
let mut row_h = 0u32;
for tile in tiles {
if cx + tile.width > atlas_w {
cx = 0;
cy += row_h;
row_h = 0;
}
if cy + tile.height > atlas_h {
continue;
}
for y in 0..tile.height {
for x in 0..tile.width {
let src_base = (y * tile.width + x) as usize * 3;
let dst_base = ((cy + y) * atlas_w + (cx + x)) as usize * 3;
if dst_base + 2 < texture.len() && src_base + 2 < tile.data.len() {
texture[dst_base] = tile.data[src_base];
texture[dst_base + 1] = tile.data[src_base + 1];
texture[dst_base + 2] = tile.data[src_base + 2];
}
}
}
uv_map.insert(
tile.glyph_id,
UvRect {
u_min: cx as f32 / atlas_w as f32,
v_min: cy as f32 / atlas_h as f32,
u_max: (cx + tile.width) as f32 / atlas_w as f32,
v_max: (cy + tile.height) as f32 / atlas_h as f32,
},
);
cx += tile.width;
row_h = row_h.max(tile.height);
}
Self {
width: atlas_w,
height: atlas_h,
texture,
uv_map,
}
}
pub fn export_png(&self, path: &std::path::Path) -> Result<(), SdfError> {
use std::io::BufWriter;
let file = std::fs::File::create(path).map_err(|e| SdfError::Io(e.to_string()))?;
let w = BufWriter::new(file);
let mut encoder = png::Encoder::new(w, self.width, self.height);
encoder.set_color(png::ColorType::Rgb);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|e| SdfError::Io(e.to_string()))?;
writer
.write_image_data(&self.texture)
.map_err(|e| SdfError::Io(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::msdf::MsdfTile;
fn make_sdf_tile(glyph_id: u16, w: u32, h: u32) -> SdfTile {
SdfTile {
glyph_id,
width: w,
height: h,
data: vec![128u8; (w * h) as usize],
bearing_x: 0,
bearing_y: 0,
advance_x: w as f32,
}
}
#[test]
fn msdf_atlas_packs_many_tiles() {
let tiles: Vec<MsdfTile> = (0..20u16)
.map(|i| MsdfTile {
glyph_id: i,
width: 16,
height: 16,
data: vec![128u8; 16 * 16 * 3],
bearing_x: 0.0,
bearing_y: 0.0,
advance_x: 16.0,
})
.collect();
let atlas = MsdfAtlas::pack(&tiles, 256);
assert_eq!(atlas.uv_map.len(), 20);
for uv in atlas.uv_map.values() {
assert!(uv.u_min >= 0.0 && uv.u_max <= 1.0);
assert!(uv.v_min >= 0.0 && uv.v_max <= 1.0);
}
}
#[test]
fn pack_with_options_returns_stats() {
let tiles: Vec<SdfTile> = (0..10u16).map(|i| make_sdf_tile(i, 16, 16)).collect();
let opts = AtlasOptions {
atlas_size: 128,
padding: 1,
..Default::default()
};
let (atlas, stats) = SdfAtlas::pack_with_options(&tiles, &opts);
assert_eq!(stats.tiles_packed + stats.tiles_dropped, 10);
assert!(
stats.utilization > 0.0 && stats.utilization <= 1.0,
"utilization out of range: {}",
stats.utilization
);
assert_eq!(atlas.uv_map.len(), stats.tiles_packed);
}
#[test]
fn remove_tile_removes_from_map() {
let tiles = vec![make_sdf_tile(42, 16, 16)];
let mut atlas = SdfAtlas::pack(&tiles);
assert!(atlas.uv_map.contains_key(&42));
assert!(atlas.remove_tile(42));
assert!(!atlas.uv_map.contains_key(&42));
assert!(!atlas.remove_tile(42));
}
#[test]
fn add_tile_places_new_tile() {
let tiles: Vec<SdfTile> = (0..4u16).map(|i| make_sdf_tile(i, 16, 16)).collect();
let opts = AtlasOptions {
atlas_size: 128,
padding: 0,
..Default::default()
};
let (mut atlas, _) = SdfAtlas::pack_with_options(&tiles, &opts);
let new_tile = make_sdf_tile(99, 16, 16);
let uv = atlas.add_tile(&new_tile);
assert!(uv.is_some(), "expected tile to be placed");
assert!(atlas.uv_map.contains_key(&99));
}
#[test]
fn from_coverage_basic() {
let coverage = vec![1.0f32; 8 * 8];
let tile =
SdfTile::from_coverage(7, &coverage, 8, 8, 4.0, 0, 0, 8.0).expect("from_coverage");
assert_eq!(tile.glyph_id, 7);
assert_eq!(tile.width, 8);
assert_eq!(tile.height, 8);
let center = tile.data[4 * 8 + 4];
assert!(
center > 128,
"center of solid square should be inside, got {center}"
);
}
#[test]
fn from_coverage_zero_size_errors() {
let cov = vec![1.0f32; 0];
assert!(SdfTile::from_coverage(0, &cov, 0, 8, 4.0, 0, 0, 0.0).is_err());
assert!(SdfTile::from_coverage(0, &cov, 8, 0, 4.0, 0, 0, 0.0).is_err());
}
#[test]
fn test_maxrects_non_overlapping() {
let tiles: Vec<SdfTile> = (0..20u16)
.map(|id| SdfTile {
glyph_id: id,
width: 16,
height: 16,
data: vec![128u8; 256],
bearing_x: 0,
bearing_y: 0,
advance_x: 16.0,
})
.collect();
let options = AtlasOptions {
atlas_size: 128,
padding: 1,
algorithm: PackingAlgorithm::MaxRects,
..Default::default()
};
let (atlas, stats) = SdfAtlas::pack_with_options(&tiles, &options);
let uvs: Vec<_> = atlas.uv_map.values().collect();
for i in 0..uvs.len() {
for j in (i + 1)..uvs.len() {
let a = uvs[i];
let b = uvs[j];
let overlap = a.u_min < b.u_max
&& a.u_max > b.u_min
&& a.v_min < b.v_max
&& a.v_max > b.v_min;
assert!(!overlap, "UV rects {:?} and {:?} overlap", a, b);
}
}
let _ = stats;
}
#[test]
fn test_growing_pack_packs_all() {
let tiles: Vec<SdfTile> = (0..50u16)
.map(|id| SdfTile {
glyph_id: id,
width: 32,
height: 32,
data: vec![128u8; 32 * 32],
bearing_x: 0,
bearing_y: 0,
advance_x: 32.0,
})
.collect();
let (atlas, stats) = pack_growing(&tiles, 64, 1024, 1, PackingAlgorithm::Shelf);
assert_eq!(stats.tiles_dropped, 0, "all tiles should fit after growing");
assert_eq!(atlas.uv_map.len(), 50);
}
#[test]
fn test_sdf_atlas_serialization_roundtrip() {
let tiles: Vec<SdfTile> = (0..5u16)
.map(|id| SdfTile {
glyph_id: id,
width: 8,
height: 8,
data: vec![id as u8; 64],
bearing_x: 0,
bearing_y: 0,
advance_x: 8.0,
})
.collect();
let (atlas, _) = SdfAtlas::pack_with_options(
&tiles,
&AtlasOptions {
atlas_size: 64,
padding: 0,
..Default::default()
},
);
let bytes = atlas.to_bytes();
let restored = SdfAtlas::from_bytes(&bytes).expect("deserialization");
assert_eq!(restored.width, atlas.width);
assert_eq!(restored.height, atlas.height);
assert_eq!(restored.uv_map.len(), atlas.uv_map.len());
for (gid, uv) in &atlas.uv_map {
let r = &restored.uv_map[gid];
assert!((r.u_min - uv.u_min).abs() < 1e-5);
assert!((r.u_max - uv.u_max).abs() < 1e-5);
}
}
#[test]
fn test_export_png_produces_valid_file() {
use std::env::temp_dir;
let tmp = temp_dir().join("test_sdf_atlas.png");
let mut atlas = SdfAtlas::new(4, 4);
atlas.texture = vec![128u8; 16];
atlas.export_png(&tmp).expect("export should succeed");
assert!(tmp.exists());
assert!(tmp.metadata().expect("metadata").len() > 0);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_msdf_export_png_produces_valid_file() {
use std::env::temp_dir;
let tmp = temp_dir().join("test_msdf_atlas.png");
let atlas = MsdfAtlas {
width: 2,
height: 2,
texture: vec![128u8; 2 * 2 * 3],
uv_map: Default::default(),
};
atlas.export_png(&tmp).expect("msdf export should succeed");
assert!(tmp.exists());
assert!(tmp.metadata().expect("metadata").len() > 0);
std::fs::remove_file(&tmp).ok();
}
#[test]
fn test_pre_allocated_atlas_capacity() {
let atlas = SdfAtlas::with_capacity(256, 256, 100, 64);
assert_eq!(atlas.width, 256);
assert_eq!(atlas.height, 256);
assert!(atlas.texture.capacity() >= atlas.texture.len());
}
#[test]
fn test_sdf_atlas_new_zeroed() {
let atlas = SdfAtlas::new(8, 8);
assert_eq!(atlas.width, 8);
assert_eq!(atlas.height, 8);
assert_eq!(atlas.texture.len(), 64);
assert!(atlas.texture.iter().all(|&b| b == 0));
assert!(atlas.uv_map.is_empty());
}
#[test]
fn test_from_static_roundtrip() {
let atlas = SdfAtlas::new(64, 64);
let bytes = atlas.to_bytes();
let static_bytes: &'static [u8] = Box::leak(bytes.into_boxed_slice());
let roundtrip = SdfAtlas::from_static(static_bytes).expect("from_static should succeed");
assert_eq!(roundtrip.width, 64);
assert_eq!(roundtrip.height, 64);
assert_eq!(roundtrip.uv_map.len(), 0);
}
}