use crate::atlas::AtlasMetadata;
use crate::export::{ExportError, ExportOptions, Exporter};
use std::fs::File;
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LibGdxFilterMode {
#[default]
Nearest,
Linear,
MipMapNearestNearest,
MipMapLinearLinear,
}
impl LibGdxFilterMode {
pub fn as_str(&self) -> &'static str {
match self {
LibGdxFilterMode::Nearest => "Nearest",
LibGdxFilterMode::Linear => "Linear",
LibGdxFilterMode::MipMapNearestNearest => "MipMapNearestNearest",
LibGdxFilterMode::MipMapLinearLinear => "MipMapLinearLinear",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LibGdxRepeatMode {
#[default]
None,
X,
Y,
XY,
}
impl LibGdxRepeatMode {
pub fn as_str(&self) -> &'static str {
match self {
LibGdxRepeatMode::None => "none",
LibGdxRepeatMode::X => "x",
LibGdxRepeatMode::Y => "y",
LibGdxRepeatMode::XY => "xy",
}
}
}
#[derive(Debug, Clone)]
pub struct LibGdxExportOptions {
pub base: ExportOptions,
pub filter: (LibGdxFilterMode, LibGdxFilterMode),
pub repeat: LibGdxRepeatMode,
pub format: String,
}
impl Default for LibGdxExportOptions {
fn default() -> Self {
Self {
base: ExportOptions::default(),
filter: (LibGdxFilterMode::Nearest, LibGdxFilterMode::Nearest),
repeat: LibGdxRepeatMode::None,
format: "RGBA8888".to_string(),
}
}
}
#[derive(Debug)]
pub struct LibGdxExporter {
min_filter: LibGdxFilterMode,
mag_filter: LibGdxFilterMode,
repeat: LibGdxRepeatMode,
format: String,
}
impl Default for LibGdxExporter {
fn default() -> Self {
Self::new()
}
}
impl LibGdxExporter {
pub fn new() -> Self {
Self {
min_filter: LibGdxFilterMode::Nearest,
mag_filter: LibGdxFilterMode::Nearest,
repeat: LibGdxRepeatMode::None,
format: "RGBA8888".to_string(),
}
}
pub fn with_min_filter(mut self, filter: LibGdxFilterMode) -> Self {
self.min_filter = filter;
self
}
pub fn with_mag_filter(mut self, filter: LibGdxFilterMode) -> Self {
self.mag_filter = filter;
self
}
pub fn with_filter(mut self, filter: LibGdxFilterMode) -> Self {
self.min_filter = filter;
self.mag_filter = filter;
self
}
pub fn with_repeat(mut self, repeat: LibGdxRepeatMode) -> Self {
self.repeat = repeat;
self
}
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = format.into();
self
}
pub fn export_libgdx(
&self,
metadata: &AtlasMetadata,
output_path: &Path,
_options: &LibGdxExportOptions,
) -> Result<(), ExportError> {
let content = self.generate_atlas_content(metadata);
if let Some(parent) = output_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
let mut file = File::create(output_path)?;
file.write_all(content.as_bytes())?;
Ok(())
}
pub fn export_to_string(&self, metadata: &AtlasMetadata) -> String {
self.generate_atlas_content(metadata)
}
fn generate_atlas_content(&self, metadata: &AtlasMetadata) -> String {
let mut content = String::new();
content.push_str(&metadata.image);
content.push('\n');
content.push_str(&format!("size: {}, {}\n", metadata.size[0], metadata.size[1]));
content.push_str(&format!("format: {}\n", self.format));
content.push_str(&format!(
"filter: {}, {}\n",
self.min_filter.as_str(),
self.mag_filter.as_str()
));
content.push_str(&format!("repeat: {}\n", self.repeat.as_str()));
let mut frame_names: Vec<&String> = metadata.frames.keys().collect();
frame_names.sort();
let animation_indices = self.build_animation_indices(metadata);
for name in frame_names {
if let Some(frame) = metadata.frames.get(name) {
content.push_str(name);
content.push('\n');
content.push_str(" rotate: false\n");
content.push_str(&format!(" xy: {}, {}\n", frame.x, frame.y));
content.push_str(&format!(" size: {}, {}\n", frame.w, frame.h));
content.push_str(&format!(" orig: {}, {}\n", frame.w, frame.h));
let (offset_x, offset_y) =
if let Some(origin) = &frame.origin { (origin[0], origin[1]) } else { (0, 0) };
content.push_str(&format!(" offset: {}, {}\n", offset_x, offset_y));
let index = animation_indices.get(name.as_str()).copied().unwrap_or(-1);
content.push_str(&format!(" index: {}\n", index));
}
}
content
}
fn build_animation_indices<'a>(
&self,
metadata: &'a AtlasMetadata,
) -> std::collections::HashMap<&'a str, i32> {
let mut indices = std::collections::HashMap::new();
for animation in metadata.animations.values() {
for (i, frame_name) in animation.frames.iter().enumerate() {
indices.insert(frame_name.as_str(), i as i32);
}
}
indices
}
}
impl Exporter for LibGdxExporter {
fn export(
&self,
metadata: &AtlasMetadata,
output_path: &Path,
options: &ExportOptions,
) -> Result<(), ExportError> {
let libgdx_options = LibGdxExportOptions { base: options.clone(), ..Default::default() };
self.export_libgdx(metadata, output_path, &libgdx_options)
}
fn format_name(&self) -> &'static str {
"libGDX TextureAtlas"
}
fn extension(&self) -> &'static str {
"atlas"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atlas::{AtlasAnimation, AtlasFrame};
use std::collections::HashMap;
use tempfile::tempdir;
fn create_test_metadata() -> AtlasMetadata {
let mut frames = HashMap::new();
frames.insert(
"player_idle".to_string(),
AtlasFrame { x: 0, y: 0, w: 32, h: 32, origin: None, boxes: None },
);
frames.insert(
"player_walk_1".to_string(),
AtlasFrame { x: 32, y: 0, w: 32, h: 32, origin: Some([16, 32]), boxes: None },
);
frames.insert(
"player_walk_2".to_string(),
AtlasFrame { x: 64, y: 0, w: 32, h: 32, origin: Some([16, 32]), boxes: None },
);
let mut animations = HashMap::new();
animations.insert(
"walk".to_string(),
AtlasAnimation {
frames: vec!["player_walk_1".to_string(), "player_walk_2".to_string()],
fps: 10,
tags: None,
},
);
AtlasMetadata { image: "atlas.png".to_string(), size: [128, 64], frames, animations }
}
#[test]
fn test_libgdx_exporter_new() {
let exporter = LibGdxExporter::new();
assert_eq!(exporter.min_filter, LibGdxFilterMode::Nearest);
assert_eq!(exporter.mag_filter, LibGdxFilterMode::Nearest);
assert_eq!(exporter.repeat, LibGdxRepeatMode::None);
assert_eq!(exporter.format, "RGBA8888");
}
#[test]
fn test_libgdx_exporter_with_options() {
let exporter = LibGdxExporter::new()
.with_min_filter(LibGdxFilterMode::Linear)
.with_mag_filter(LibGdxFilterMode::MipMapLinearLinear)
.with_repeat(LibGdxRepeatMode::XY)
.with_format("RGB888");
assert_eq!(exporter.min_filter, LibGdxFilterMode::Linear);
assert_eq!(exporter.mag_filter, LibGdxFilterMode::MipMapLinearLinear);
assert_eq!(exporter.repeat, LibGdxRepeatMode::XY);
assert_eq!(exporter.format, "RGB888");
}
#[test]
fn test_libgdx_export_options_default() {
let options = LibGdxExportOptions::default();
assert_eq!(options.filter.0, LibGdxFilterMode::Nearest);
assert_eq!(options.filter.1, LibGdxFilterMode::Nearest);
assert_eq!(options.repeat, LibGdxRepeatMode::None);
assert_eq!(options.format, "RGBA8888");
}
#[test]
fn test_filter_mode_as_str() {
assert_eq!(LibGdxFilterMode::Nearest.as_str(), "Nearest");
assert_eq!(LibGdxFilterMode::Linear.as_str(), "Linear");
assert_eq!(LibGdxFilterMode::MipMapNearestNearest.as_str(), "MipMapNearestNearest");
assert_eq!(LibGdxFilterMode::MipMapLinearLinear.as_str(), "MipMapLinearLinear");
}
#[test]
fn test_repeat_mode_as_str() {
assert_eq!(LibGdxRepeatMode::None.as_str(), "none");
assert_eq!(LibGdxRepeatMode::X.as_str(), "x");
assert_eq!(LibGdxRepeatMode::Y.as_str(), "y");
assert_eq!(LibGdxRepeatMode::XY.as_str(), "xy");
}
#[test]
fn test_export_to_string_header() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
assert!(content.starts_with("atlas.png\n"));
assert!(content.contains("size: 128, 64\n"));
assert!(content.contains("format: RGBA8888\n"));
assert!(content.contains("filter: Nearest, Nearest\n"));
assert!(content.contains("repeat: none\n"));
}
#[test]
fn test_export_to_string_frames() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
assert!(content.contains("player_idle\n"));
assert!(content.contains(" xy: 0, 0\n"));
assert!(content.contains(" size: 32, 32\n"));
assert!(content.contains("player_walk_1\n"));
assert!(content.contains(" xy: 32, 0\n"));
}
#[test]
fn test_export_frame_with_origin() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
assert!(content.contains(" offset: 16, 32\n"));
}
#[test]
fn test_export_frame_without_origin() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
let lines: Vec<&str> = content.lines().collect();
let idle_idx = lines.iter().position(|l| *l == "player_idle").unwrap();
let offset_line = lines[idle_idx + 5]; assert_eq!(offset_line, " offset: 0, 0");
}
#[test]
fn test_export_animation_indices() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
let lines: Vec<&str> = content.lines().collect();
let walk1_idx = lines.iter().position(|l| *l == "player_walk_1").unwrap();
let index1_line = lines[walk1_idx + 6]; assert_eq!(index1_line, " index: 0");
let walk2_idx = lines.iter().position(|l| *l == "player_walk_2").unwrap();
let index2_line = lines[walk2_idx + 6];
assert_eq!(index2_line, " index: 1");
let idle_idx = lines.iter().position(|l| *l == "player_idle").unwrap();
let index_idle_line = lines[idle_idx + 6];
assert_eq!(index_idle_line, " index: -1");
}
#[test]
fn test_export_creates_file() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let options = LibGdxExportOptions::default();
let dir = tempdir().unwrap();
let output_path = dir.path().join("test.atlas");
exporter.export_libgdx(&metadata, &output_path, &options).unwrap();
assert!(output_path.exists());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(content.starts_with("atlas.png\n"));
}
#[test]
fn test_export_creates_directories() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let options = LibGdxExportOptions::default();
let dir = tempdir().unwrap();
let output_path = dir.path().join("nested").join("dir").join("test.atlas");
exporter.export_libgdx(&metadata, &output_path, &options).unwrap();
assert!(output_path.exists());
}
#[test]
fn test_export_via_trait() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let options = ExportOptions::default();
let dir = tempdir().unwrap();
let output_path = dir.path().join("test.atlas");
exporter.export(&metadata, &output_path, &options).unwrap();
assert!(output_path.exists());
}
#[test]
fn test_format_name() {
let exporter = LibGdxExporter::new();
assert_eq!(exporter.format_name(), "libGDX TextureAtlas");
}
#[test]
fn test_extension() {
let exporter = LibGdxExporter::new();
assert_eq!(exporter.extension(), "atlas");
}
#[test]
fn test_export_with_linear_filter() {
let exporter = LibGdxExporter::new().with_filter(LibGdxFilterMode::Linear);
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
assert!(content.contains("filter: Linear, Linear\n"));
}
#[test]
fn test_export_with_repeat() {
let exporter = LibGdxExporter::new().with_repeat(LibGdxRepeatMode::XY);
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
assert!(content.contains("repeat: xy\n"));
}
#[test]
fn test_frames_sorted_alphabetically() {
let exporter = LibGdxExporter::new();
let metadata = create_test_metadata();
let content = exporter.export_to_string(&metadata);
let lines: Vec<&str> = content.lines().collect();
let frame_lines: Vec<&str> = lines
.iter()
.skip(5) .filter(|l| !l.starts_with(' ') && !l.is_empty())
.copied()
.collect();
assert_eq!(frame_lines, vec!["player_idle", "player_walk_1", "player_walk_2"]);
}
#[test]
fn test_export_empty_animations() {
let exporter = LibGdxExporter::new();
let mut frames = HashMap::new();
frames.insert(
"sprite".to_string(),
AtlasFrame { x: 0, y: 0, w: 16, h: 16, origin: None, boxes: None },
);
let metadata = AtlasMetadata {
image: "test.png".to_string(),
size: [16, 16],
frames,
animations: HashMap::new(),
};
let content = exporter.export_to_string(&metadata);
assert!(content.contains(" index: -1\n"));
}
}