use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; use exun::RawUnexpected; use image::{GenericImage, RgbaImage}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Rectangle { pub x: u32, pub y: u32, pub width: u32, pub height: u32, } #[derive(Debug, Clone, PartialEq, Eq)] struct Texture { id: Arc, x: u32, y: u32, texture: Arc, } #[derive(Debug, Clone, PartialEq, Eq)] struct ImageRect(Arc, Arc); #[derive(Debug, Default, Clone)] pub struct RectanglePacker { min_width: u32, textures: Vec, } #[derive(Debug, Clone)] pub struct TextureAtlas { atlas: RgbaImage, ids: HashMap, Rectangle>, } impl PartialOrd for ImageRect { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for ImageRect { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.0.height().cmp(&other.0.height()) } } impl RectanglePacker { pub fn new() -> Self { Self { min_width: 1, textures: Vec::new(), } } pub fn with_capacity(capacity: usize) -> Self { Self { min_width: 1, textures: Vec::with_capacity(capacity), } } pub fn capacity(&self) -> usize { self.textures.capacity() } pub fn len(&self) -> usize { self.textures.len() } pub fn is_empty(&self) -> bool { self.textures.is_empty() } pub fn reserve(&mut self, additional: usize) { self.textures.reserve(additional) } pub fn shrink_to_fit(&mut self) { self.textures.shrink_to_fit() } pub fn add_texture(&mut self, name: Arc, texture: Arc) { if texture.width() > self.min_width { self.min_width = texture.width() + 1; } self.textures.push(ImageRect(texture, name)); } fn pack(&mut self, min_width: u32) -> (Vec, u32, u32) { let image_width = self.min_width.max(min_width); // to make sure padding is rounded up and not down, 64 is added // to make sure some padding is always present, minimum is 1 pixel let horizontal_padding = ((image_width + 64) / 128).max(1); let mut x_position = 0; let mut y_position = 0; let mut largest_row_height = 0; let mut rectangles = Vec::with_capacity(self.textures.len()); self.textures.sort(); self.textures.reverse(); for texture in &self.textures { // loop to the next row if we've gone off the edge if (x_position + texture.0.width()) > image_width { let vertical_padding = ((largest_row_height + 64) / 128).max(1); y_position += largest_row_height + vertical_padding; x_position = 0; largest_row_height = 0; } // set the rectangle position let x = x_position; let y = y_position; x_position += texture.0.width() + horizontal_padding; if texture.0.height() > largest_row_height { largest_row_height = texture.0.height(); } rectangles.push(Texture { id: texture.1.clone(), x, y, texture: texture.0.clone(), }); } let vertical_padding = ((largest_row_height + 64) / 128).max(1); let total_height = y_position + largest_row_height + vertical_padding; (rectangles, image_width, total_height) } pub fn output( &mut self, min_width: u32, min_height: u32, ) -> Result { let (rectangles, image_width, image_height) = self.pack(min_width); let image_height = image_height.max(min_height); let mut atlas = RgbaImage::new(image_width, image_height); let mut ids = HashMap::with_capacity(rectangles.len()); for rectangle in rectangles { atlas.copy_from(rectangle.texture.deref(), rectangle.x, rectangle.y)?; ids.insert( rectangle.id, Rectangle { x: rectangle.x, y: rectangle.y, width: rectangle.texture.width(), height: rectangle.texture.height(), }, ); } Ok(TextureAtlas { atlas, ids }) } } impl TextureAtlas { pub fn get_full_atlas(&self) -> &RgbaImage { &self.atlas } pub fn get_texture_rect(&self, id: &str) -> Option { self.ids.get(id).cloned() } }