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<str>,
x: u32,
y: u32,
texture: Arc<RgbaImage>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ImageRect(Arc<RgbaImage>, Arc<str>);
#[derive(Debug, Default, Clone)]
pub struct RectanglePacker {
min_width: u32,
textures: Vec<ImageRect>,
}
#[derive(Debug, Clone)]
pub struct TextureAtlas {
atlas: RgbaImage,
ids: HashMap<Arc<str>, Rectangle>,
}
impl PartialOrd for ImageRect {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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<str>, texture: Arc<RgbaImage>) {
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<Texture>, 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<TextureAtlas, RawUnexpected> {
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<Rectangle> {
self.ids.get(id).cloned()
}
}
|