summaryrefslogtreecommitdiff
path: root/packer/src/lib.rs
blob: 6715a9e228a2802f57a465d89d05165b5213e5de (plain)
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()
	}
}