use log::info;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use flow_rectpack::FreeRectHeuristic;
use flow_rectpack::RectsBinPack;
use crate::Texture;
use crate::texpack::app::AtlasImage;
pub const MIN_SIZE: u32 = 64;
pub const MAX_SIZE: u32 = 8192;
#[derive(PartialEq, Clone, Debug)]
pub enum PackerError {
InvalidArg,
}
#[derive(Default, Clone, Debug)]
pub struct Point {
pub x: i32,
pub y: i32,
pub duplicate_id: usize,
pub rotate: bool,
}
#[derive(Clone, Debug)]
pub struct PackerState {
pub width: u32,
pub height: u32,
pub padding: i32,
pub generate_mipmaps: bool,
pub textures: Vec<Texture>,
pub points: Vec<Point>,
pub duplicates: HashMap<u64, usize>,
}
impl PackerState {
pub fn new(width: u32, height: u32, padding: i32, generate_mipmaps: bool) -> Self {
Self {
width,
height,
padding,
generate_mipmaps,
textures: Vec::new(),
points: Vec::new(),
duplicates: HashMap::new(),
}
}
}
#[derive(Debug)]
pub struct Packer {
pub state: Mutex<PackerState>,
}
impl Packer {
pub fn new(
width: u32,
height: u32,
padding: i32,
generate_mipmaps: bool,
) -> Result<Self, PackerError> {
if (MIN_SIZE..=MAX_SIZE).contains(&width)
&& (MIN_SIZE..=MAX_SIZE).contains(&height)
&& (width & (width - 1)) == 0
&& (height & (height - 1)) == 0
{
Ok(Self {
state: Mutex::new(PackerState::new(width, height, padding, generate_mipmaps)),
})
} else {
Err(PackerError::InvalidArg)
}
}
pub fn shared(self) -> Arc<Self> {
Arc::new(self)
}
pub fn pack(
&mut self,
textures: &mut Vec<Texture>,
unique: bool,
rotate: bool,
square: bool,
adjust_size: bool,
heuristic: FreeRectHeuristic,
) {
assert!(!textures.is_empty());
let mut exists_larger = false;
if !square {
exists_larger = self.exists_larger_texture(textures);
}
if adjust_size {
self.adjust_size_to_fit(textures);
}
let mut lock = self.state.lock().unwrap();
let mut rbp = RectsBinPack::new(
lock.width.try_into().unwrap(),
lock.height.try_into().unwrap(),
rotate,
)
.unwrap();
let mut ww: u32 = 0;
let mut hh: u32 = 0;
while !textures.is_empty() {
if let Some(texture) = textures.last() {
if unique
&& let Some(value) = lock.duplicates.get(&texture.hash_value)
&& let Some(point) = lock.points.get(*value)
{
info!(
"Texture '{}' with hash: {} is not unique (not packed but will be added in descriptor)",
texture.file_name, texture.hash_value
);
let mut p = point.clone();
p.duplicate_id = *value;
lock.points.push(p);
lock.textures.push(texture.clone());
textures.pop();
continue;
}
{
let tw: i32 = texture.width.try_into().unwrap();
let th: i32 = texture.height.try_into().unwrap();
let width: i32 = tw + lock.padding;
let height: i32 = th + lock.padding;
if let Some(rect) = rbp.insert(width, height, heuristic.clone()) {
if unique {
let num_points = lock.points.len();
lock.duplicates.insert(texture.hash_value, num_points);
}
let p = Point {
x: rect.x,
y: rect.y,
duplicate_id: usize::MAX,
rotate: rotate && tw != rect.width - lock.padding,
};
info!(
"Packed '{}' w: {} h: {} rotated: {} hash: {}",
texture.file_name,
texture.width,
texture.height,
p.rotate,
texture.hash_value
);
lock.points.push(p);
lock.textures.push(texture.clone());
textures.pop();
ww = std::cmp::max((rect.x + rect.width).try_into().unwrap(), ww);
hh = std::cmp::max((rect.y + rect.height).try_into().unwrap(), hh);
} else {
break;
}
}
} else {
panic!("texture.last() failed!");
}
}
if !square && !exists_larger {
while lock.width / 2 >= ww {
lock.width /= 2;
}
while lock.height / 2 >= hh {
lock.height /= 2;
}
}
}
pub fn save_image(&self, file_path: &PathBuf, image_type: AtlasImage) {
let lock = self.state.lock().unwrap();
let mut texture = Texture::with_details(lock.width, lock.height).unwrap();
for i in 0..lock.textures.len() {
if let Some(src) = lock.textures.get(i)
&& let Some(point) = lock.points.get(i)
&& point.duplicate_id == usize::MAX
{
if point.rotate {
texture.copy_pixels_rot_90cw(
src,
point.x.try_into().unwrap(),
point.y.try_into().unwrap(),
);
} else {
texture.copy_pixels(
src,
point.x.try_into().unwrap(),
point.y.try_into().unwrap(),
);
}
}
}
texture.save(file_path, image_type);
}
pub fn save_json(&self, file: &mut File, file_name: &str, image_ext: &str) {
let lock = self.state.lock().unwrap();
file.write_all(String::from("\t\t\t{\n").as_bytes())
.unwrap();
file.write_all(format!("\t\t\t\t\"n\": \"{}.{}\",\n", file_name, image_ext).as_bytes())
.unwrap();
file.write_all(format!("\t\t\t\t\"numImages\": {},\n", lock.textures.len()).as_bytes())
.unwrap();
file.write_all(format!("\t\t\t\t\"width\": {},\n", lock.width).as_bytes())
.unwrap();
file.write_all(format!("\t\t\t\t\"height\": {},\n", lock.height).as_bytes())
.unwrap();
file.write_all(
format!(
"\t\t\t\t\"generateMipMaps\": {},\n",
lock.generate_mipmaps as u8
)
.as_bytes(),
)
.unwrap();
file.write_all(String::from("\t\t\t\t\"img\":\n").as_bytes())
.unwrap();
file.write_all(String::from("\t\t\t\t[\n").as_bytes())
.unwrap();
for i in 0..lock.textures.len() {
if let Some(texture) = lock.textures.get(i)
&& let Some(point) = lock.points.get(i)
{
let mut trimmed = false;
if texture.frame_w != texture.width || texture.frame_h != texture.height {
trimmed = true;
}
if i > 0 {
file.write_all(String::from(",\n").as_bytes()).unwrap();
}
file.write_all(String::from("\t\t\t\t\t{\n").as_bytes())
.unwrap();
file.write_all(
format!("\t\t\t\t\t\t\"n\": \"{}\", ", texture.file_name).as_bytes(),
)
.unwrap();
file.write_all(format!("\"x\": {}, ", point.x).as_bytes())
.unwrap();
file.write_all(format!("\"y\": {}, ", point.y).as_bytes())
.unwrap();
file.write_all(format!("\"w\": {}, ", texture.width).as_bytes())
.unwrap();
file.write_all(format!("\"h\": {}, ", texture.height).as_bytes())
.unwrap();
file.write_all(format!("\"trimmed\": {}, ", trimmed as u8).as_bytes())
.unwrap();
file.write_all(format!("\"rotated\": {}, ", point.rotate as u8).as_bytes())
.unwrap();
file.write_all(format!("\"fx\": {}, ", texture.frame_x).as_bytes())
.unwrap();
file.write_all(format!("\"fy\": {}, ", texture.frame_y).as_bytes())
.unwrap();
file.write_all(format!("\"fw\": {}, ", texture.frame_w).as_bytes())
.unwrap();
file.write_all(format!("\"fh\": {}\n", texture.frame_h).as_bytes())
.unwrap();
file.write_all(String::from("\t\t\t\t\t}").as_bytes())
.unwrap();
}
}
file.write_all(String::from("\n\t\t\t\t]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t\t\t}").as_bytes()).unwrap();
}
pub fn save_txt(&self, file: &mut File, file_name: &str, image_ext: &str) {
let lock = self.state.lock().unwrap();
file.write_all(format!("{}.{}", file_name, image_ext).as_bytes())
.unwrap();
file.write_all(format!(",{}", lock.textures.len()).as_bytes())
.unwrap();
file.write_all(format!(",{}", lock.width).as_bytes())
.unwrap();
file.write_all(format!(",{}", lock.height).as_bytes())
.unwrap();
file.write_all(format!(",{}\n", lock.generate_mipmaps as u8).as_bytes())
.unwrap();
for i in 0..lock.textures.len() {
if let Some(texture) = lock.textures.get(i)
&& let Some(point) = lock.points.get(i)
{
let mut trimmed = false;
if texture.frame_w != texture.width || texture.frame_h != texture.height {
trimmed = true;
}
file.write_all(texture.file_name.to_string().as_bytes())
.unwrap();
file.write_all(format!(",{}", point.x).as_bytes()).unwrap();
file.write_all(format!(",{}", point.y).as_bytes()).unwrap();
file.write_all(format!(",{}", texture.width).as_bytes())
.unwrap();
file.write_all(format!(",{}", texture.height).as_bytes())
.unwrap();
file.write_all(format!(",{}", trimmed as u8).as_bytes())
.unwrap();
file.write_all(format!(",{}", point.rotate as u8).as_bytes())
.unwrap();
file.write_all(format!(",{}", texture.frame_x).as_bytes())
.unwrap();
file.write_all(format!(",{}", texture.frame_y).as_bytes())
.unwrap();
file.write_all(format!(",{}", texture.frame_w).as_bytes())
.unwrap();
file.write_all(format!(",{}\n", texture.frame_h).as_bytes())
.unwrap();
}
}
}
fn adjust_size_to_fit(&mut self, textures: &[Texture]) -> bool {
let mut lock = self.state.lock().unwrap();
let mut adjusted_size = false;
let padding: u32 = lock.padding.try_into().unwrap();
for i in 0..textures.len() {
if let Some(texture) = textures.get(i) {
if texture.width + padding > lock.width {
lock.width *= 2;
lock.height = lock.width;
adjusted_size = true;
}
if texture.height + padding > lock.height {
lock.height *= 2;
lock.width = lock.height;
adjusted_size = true;
}
if lock.width > MAX_SIZE || lock.height > MAX_SIZE {
panic!(
"adjust_size_to_fit failed. Maximum allowed width / height is {}",
MAX_SIZE
);
}
if lock.width < MIN_SIZE {
lock.width = MIN_SIZE;
}
if lock.height < MIN_SIZE {
lock.height = MIN_SIZE;
}
}
}
if adjusted_size {
info!(
"Packer: Adjusted size to {}x{} to fit textures.",
lock.width, lock.height
);
}
adjusted_size
}
fn exists_larger_texture(&self, textures: &[Texture]) -> bool {
let lock = self.state.lock().unwrap();
let padding: u32 = lock.padding.try_into().unwrap();
for i in 0..textures.len() {
if let Some(texture) = textures.get(i)
&& (texture.width + padding > lock.width || texture.height + padding > lock.height)
{
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::texpack::app::{exists_file, get_atlas_image_extension, remove_file};
#[test]
fn point_basics() {
let p = Point::default();
assert_eq!(p.x, 0);
assert_eq!(p.y, 0);
assert_eq!(p.duplicate_id, 0);
assert!(!p.rotate);
}
#[test]
fn packer_error() {
assert_eq!(
Packer::new(0, 0, 1, false).unwrap_err(),
PackerError::InvalidArg
);
assert_eq!(
Packer::new(32, 32, 1, false).unwrap_err(),
PackerError::InvalidArg
);
assert_eq!(
Packer::new(8192, 8193, 1, false).unwrap_err(),
PackerError::InvalidArg
);
assert_eq!(
Packer::new(32, 64, 1, false).unwrap_err(),
PackerError::InvalidArg
);
assert_eq!(
Packer::new(64, 32, 1, false).unwrap_err(),
PackerError::InvalidArg
);
}
fn load_textures() -> Vec<Texture> {
let mut textures: Vec<Texture> = Vec::new();
let mut t1 = Texture::default();
t1.load(
&PathBuf::from("test_data/white_32x32.png"),
false,
false,
false,
0,
64,
);
textures.push(t1);
let mut t2 = Texture::default();
t2.load(
&PathBuf::from("test_data/red_32x32.png"),
false,
false,
false,
0,
64,
);
textures.push(t2);
let mut t3 = Texture::default();
t3.load(
&PathBuf::from("test_data/green_32x32.png"),
false,
false,
false,
0,
64,
);
textures.push(t3);
let mut t4 = Texture::default();
t4.load(
&PathBuf::from("test_data/blue_32x32.png"),
false,
false,
false,
0,
64,
);
textures.push(t4);
textures
}
#[test]
fn packer_basics_short_side_fit() {
let mut textures = load_textures();
assert_eq!(textures.len(), 4);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::ShortSideFit,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_short_side_fit.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_basics_long_side_fit() {
let mut textures = load_textures();
assert!(textures.len() == 4);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::LongSideFit,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_long_side_fit.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_basics_area_fit() {
let mut textures = load_textures();
assert!(textures.len() == 4);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::AreaFit,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_area_fit.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_basics_bottom_left() {
let mut textures = load_textures();
assert!(textures.len() == 4);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::BottomLeft,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_bottom_left.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_basics_contact_point() {
let mut textures = load_textures();
assert!(textures.len() == 4);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::ContactPoint,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_contact_point.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_adjust_size_to_fit() {
let mut textures: Vec<Texture> = Vec::new();
let mut t1 = Texture::default();
t1.load(
&PathBuf::from("test_data/white_128x128.png"),
false,
false,
true,
0,
64,
);
textures.push(t1);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::ContactPoint,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_adjust_size_to_fit.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
let mut output = Texture::default();
output.load(
&PathBuf::from("test_data/atlas_adjust_size_to_fit.png"),
false,
false,
false,
0,
64,
);
assert_eq!(output.width, 64);
assert_eq!(output.height, 64);
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_trim() {
let mut textures: Vec<Texture> = Vec::new();
let mut t1 = Texture::default();
t1.load(
&PathBuf::from("test_data/blue_trimmable_128x128.png"),
false,
true,
false,
0,
128,
);
textures.push(t1);
let mut packer = Packer::new(128, 128, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::BottomLeft,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_trimmed.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
let mut output = Texture::default();
output.load(
&PathBuf::from("test_data/atlas_trimmed.png"),
false,
false,
false,
0,
64,
);
assert_eq!(output.width, 32);
assert_eq!(output.height, 32);
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_rotated() {
let mut textures: Vec<Texture> = Vec::new();
let mut t1 = Texture::default();
t1.load(
&PathBuf::from("test_data/white_128x64.png"),
false,
false,
false,
0,
64,
);
textures.push(t1);
assert!(textures.len() == 1);
let mut packer = Packer::new(64, 128, 0, true).unwrap();
packer.pack(
&mut textures,
true,
true,
false,
false,
FreeRectHeuristic::LongSideFit,
);
assert!(textures.is_empty());
let file_path = PathBuf::from("test_data/atlas_rotated.png");
packer.save_image(&file_path, AtlasImage::Png);
assert!(exists_file(&file_path));
let mut output = Texture::default();
output.load(
&PathBuf::from("test_data/atlas_rotated.png"),
false,
false,
false,
0,
64,
);
assert_eq!(output.width, 64);
assert_eq!(output.height, 128);
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
#[test]
fn packer_save_all_supported_types() {
let mut textures = load_textures();
assert!(textures.len() == 4);
let mut packer = Packer::new(64, 64, 0, true).unwrap();
packer.pack(
&mut textures,
true,
false,
false,
false,
FreeRectHeuristic::ShortSideFit,
);
assert!(textures.is_empty());
let base_file_path = PathBuf::from("test_data/atlas_save");
let image_types = vec![
AtlasImage::Png,
AtlasImage::Tga,
AtlasImage::Tiff,
AtlasImage::Webp,
];
for image_type in image_types {
let file_path = PathBuf::from(format!(
"{}.{}",
base_file_path.display(),
get_atlas_image_extension(image_type)
));
println!("{}", file_path.display());
packer.save_image(&file_path, image_type);
assert!(exists_file(&file_path));
remove_file(&file_path);
assert!(!exists_file(&file_path));
}
}
}