use crate::atlas::AtlasMetadata;
use crate::export::{ExportError, ExportOptions, Exporter};
use serde::Serialize;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default)]
pub enum UnityFilterMode {
#[default]
Point,
Bilinear,
Trilinear,
}
impl UnityFilterMode {
pub fn from_config(mode: &crate::config::FilterMode) -> Self {
match mode {
crate::config::FilterMode::Point => Self::Point,
crate::config::FilterMode::Bilinear => Self::Bilinear,
}
}
}
#[derive(Debug, Clone)]
pub struct UnityExportOptions {
pub base: ExportOptions,
pub pixels_per_unit: u32,
pub filter_mode: UnityFilterMode,
pub include_animations: bool,
pub generate_meta: bool,
pub generate_anim_files: bool,
pub generate_json: bool,
}
impl Default for UnityExportOptions {
fn default() -> Self {
Self {
base: ExportOptions::default(),
pixels_per_unit: 16,
filter_mode: UnityFilterMode::Point,
include_animations: true,
generate_meta: true,
generate_anim_files: true,
generate_json: true,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct UnitySprite {
pub name: String,
pub rect: UnityRect,
pub pivot: UnityVector2,
pub border: UnityVector4,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnityRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnityVector2 {
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnityVector4 {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UnityAnimation {
pub name: String,
pub frame_rate: u32,
pub sprites: Vec<String>,
pub loop_animation: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UnityAtlasData {
pub texture: String,
pub texture_size: UnityVector2,
pub pixels_per_unit: u32,
pub filter_mode: UnityFilterMode,
pub sprites: Vec<UnitySprite>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub animations: Vec<UnityAnimation>,
}
#[derive(Debug, Default)]
pub struct UnityExporter {
pixels_per_unit: u32,
filter_mode: UnityFilterMode,
include_animations: bool,
generate_meta: bool,
generate_anim_files: bool,
generate_json: bool,
}
impl UnityExporter {
pub fn new() -> Self {
Self {
pixels_per_unit: 16,
filter_mode: UnityFilterMode::Point,
include_animations: true,
generate_meta: true,
generate_anim_files: true,
generate_json: true,
}
}
pub fn with_pixels_per_unit(mut self, ppu: u32) -> Self {
self.pixels_per_unit = ppu;
self
}
pub fn with_filter_mode(mut self, mode: UnityFilterMode) -> Self {
self.filter_mode = mode;
self
}
pub fn with_animations(mut self, enabled: bool) -> Self {
self.include_animations = enabled;
self
}
pub fn with_meta_file(mut self, enabled: bool) -> Self {
self.generate_meta = enabled;
self
}
pub fn with_anim_files(mut self, enabled: bool) -> Self {
self.generate_anim_files = enabled;
self
}
pub fn with_json(mut self, enabled: bool) -> Self {
self.generate_json = enabled;
self
}
pub fn export_unity(
&self,
metadata: &AtlasMetadata,
output_path: &Path,
options: &UnityExportOptions,
) -> Result<Vec<std::path::PathBuf>, ExportError> {
let mut outputs = Vec::new();
let output_dir = output_path.parent().unwrap_or(Path::new("."));
fs::create_dir_all(output_dir)?;
if options.generate_json {
let data = self.build_atlas_data(metadata, options);
let json = if options.base.pretty {
serde_json::to_string_pretty(&data)?
} else {
serde_json::to_string(&data)?
};
let mut file = File::create(output_path)?;
file.write_all(json.as_bytes())?;
outputs.push(output_path.to_path_buf());
}
if options.generate_meta {
let meta_path = output_dir.join(format!("{}.meta", metadata.image));
let meta_content = self.generate_texture_meta(metadata, options);
let mut file = File::create(&meta_path)?;
file.write_all(meta_content.as_bytes())?;
outputs.push(meta_path);
}
if options.generate_anim_files && options.include_animations {
for (anim_name, anim) in &metadata.animations {
let anim_path = output_dir.join(format!("{}.anim", anim_name));
let anim_content = self.generate_animation_clip(anim_name, anim, metadata, options);
let mut file = File::create(&anim_path)?;
file.write_all(anim_content.as_bytes())?;
outputs.push(anim_path);
}
}
Ok(outputs)
}
fn generate_texture_meta(
&self,
metadata: &AtlasMetadata,
options: &UnityExportOptions,
) -> String {
let texture_height = metadata.size[1];
let guid = generate_guid(&metadata.image);
let mut sprites_yaml = String::new();
let mut sprite_entries: Vec<_> = metadata.frames.iter().collect();
sprite_entries.sort_by_key(|(name, _)| *name);
for (i, (name, frame)) in sprite_entries.iter().enumerate() {
let unity_y = texture_height - frame.y - frame.h;
let (pivot_x, pivot_y) = if let Some(origin) = frame.origin {
(origin[0] as f32 / frame.w as f32, 1.0 - (origin[1] as f32 / frame.h as f32))
} else {
(0.5, 0.0) };
let internal_id = 21300000 + (i as i64 * 2);
sprites_yaml.push_str(&format!(
r#" - serializedVersion: 2
name: {}
rect:
serializedVersion: 2
x: {}
y: {}
width: {}
height: {}
alignment: 9
pivot: {{x: {:.6}, y: {:.6}}}
border: {{x: 0, y: 0, z: 0, w: 0}}
outline: []
physicsShape: []
tessellationDetail: 0
bones: []
spriteID: {:016x}
internalID: {}
vertices: []
indices:
edges: []
weights: []
"#,
name,
frame.x,
unity_y,
frame.w,
frame.h,
pivot_x,
pivot_y,
generate_sprite_id(name),
internal_id
));
}
let filter_mode_value = match options.filter_mode {
UnityFilterMode::Point => 0,
UnityFilterMode::Bilinear => 1,
UnityFilterMode::Trilinear => 2,
};
format!(
r#"fileFormatVersion: 2
guid: {}
TextureImporter:
internalIDToNameTable: []
externalObjects: {{}}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: {}
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {{x: 0.5, y: 0.5}}
spritePixelsToUnits: {}
spriteBorder: {{x: 0, y: 0, z: 0, w: 0}}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
{} outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {{}}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
"#,
guid, filter_mode_value, options.pixels_per_unit, sprites_yaml
)
}
fn generate_animation_clip(
&self,
name: &str,
anim: &crate::atlas::AtlasAnimation,
metadata: &AtlasMetadata,
_options: &UnityExportOptions,
) -> String {
let guid = generate_guid(&metadata.image);
let fps = anim.fps as f32;
let frame_duration = 1.0 / fps;
let mut keyframes = String::new();
for (i, frame_name) in anim.frames.iter().enumerate() {
let time = i as f32 * frame_duration;
let sprite_index = metadata
.frames
.keys()
.collect::<Vec<_>>()
.iter()
.position(|k| *k == frame_name)
.unwrap_or(0);
let internal_id = 21300000 + (sprite_index as i64 * 2);
keyframes.push_str(&format!(
r#" - time: {:.6}
value: {{fileID: {}, guid: {}, type: 3}}
"#,
time, internal_id, guid
));
}
let stop_time = anim.frames.len() as f32 * frame_duration;
format!(
r#"%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!74 &7400000
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {{fileID: 0}}
m_PrefabInstance: {{fileID: 0}}
m_PrefabAsset: {{fileID: 0}}
m_Name: {}
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves: []
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves:
- curve:
{} attribute: m_Sprite
path:
classID: 212
script: {{fileID: 0}}
flags: 0
m_SampleRate: {}
m_WrapMode: 0
m_Bounds:
m_Center: {{x: 0, y: 0, z: 0}}
m_Extent: {{x: 0, y: 0, z: 0}}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 0
script: {{fileID: 0}}
typeID: 212
customType: 23
isPPtrCurve: 1
isIntCurve: 0
isSerializeReferenceCurve: 0
pptrCurveMapping:
- {{fileID: 0}}
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {{fileID: 0}}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: {:.6}
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves: []
m_EulerEditorCurves: []
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
"#,
name, keyframes, fps, stop_time
)
}
fn build_atlas_data(
&self,
metadata: &AtlasMetadata,
options: &UnityExportOptions,
) -> UnityAtlasData {
let texture_height = metadata.size[1] as f32;
let mut sprites: Vec<UnitySprite> = metadata
.frames
.iter()
.map(|(name, frame)| {
let pivot = if let Some(origin) = frame.origin {
UnityVector2 {
x: origin[0] as f32 / frame.w as f32,
y: 1.0 - (origin[1] as f32 / frame.h as f32),
}
} else {
UnityVector2 { x: 0.5, y: 0.0 }
};
UnitySprite {
name: name.clone(),
rect: UnityRect {
x: frame.x as f32,
y: texture_height - frame.y as f32 - frame.h as f32,
w: frame.w as f32,
h: frame.h as f32,
},
pivot,
border: UnityVector4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 },
}
})
.collect();
sprites.sort_by(|a, b| a.name.cmp(&b.name));
let animations: Vec<UnityAnimation> = if options.include_animations {
metadata
.animations
.iter()
.map(|(name, anim)| UnityAnimation {
name: name.clone(),
frame_rate: anim.fps,
sprites: anim.frames.clone(),
loop_animation: true,
})
.collect()
} else {
vec![]
};
UnityAtlasData {
texture: metadata.image.clone(),
texture_size: UnityVector2 { x: metadata.size[0] as f32, y: metadata.size[1] as f32 },
pixels_per_unit: options.pixels_per_unit,
filter_mode: options.filter_mode,
sprites,
animations,
}
}
pub fn export_to_string(
&self,
metadata: &AtlasMetadata,
options: &UnityExportOptions,
) -> Result<String, ExportError> {
let data = self.build_atlas_data(metadata, options);
let json = if options.base.pretty {
serde_json::to_string_pretty(&data)?
} else {
serde_json::to_string(&data)?
};
Ok(json)
}
}
impl Exporter for UnityExporter {
fn export(
&self,
metadata: &AtlasMetadata,
output_path: &Path,
options: &ExportOptions,
) -> Result<(), ExportError> {
let unity_options = UnityExportOptions {
base: options.clone(),
pixels_per_unit: self.pixels_per_unit,
filter_mode: self.filter_mode,
include_animations: self.include_animations,
generate_meta: self.generate_meta,
generate_anim_files: self.generate_anim_files,
generate_json: self.generate_json,
};
self.export_unity(metadata, output_path, &unity_options)?;
Ok(())
}
fn format_name(&self) -> &'static str {
"unity"
}
fn extension(&self) -> &'static str {
"json"
}
}
fn generate_guid(input: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
input.hash(&mut hasher);
let hash1 = hasher.finish();
let mut hasher2 = DefaultHasher::new();
format!("{}_guid", input).hash(&mut hasher2);
let hash2 = hasher2.finish();
format!("{:016x}{:016x}", hash1, hash2)
}
fn generate_sprite_id(name: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
name.hash(&mut hasher);
hasher.finish()
}
pub fn export_unity(
metadata: &AtlasMetadata,
output_path: &Path,
pixels_per_unit: u32,
) -> Result<Vec<std::path::PathBuf>, ExportError> {
let exporter = UnityExporter::new().with_pixels_per_unit(pixels_per_unit);
let options = UnityExportOptions { pixels_per_unit, ..Default::default() };
exporter.export_unity(metadata, output_path, &options)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atlas::{AtlasAnimation, AtlasFrame};
use std::collections::HashMap;
use tempfile::TempDir;
fn create_test_metadata() -> AtlasMetadata {
AtlasMetadata {
image: "sprites.png".to_string(),
size: [128, 128],
frames: HashMap::from([
(
"player_idle".to_string(),
AtlasFrame {
x: 0,
y: 0,
w: 32,
h: 32,
origin: Some([16, 32]), boxes: None,
},
),
(
"player_walk_1".to_string(),
AtlasFrame { x: 32, y: 0, w: 32, h: 32, origin: None, boxes: None },
),
(
"player_walk_2".to_string(),
AtlasFrame { x: 64, y: 0, w: 32, h: 32, origin: None, boxes: None },
),
]),
animations: HashMap::from([(
"walk".to_string(),
AtlasAnimation {
frames: vec!["player_walk_1".to_string(), "player_walk_2".to_string()],
fps: 10,
tags: None,
},
)]),
}
}
#[test]
fn test_unity_exporter_new() {
let exporter = UnityExporter::new();
assert_eq!(exporter.format_name(), "unity");
assert_eq!(exporter.extension(), "json");
assert_eq!(exporter.pixels_per_unit, 16);
}
#[test]
fn test_unity_exporter_with_options() {
let exporter = UnityExporter::new()
.with_pixels_per_unit(32)
.with_filter_mode(UnityFilterMode::Bilinear)
.with_animations(false);
assert_eq!(exporter.pixels_per_unit, 32);
assert_eq!(exporter.filter_mode, UnityFilterMode::Bilinear);
assert!(!exporter.include_animations);
}
#[test]
fn test_unity_filter_mode_default() {
let mode = UnityFilterMode::default();
assert_eq!(mode, UnityFilterMode::Point);
}
#[test]
fn test_unity_filter_mode_from_config() {
assert_eq!(
UnityFilterMode::from_config(&crate::config::FilterMode::Point),
UnityFilterMode::Point
);
assert_eq!(
UnityFilterMode::from_config(&crate::config::FilterMode::Bilinear),
UnityFilterMode::Bilinear
);
}
#[test]
fn test_export_to_string() {
let exporter = UnityExporter::new();
let metadata = create_test_metadata();
let options = UnityExportOptions::default();
let json = exporter.export_to_string(&metadata, &options).unwrap();
assert!(json.contains("\"texture\": \"sprites.png\""));
assert!(json.contains("\"pixelsPerUnit\": 16"));
assert!(json.contains("\"filterMode\": \"Point\""));
assert!(json.contains("\"sprites\""));
assert!(json.contains("\"animations\""));
}
#[test]
fn test_export_unity_creates_file() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
export_unity(&metadata, &output_path, 16).unwrap();
assert!(output_path.exists());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(content.contains("sprites.png"));
}
#[test]
fn test_export_sprite_rect() {
let exporter = UnityExporter::new();
let metadata = create_test_metadata();
let options = UnityExportOptions::default();
let json = exporter.export_to_string(&metadata, &options).unwrap();
let data: serde_json::Value = serde_json::from_str(&json).unwrap();
let sprites = data["sprites"].as_array().unwrap();
let player_idle = sprites.iter().find(|s| s["name"] == "player_idle").unwrap();
assert_eq!(player_idle["rect"]["x"], 0.0);
assert_eq!(player_idle["rect"]["y"], 96.0); assert_eq!(player_idle["rect"]["w"], 32.0);
assert_eq!(player_idle["rect"]["h"], 32.0);
}
#[test]
fn test_export_sprite_pivot_from_origin() {
let exporter = UnityExporter::new();
let metadata = create_test_metadata();
let options = UnityExportOptions::default();
let json = exporter.export_to_string(&metadata, &options).unwrap();
let data: serde_json::Value = serde_json::from_str(&json).unwrap();
let sprites = data["sprites"].as_array().unwrap();
let player_idle = sprites.iter().find(|s| s["name"] == "player_idle").unwrap();
assert_eq!(player_idle["pivot"]["x"], 0.5);
assert_eq!(player_idle["pivot"]["y"], 0.0);
}
#[test]
fn test_export_sprite_default_pivot() {
let exporter = UnityExporter::new();
let metadata = create_test_metadata();
let options = UnityExportOptions::default();
let json = exporter.export_to_string(&metadata, &options).unwrap();
let data: serde_json::Value = serde_json::from_str(&json).unwrap();
let sprites = data["sprites"].as_array().unwrap();
let walk_1 = sprites.iter().find(|s| s["name"] == "player_walk_1").unwrap();
assert_eq!(walk_1["pivot"]["x"], 0.5);
assert_eq!(walk_1["pivot"]["y"], 0.0);
}
#[test]
fn test_export_animations() {
let exporter = UnityExporter::new();
let metadata = create_test_metadata();
let options = UnityExportOptions::default();
let json = exporter.export_to_string(&metadata, &options).unwrap();
let data: serde_json::Value = serde_json::from_str(&json).unwrap();
let animations = data["animations"].as_array().unwrap();
assert_eq!(animations.len(), 1);
let walk = &animations[0];
assert_eq!(walk["name"], "walk");
assert_eq!(walk["frameRate"], 10);
assert_eq!(walk["loopAnimation"], true);
let sprites = walk["sprites"].as_array().unwrap();
assert_eq!(sprites.len(), 2);
}
#[test]
fn test_export_without_animations() {
let exporter = UnityExporter::new().with_animations(false);
let metadata = create_test_metadata();
let options = UnityExportOptions { include_animations: false, ..Default::default() };
let json = exporter.export_to_string(&metadata, &options).unwrap();
assert!(!json.contains("\"animations\""));
}
#[test]
fn test_export_custom_pixels_per_unit() {
let exporter = UnityExporter::new().with_pixels_per_unit(100);
let metadata = create_test_metadata();
let options = UnityExportOptions { pixels_per_unit: 100, ..Default::default() };
let json = exporter.export_to_string(&metadata, &options).unwrap();
assert!(json.contains("\"pixelsPerUnit\": 100"));
}
#[test]
fn test_export_bilinear_filter() {
let exporter = UnityExporter::new().with_filter_mode(UnityFilterMode::Bilinear);
let metadata = create_test_metadata();
let options =
UnityExportOptions { filter_mode: UnityFilterMode::Bilinear, ..Default::default() };
let json = exporter.export_to_string(&metadata, &options).unwrap();
assert!(json.contains("\"filterMode\": \"Bilinear\""));
}
#[test]
fn test_export_via_trait() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let exporter = UnityExporter::new();
let options = ExportOptions::default();
exporter.export(&metadata, &output_path, &options).unwrap();
assert!(output_path.exists());
}
#[test]
fn test_sprites_sorted_by_name() {
let exporter = UnityExporter::new();
let metadata = create_test_metadata();
let options = UnityExportOptions::default();
let json = exporter.export_to_string(&metadata, &options).unwrap();
let data: serde_json::Value = serde_json::from_str(&json).unwrap();
let sprites = data["sprites"].as_array().unwrap();
let names: Vec<&str> = sprites.iter().map(|s| s["name"].as_str().unwrap()).collect();
assert_eq!(names, vec!["player_idle", "player_walk_1", "player_walk_2"]);
}
#[test]
fn test_unity_export_options_default() {
let options = UnityExportOptions::default();
assert_eq!(options.pixels_per_unit, 16);
assert_eq!(options.filter_mode, UnityFilterMode::Point);
assert!(options.include_animations);
assert!(options.generate_meta);
assert!(options.generate_anim_files);
assert!(options.generate_json);
}
#[test]
fn test_exporter_with_meta_file_option() {
let exporter = UnityExporter::new().with_meta_file(false);
assert!(!exporter.generate_meta);
}
#[test]
fn test_exporter_with_anim_files_option() {
let exporter = UnityExporter::new().with_anim_files(false);
assert!(!exporter.generate_anim_files);
}
#[test]
fn test_exporter_with_json_option() {
let exporter = UnityExporter::new().with_json(false);
assert!(!exporter.generate_json);
}
#[test]
fn test_export_generates_meta_file() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let outputs = export_unity(&metadata, &output_path, 16).unwrap();
let meta_path = temp.path().join("sprites.png.meta");
assert!(meta_path.exists());
assert!(outputs.iter().any(|p| p == &meta_path));
let content = std::fs::read_to_string(&meta_path).unwrap();
assert!(content.contains("fileFormatVersion: 2"));
assert!(content.contains("TextureImporter:"));
assert!(content.contains("spriteMode: 2"));
assert!(content.contains("spritePixelsToUnits: 16"));
assert!(content.contains("player_idle"));
assert!(content.contains("player_walk_1"));
assert!(content.contains("player_walk_2"));
}
#[test]
fn test_export_generates_anim_file() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let outputs = export_unity(&metadata, &output_path, 16).unwrap();
let anim_path = temp.path().join("walk.anim");
assert!(anim_path.exists());
assert!(outputs.iter().any(|p| p == &anim_path));
let content = std::fs::read_to_string(&anim_path).unwrap();
assert!(content.contains("%YAML 1.1"));
assert!(content.contains("AnimationClip:"));
assert!(content.contains("m_Name: walk"));
assert!(content.contains("m_SampleRate: 10"));
assert!(content.contains("m_LoopTime: 1"));
}
#[test]
fn test_meta_file_sprite_coordinates() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
export_unity(&metadata, &output_path, 16).unwrap();
let meta_path = temp.path().join("sprites.png.meta");
let content = std::fs::read_to_string(&meta_path).unwrap();
assert!(content.contains("x: 0"));
assert!(content.contains("y: 96")); assert!(content.contains("width: 32"));
assert!(content.contains("height: 32"));
}
#[test]
fn test_meta_file_ppu_setting() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
export_unity(&metadata, &output_path, 100).unwrap();
let meta_path = temp.path().join("sprites.png.meta");
let content = std::fs::read_to_string(&meta_path).unwrap();
assert!(content.contains("spritePixelsToUnits: 100"));
}
#[test]
fn test_meta_file_filter_mode_bilinear() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let exporter = UnityExporter::new().with_filter_mode(UnityFilterMode::Bilinear);
let options =
UnityExportOptions { filter_mode: UnityFilterMode::Bilinear, ..Default::default() };
exporter.export_unity(&metadata, &output_path, &options).unwrap();
let meta_path = temp.path().join("sprites.png.meta");
let content = std::fs::read_to_string(&meta_path).unwrap();
assert!(content.contains("filterMode: 1"));
}
#[test]
fn test_export_without_meta_file() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let exporter = UnityExporter::new().with_meta_file(false);
let options = UnityExportOptions { generate_meta: false, ..Default::default() };
let outputs = exporter.export_unity(&metadata, &output_path, &options).unwrap();
let meta_path = temp.path().join("sprites.png.meta");
assert!(!meta_path.exists());
assert!(!outputs.iter().any(|p| p.extension().is_some_and(|e| e == "meta")));
}
#[test]
fn test_export_without_anim_files() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let exporter = UnityExporter::new().with_anim_files(false);
let options = UnityExportOptions { generate_anim_files: false, ..Default::default() };
let outputs = exporter.export_unity(&metadata, &output_path, &options).unwrap();
let anim_path = temp.path().join("walk.anim");
assert!(!anim_path.exists());
assert!(!outputs.iter().any(|p| p.extension().is_some_and(|e| e == "anim")));
}
#[test]
fn test_export_without_json() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
let exporter = UnityExporter::new().with_json(false);
let options = UnityExportOptions { generate_json: false, ..Default::default() };
let outputs = exporter.export_unity(&metadata, &output_path, &options).unwrap();
assert!(!output_path.exists());
assert!(temp.path().join("sprites.png.meta").exists());
assert!(temp.path().join("walk.anim").exists());
assert!(!outputs.iter().any(|p| p == &output_path));
}
#[test]
fn test_generate_guid_deterministic() {
let guid1 = generate_guid("test.png");
let guid2 = generate_guid("test.png");
assert_eq!(guid1, guid2);
assert_eq!(guid1.len(), 32);
let guid3 = generate_guid("other.png");
assert_ne!(guid1, guid3);
}
#[test]
fn test_generate_sprite_id_deterministic() {
let id1 = generate_sprite_id("player_idle");
let id2 = generate_sprite_id("player_idle");
assert_eq!(id1, id2);
let id3 = generate_sprite_id("player_walk");
assert_ne!(id1, id3);
}
#[test]
fn test_meta_file_sprite_pivot() {
let temp = TempDir::new().unwrap();
let output_path = temp.path().join("atlas.json");
let metadata = create_test_metadata();
export_unity(&metadata, &output_path, 16).unwrap();
let meta_path = temp.path().join("sprites.png.meta");
let content = std::fs::read_to_string(&meta_path).unwrap();
assert!(content.contains("pivot: {x: 0.500000, y: 0.000000}"));
}
}