using UnityEngine;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine.U2D;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using ArtResource;
using Thrift;
using Thrift.Protocol;
using Thrift.Transport;
using Thrift.Transport.Client;
namespace ArtTools
{
///
/// 图集构建工具
///
public class AtlasBuilderEditor : EditorWindow
{
private const string ATLAS_ROOT_PATH = "Assets/Art_SubModule/Art_Atlas";
private const string SPRITE_ROOT_PATH = "Assets/Art_SubModule";
private const string CONFIG_PATH = "Assets/Art_SubModule/Art_Atlas/AtlasConfig.json";
private const string BYTES_PATH = "Assets/Art_SubModule/Art_Bytes/ArtAtlasConfig.bytes";
private enum MenuState
{
Normal,
Add,
Rename,
Remove
}
// 数据
private AtlasConfig config;
private AtlasData selectedAtlas;
private SpriteFolder spriteRoot;
// UI状态
private MenuState menuState = MenuState.Normal;
private Vector2 atlasListScroll = Vector2.zero;
private Vector2 atlasContentScroll = Vector2.zero;
private Vector2 spriteListScroll = Vector2.zero;
// 选择状态
private HashSet selectedSpritesInAtlas = new HashSet();
private HashSet expandedSpriteFolders = new HashSet();
private HashSet selectedSpriteAssets = new HashSet();
private HashSet selectedSpriteFolders = new HashSet();
// 缓存
private HashSet cachedAssignedSprites = new HashSet();
private HashSet cachedAssignedFolders = new HashSet();
// 输入
private string inputAtlasName = "";
private bool hideAssignedSprites = false;
// 绘制计数
private int currentAtlasRowOnDraw = 0;
private int currentSpriteRowOnDraw = 0;
[MenuItem("程序工具/图集构建工具")]
public static void ShowWindow()
{
var window = GetWindow("图集构建工具");
window.minSize = new Vector2(1400, 600);
window.Show();
}
private void OnEnable()
{
LoadConfig();
RefreshSpriteTree();
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(GUILayout.Width(position.width), GUILayout.Height(position.height));
{
GUILayout.Space(2f);
// 左侧:图集列表
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.25f));
{
GUILayout.Space(5f);
EditorGUILayout.LabelField($"图集列表 ({config.Atlases.Count})", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height - 52f));
{
DrawAtlasList();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
{
GUILayout.Space(5f);
DrawAtlasListMenu();
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
// 中间:图集内容
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.25f));
{
GUILayout.Space(5f);
EditorGUILayout.LabelField($"图集内容 ({(selectedAtlas != null ? selectedAtlas.SpritePaths.Count : 0)})", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height - 52f));
{
DrawAtlasContent();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
{
GUILayout.Space(5f);
DrawAtlasContentMenu();
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
// 右侧:Sprite列表
EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.5f - 16f));
{
GUILayout.Space(5f);
EditorGUILayout.LabelField("Sprite列表", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal("box", GUILayout.Height(position.height - 52f));
{
DrawSpriteList();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
{
GUILayout.Space(5f);
DrawSpriteListMenu();
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
GUILayout.Space(5f);
}
EditorGUILayout.EndHorizontal();
}
// ==================== 绘制方法 ====================
private void DrawAtlasList()
{
currentAtlasRowOnDraw = 0;
atlasListScroll = EditorGUILayout.BeginScrollView(atlasListScroll);
{
if (config != null && config.Atlases != null)
{
foreach (var atlas in config.Atlases)
{
DrawAtlasItem(atlas);
}
}
}
EditorGUILayout.EndScrollView();
}
private void DrawAtlasItem(AtlasData atlas)
{
EditorGUILayout.BeginHorizontal();
{
bool isSelected = selectedAtlas == atlas;
float emptySpace = position.width * 0.25f;
if (EditorGUILayout.Toggle(isSelected, GUILayout.Width(emptySpace - 12f)))
{
ChangeSelectedAtlas(atlas);
}
else if (isSelected)
{
ChangeSelectedAtlas(null);
}
GUILayout.Space(-emptySpace + 24f);
Texture2D icon = EditorGUIUtility.FindTexture("SpriteAtlas Icon");
GUI.DrawTexture(new Rect(32f, 20f * currentAtlasRowOnDraw + 3f, 16f, 16f), icon);
GUILayout.Space(20f);
EditorGUILayout.LabelField($"{atlas.Name} ({atlas.SpritePaths.Count})");
}
EditorGUILayout.EndHorizontal();
currentAtlasRowOnDraw++;
}
private void DrawAtlasListMenu()
{
switch (menuState)
{
case MenuState.Normal:
DrawAtlasListMenu_Normal();
break;
case MenuState.Add:
DrawAtlasListMenu_Add();
break;
case MenuState.Rename:
DrawAtlasListMenu_Rename();
break;
case MenuState.Remove:
DrawAtlasListMenu_Remove();
break;
}
}
private void DrawAtlasListMenu_Normal()
{
if (GUILayout.Button("添加", GUILayout.Width(65f)))
{
menuState = MenuState.Add;
inputAtlasName = "";
GUI.FocusControl(null);
}
EditorGUI.BeginDisabledGroup(selectedAtlas == null);
{
if (GUILayout.Button("重命名", GUILayout.Width(65f)))
{
menuState = MenuState.Rename;
inputAtlasName = selectedAtlas != null ? selectedAtlas.Name : "";
GUI.FocusControl(null);
}
if (GUILayout.Button("移除", GUILayout.Width(65f)))
{
menuState = MenuState.Remove;
}
}
EditorGUI.EndDisabledGroup();
}
private void DrawAtlasListMenu_Add()
{
GUI.SetNextControlName("NewAtlasNameTextField");
inputAtlasName = EditorGUILayout.TextField(inputAtlasName);
if (GUI.GetNameOfFocusedControl() == "NewAtlasNameTextField")
{
if (Event.current.isKey && Event.current.keyCode == KeyCode.Return)
{
AddAtlas(inputAtlasName);
Repaint();
}
}
if (GUILayout.Button("添加", GUILayout.Width(50f)))
{
AddAtlas(inputAtlasName);
}
if (GUILayout.Button("取消", GUILayout.Width(50f)))
{
menuState = MenuState.Normal;
}
}
private void DrawAtlasListMenu_Rename()
{
if (selectedAtlas == null)
{
menuState = MenuState.Normal;
return;
}
GUI.SetNextControlName("RenameAtlasNameTextField");
inputAtlasName = EditorGUILayout.TextField(inputAtlasName);
if (GUI.GetNameOfFocusedControl() == "RenameAtlasNameTextField")
{
if (Event.current.isKey && Event.current.keyCode == KeyCode.Return)
{
RenameAtlas(selectedAtlas, inputAtlasName);
Repaint();
}
}
if (GUILayout.Button("确定", GUILayout.Width(50f)))
{
RenameAtlas(selectedAtlas, inputAtlasName);
}
if (GUILayout.Button("取消", GUILayout.Width(50f)))
{
menuState = MenuState.Normal;
}
}
private void DrawAtlasListMenu_Remove()
{
if (selectedAtlas == null)
{
menuState = MenuState.Normal;
return;
}
GUILayout.Label($"移除图集 '{selectedAtlas.Name}' ?");
if (GUILayout.Button("确定", GUILayout.Width(50f)))
{
RemoveAtlas();
menuState = MenuState.Normal;
}
if (GUILayout.Button("取消", GUILayout.Width(50f)))
{
menuState = MenuState.Normal;
}
}
private void DrawAtlasContent()
{
atlasContentScroll = EditorGUILayout.BeginScrollView(atlasContentScroll);
{
if (selectedAtlas != null)
{
foreach (string spritePath in selectedAtlas.SpritePaths)
{
EditorGUILayout.BeginHorizontal();
{
bool select = selectedSpritesInAtlas.Contains(spritePath);
float emptySpace = position.width * 0.25f;
if (select != EditorGUILayout.Toggle(select, GUILayout.Width(emptySpace - 12f)))
{
if (select)
selectedSpritesInAtlas.Remove(spritePath);
else
selectedSpritesInAtlas.Add(spritePath);
}
GUILayout.Space(-emptySpace + 24f);
Sprite sprite = AssetDatabase.LoadAssetAtPath(spritePath);
// 正确处理图集sprite的预览
Rect iconRect = new Rect(32f, 20f * (selectedAtlas.SpritePaths.IndexOf(spritePath)) + 3f, 16f, 16f);
if (sprite != null && sprite.texture != null)
{
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
GUI.DrawTextureWithTexCoords(iconRect, tex, normalizedCoords, true);
}
else
{
Texture2D icon = EditorGUIUtility.FindTexture("Sprite Icon");
GUI.DrawTexture(iconRect, icon);
}
EditorGUILayout.LabelField(string.Empty, GUILayout.Width(26f), GUILayout.Height(18f));
EditorGUILayout.LabelField(sprite != null ? sprite.name : Path.GetFileName(spritePath));
}
EditorGUILayout.EndHorizontal();
}
}
}
EditorGUILayout.EndScrollView();
}
private void DrawAtlasContentMenu()
{
if (GUILayout.Button("全选", GUILayout.Width(50f)) && selectedAtlas != null)
{
selectedSpritesInAtlas.Clear();
foreach (string path in selectedAtlas.SpritePaths)
{
selectedSpritesInAtlas.Add(path);
}
}
if (GUILayout.Button("全不选", GUILayout.Width(60f)))
{
selectedSpritesInAtlas.Clear();
}
GUILayout.Label(string.Empty);
EditorGUI.BeginDisabledGroup(selectedAtlas == null || selectedSpritesInAtlas.Count <= 0);
{
if (GUILayout.Button($"{selectedSpritesInAtlas.Count} >>", GUILayout.Width(80f)))
{
foreach (string path in selectedSpritesInAtlas.ToList())
{
selectedAtlas.SpritePaths.Remove(path);
}
selectedSpritesInAtlas.Clear();
RefreshAssignedCache();
}
}
EditorGUI.EndDisabledGroup();
}
private void DrawSpriteList()
{
currentSpriteRowOnDraw = 0;
spriteListScroll = EditorGUILayout.BeginScrollView(spriteListScroll);
{
if (spriteRoot != null)
{
DrawSpriteFolder(spriteRoot);
}
}
EditorGUILayout.EndScrollView();
}
private void DrawSpriteFolder(SpriteFolder folder)
{
// 跳过没有可见Sprite的文件夹(HasVisibleSprites已经考虑了hideAssignedSprites)
if (!HasVisibleSprites(folder))
{
return;
}
EditorGUILayout.BeginHorizontal();
{
bool select = IsSelectedFolder(folder);
if (select != EditorGUILayout.Toggle(select, GUILayout.Width(12f + 14f * folder.Depth)))
{
SetSelectedFolder(folder, !select);
}
GUILayout.Space(-14f * folder.Depth);
bool expand = expandedSpriteFolders.Contains(folder);
bool foldout = EditorGUI.Foldout(new Rect(18f + 14f * folder.Depth, 20f * currentSpriteRowOnDraw + 4f, int.MaxValue, 14f), expand, string.Empty, true);
if (expand != foldout)
{
if (foldout)
expandedSpriteFolders.Add(folder);
else
expandedSpriteFolders.Remove(folder);
}
Texture2D icon = EditorGUIUtility.FindTexture("Folder Icon");
GUI.DrawTexture(new Rect(32f + 14f * folder.Depth, 20f * currentSpriteRowOnDraw + 3f, 16f, 16f), icon);
EditorGUILayout.LabelField(string.Empty, GUILayout.Width(30f + 14f * folder.Depth), GUILayout.Height(18f));
EditorGUILayout.LabelField(folder.Name);
}
EditorGUILayout.EndHorizontal();
currentSpriteRowOnDraw++;
if (expandedSpriteFolders.Contains(folder))
{
foreach (var subFolder in folder.SubFolders)
{
DrawSpriteFolder(subFolder);
}
foreach (var sprite in folder.Sprites)
{
DrawSpriteAsset(sprite);
}
}
}
private void DrawSpriteAsset(SpriteAsset spriteAsset)
{
if (hideAssignedSprites && IsAssignedSprite(spriteAsset))
{
return;
}
EditorGUILayout.BeginHorizontal();
{
bool select = selectedSpriteAssets.Contains(spriteAsset);
float emptySpace = position.width * 0.5f - 16f;
if (select != EditorGUILayout.Toggle(select, GUILayout.Width(emptySpace - 12f)))
{
SetSelectedSprite(spriteAsset, !select);
}
GUILayout.Space(-emptySpace + 24f);
Sprite sprite = AssetDatabase.LoadAssetAtPath(spriteAsset.Path);
// 正确处理图集sprite的预览
Rect iconRect = new Rect(32f + 14f * spriteAsset.Depth, 20f * currentSpriteRowOnDraw + 3f, 16f, 16f);
if (sprite != null && sprite.texture != null)
{
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
GUI.DrawTextureWithTexCoords(iconRect, tex, normalizedCoords, true);
}
else
{
Texture2D icon = EditorGUIUtility.FindTexture("Sprite Icon");
GUI.DrawTexture(iconRect, icon);
}
EditorGUILayout.LabelField(string.Empty, GUILayout.Width(26f + 14f * spriteAsset.Depth), GUILayout.Height(18f));
EditorGUILayout.LabelField(spriteAsset.Name);
}
EditorGUILayout.EndHorizontal();
currentSpriteRowOnDraw++;
}
private void DrawSpriteListMenu()
{
HashSet selectedSprites = GetSelectedSprites();
EditorGUI.BeginDisabledGroup(selectedAtlas == null || selectedSprites.Count <= 0);
{
if (GUILayout.Button($"<< {selectedSprites.Count}", GUILayout.Width(80f)))
{
foreach (var sprite in selectedSprites)
{
if (!selectedAtlas.SpritePaths.Contains(sprite.Path))
{
selectedAtlas.SpritePaths.Add(sprite.Path);
}
}
selectedSpriteAssets.Clear();
selectedSpriteFolders.Clear();
RefreshAssignedCache();
}
}
EditorGUI.EndDisabledGroup();
bool newHideState = EditorGUILayout.ToggleLeft("隐藏在图集中的sprite", hideAssignedSprites, GUILayout.Width(180f));
if (newHideState != hideAssignedSprites)
{
hideAssignedSprites = newHideState;
RefreshAssignedCache();
}
GUILayout.Label(string.Empty);
if (GUILayout.Button("清除丢失引用", GUILayout.Width(100f)))
{
CleanMissingReferences();
}
if (GUILayout.Button("保存", GUILayout.Width(80f)))
{
SaveAndBuildAllAtlases();
}
}
// ==================== 逻辑方法 ====================
private void ChangeSelectedAtlas(AtlasData atlas)
{
if (selectedAtlas == atlas) return;
selectedAtlas = atlas;
selectedSpritesInAtlas.Clear();
RefreshAssignedCache();
}
private void AddAtlas(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
EditorUtility.DisplayDialog("错误", "图集名称不能为空", "确定");
return;
}
if (config.Atlases.Any(a => a.Name == name))
{
EditorUtility.DisplayDialog("错误", $"图集 '{name}' 已存在", "确定");
return;
}
config.Atlases.Add(new AtlasData
{
Name = name,
SpritePaths = new List()
});
menuState = MenuState.Normal;
selectedAtlas = config.Atlases[config.Atlases.Count - 1];
}
private void RenameAtlas(AtlasData atlas, string newName)
{
if (string.IsNullOrWhiteSpace(newName))
{
EditorUtility.DisplayDialog("错误", "图集名称不能为空", "确定");
return;
}
if (config.Atlases.Any(a => a != atlas && a.Name == newName))
{
EditorUtility.DisplayDialog("错误", $"图集 '{newName}' 已存在", "确定");
return;
}
string oldPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlas";
string newPath = $"{ATLAS_ROOT_PATH}/{newName}.spriteatlas";
if (File.Exists(oldPath))
{
AssetDatabase.MoveAsset(oldPath, newPath);
}
atlas.Name = newName;
menuState = MenuState.Normal;
}
private void RemoveAtlas()
{
if (selectedAtlas == null) return;
string atlasPath = $"{ATLAS_ROOT_PATH}/{selectedAtlas.Name}.spriteatlas";
if (File.Exists(atlasPath))
{
AssetDatabase.DeleteAsset(atlasPath);
}
config.Atlases.Remove(selectedAtlas);
selectedAtlas = null;
selectedSpritesInAtlas.Clear();
}
private void CleanMissingReferences()
{
int cleanedCount = 0;
foreach (var atlas in config.Atlases)
{
int originalCount = atlas.SpritePaths.Count;
atlas.SpritePaths.RemoveAll(path => !File.Exists(path));
cleanedCount += originalCount - atlas.SpritePaths.Count;
}
if (cleanedCount > 0)
{
RefreshAssignedCache();
EditorUtility.DisplayDialog("提示", $"已清除 {cleanedCount} 个丢失的引用", "确定");
}
else
{
EditorUtility.DisplayDialog("提示", "没有发现丢失的引用", "确定");
}
}
private void SaveAndBuildAllAtlases()
{
SaveConfig();
EditorUtility.DisplayProgressBar("构建图集", "开始构建...", 0f);
int count = config.Atlases.Count;
for (int i = 0; i < count; i++)
{
var atlas = config.Atlases[i];
EditorUtility.DisplayProgressBar("构建图集", $"构建 {atlas.Name}...", (float)i / count);
BuildAtlas(atlas);
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("提示", $"已构建 {count} 个图集并保存配置", "确定");
}
private void BuildAtlas(AtlasData atlasData)
{
if (!Directory.Exists(ATLAS_ROOT_PATH))
{
Directory.CreateDirectory(ATLAS_ROOT_PATH);
}
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlasData.Name}.spriteatlas";
SpriteAtlas spriteAtlas = AssetDatabase.LoadAssetAtPath(atlasPath);
bool isNewAtlas = spriteAtlas == null;
if (isNewAtlas)
{
spriteAtlas = new SpriteAtlas();
AssetDatabase.CreateAsset(spriteAtlas, atlasPath);
// 只对新建的图集设置默认参数
SpriteAtlasPackingSettings packingSettings = new SpriteAtlasPackingSettings()
{
blockOffset = 1,
enableRotation = false,
enableTightPacking = false,
padding = 2
};
spriteAtlas.SetPackingSettings(packingSettings);
SpriteAtlasTextureSettings textureSettings = new SpriteAtlasTextureSettings()
{
readable = false,
generateMipMaps = false,
sRGB = true,
filterMode = FilterMode.Bilinear
};
spriteAtlas.SetTextureSettings(textureSettings);
TextureImporterPlatformSettings platformSettings = new TextureImporterPlatformSettings()
{
maxTextureSize = 2048,
format = TextureImporterFormat.Automatic,
textureCompression = TextureImporterCompression.Compressed
};
spriteAtlas.SetPlatformSettings(platformSettings);
}
// 更新 Objects for Packing(对所有图集都执行)
spriteAtlas.Remove(spriteAtlas.GetPackables());
var validSprites = atlasData.SpritePaths
.Select(path => AssetDatabase.LoadAssetAtPath(path))
.Where(sprite => sprite != null)
.Cast