1130 lines
40 KiB
C#
1130 lines
40 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using UnityEditor.U2D;
|
||
using UnityEngine.U2D;
|
||
using System.IO;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using ArtResource;
|
||
using Thrift;
|
||
using Thrift.Protocol;
|
||
using Thrift.Transport;
|
||
using Thrift.Transport.Client;
|
||
|
||
namespace ArtTools
|
||
{
|
||
/// <summary>
|
||
/// 图集构建工具
|
||
/// </summary>
|
||
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<string> selectedSpritesInAtlas = new HashSet<string>();
|
||
private HashSet<SpriteFolder> expandedSpriteFolders = new HashSet<SpriteFolder>();
|
||
private HashSet<SpriteAsset> selectedSpriteAssets = new HashSet<SpriteAsset>();
|
||
private HashSet<SpriteFolder> selectedSpriteFolders = new HashSet<SpriteFolder>();
|
||
|
||
// 缓存
|
||
private HashSet<SpriteAsset> cachedAssignedSprites = new HashSet<SpriteAsset>();
|
||
private HashSet<SpriteFolder> cachedAssignedFolders = new HashSet<SpriteFolder>();
|
||
|
||
// 输入
|
||
private string inputAtlasName = "";
|
||
private bool hideAssignedSprites = false;
|
||
|
||
// 绘制计数
|
||
private int currentAtlasRowOnDraw = 0;
|
||
private int currentSpriteRowOnDraw = 0;
|
||
|
||
[MenuItem("程序工具/图集构建工具")]
|
||
public static void ShowWindow()
|
||
{
|
||
var window = GetWindow<AtlasBuilderEditor>("图集构建工具");
|
||
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<Sprite>(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<Sprite>(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<SpriteAsset> 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<string>()
|
||
});
|
||
|
||
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}.spriteatlasv2";
|
||
string newPath = $"{ATLAS_ROOT_PATH}/{newName}.spriteatlasv2";
|
||
|
||
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}.spriteatlasv2";
|
||
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();
|
||
|
||
// 清理旧版 .spriteatlas 文件
|
||
CleanOldSpriteAtlasFiles();
|
||
|
||
if (!Directory.Exists(ATLAS_ROOT_PATH))
|
||
{
|
||
Directory.CreateDirectory(ATLAS_ROOT_PATH);
|
||
}
|
||
|
||
EditorUtility.DisplayProgressBar("构建图集", "生成图集文件...", 0f);
|
||
|
||
int count = config.Atlases.Count;
|
||
|
||
// 第一阶段:为每个图集写入 .spriteatlasv2 YAML 文件并逐个同步导入
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var atlas = config.Atlases[i];
|
||
EditorUtility.DisplayProgressBar("构建图集", $"生成 {atlas.Name}...", (float)i / count * 0.5f);
|
||
|
||
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlasv2";
|
||
WriteV2AtlasFile(atlas);
|
||
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport);
|
||
}
|
||
|
||
// 第二阶段:统一打包
|
||
EditorUtility.DisplayProgressBar("构建图集", "打包图集...", 0.6f);
|
||
|
||
List<SpriteAtlas> allAtlases = new List<SpriteAtlas>();
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
var atlas = config.Atlases[i];
|
||
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlas.Name}.spriteatlasv2";
|
||
SpriteAtlas spriteAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasPath);
|
||
if (spriteAtlas != null)
|
||
{
|
||
allAtlases.Add(spriteAtlas);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[AtlasBuilderEditor] 图集加载失败,跳过打包: {atlasPath}");
|
||
}
|
||
}
|
||
|
||
if (allAtlases.Count > 0)
|
||
{
|
||
SpriteAtlasUtility.PackAtlases(allAtlases.ToArray(), EditorUserBuildSettings.activeBuildTarget);
|
||
}
|
||
|
||
// 第三阶段:重新生成 atlas_mapping.bytes(运行时通过此文件查找 sprite→atlas 映射)
|
||
EditorUtility.DisplayProgressBar("构建图集", "生成 Atlas Mapping...", 0.9f);
|
||
AtlasMapper.Mapping();
|
||
|
||
AssetDatabase.Refresh();
|
||
EditorUtility.ClearProgressBar();
|
||
EditorUtility.DisplayDialog("提示", $"已构建 {allAtlases.Count} 个图集并保存配置\n已重新生成 Atlas Mapping", "确定");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 直接写入 .spriteatlasv2 YAML 文件(V2 格式)
|
||
/// </summary>
|
||
private void WriteV2AtlasFile(AtlasData atlasData)
|
||
{
|
||
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlasData.Name}.spriteatlasv2";
|
||
|
||
// 收集所有有效 Sprite 的 GUID 和 localFileID
|
||
var packableEntries = new List<string>();
|
||
foreach (string spritePath in atlasData.SpritePaths)
|
||
{
|
||
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spritePath);
|
||
if (sprite != null)
|
||
{
|
||
string guid;
|
||
long localId;
|
||
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sprite, out guid, out localId))
|
||
{
|
||
packableEntries.Add($" - {{fileID: {localId}, guid: {guid}, type: 3}}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建 YAML
|
||
var sb = new StringBuilder();
|
||
sb.Append("%YAML 1.1\n");
|
||
sb.Append("%TAG !u! tag:unity3d.com,2011:\n");
|
||
sb.Append("--- !u!612988286 &1\n");
|
||
sb.Append("SpriteAtlasAsset:\n");
|
||
sb.Append(" m_ObjectHideFlags: 0\n");
|
||
sb.Append(" m_CorrespondingSourceObject: {fileID: 0}\n");
|
||
sb.Append(" m_PrefabInstance: {fileID: 0}\n");
|
||
sb.Append(" m_PrefabAsset: {fileID: 0}\n");
|
||
sb.Append(" m_Name:\n");
|
||
sb.Append(" serializedVersion: 2\n");
|
||
sb.Append(" m_MasterAtlas: {fileID: 0}\n");
|
||
sb.Append(" m_ImporterData:\n");
|
||
|
||
if (packableEntries.Count > 0)
|
||
{
|
||
sb.Append(" packables:\n");
|
||
foreach (string entry in packableEntries)
|
||
{
|
||
sb.Append(entry).Append("\n");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
sb.Append(" packables: []\n");
|
||
}
|
||
|
||
sb.Append(" m_IsVariant: 0\n");
|
||
|
||
File.WriteAllText(atlasPath, sb.ToString());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理旧版 .spriteatlas 文件(V1格式),只保留 .spriteatlasv2
|
||
/// </summary>
|
||
private void CleanOldSpriteAtlasFiles()
|
||
{
|
||
if (!Directory.Exists(ATLAS_ROOT_PATH)) return;
|
||
|
||
string[] oldFiles = Directory.GetFiles(ATLAS_ROOT_PATH, "*.spriteatlas")
|
||
.Where(f => !f.EndsWith(".spriteatlasv2") && !f.EndsWith(".meta"))
|
||
.ToArray();
|
||
|
||
foreach (string oldFile in oldFiles)
|
||
{
|
||
string assetPath = oldFile.Replace("\\", "/");
|
||
AssetDatabase.DeleteAsset(assetPath);
|
||
}
|
||
|
||
if (oldFiles.Length > 0)
|
||
{
|
||
Debug.Log($"[AtlasBuilderEditor] 已清理 {oldFiles.Length} 个旧版 .spriteatlas 文件");
|
||
}
|
||
}
|
||
|
||
|
||
private void BuildAtlas(AtlasData atlasData)
|
||
{
|
||
if (!Directory.Exists(ATLAS_ROOT_PATH))
|
||
{
|
||
Directory.CreateDirectory(ATLAS_ROOT_PATH);
|
||
}
|
||
|
||
string atlasPath = $"{ATLAS_ROOT_PATH}/{atlasData.Name}.spriteatlasv2";
|
||
WriteV2AtlasFile(atlasData);
|
||
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceSynchronousImport);
|
||
|
||
SpriteAtlas spriteAtlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(atlasPath);
|
||
if (spriteAtlas != null)
|
||
{
|
||
SpriteAtlasUtility.PackAtlases(new[] { spriteAtlas }, EditorUserBuildSettings.activeBuildTarget);
|
||
}
|
||
}
|
||
|
||
private void LoadConfig()
|
||
{
|
||
if (File.Exists(CONFIG_PATH))
|
||
{
|
||
string json = File.ReadAllText(CONFIG_PATH);
|
||
config = JsonUtility.FromJson<AtlasConfig>(json);
|
||
}
|
||
else
|
||
{
|
||
config = new AtlasConfig { Atlases = new List<AtlasData>() };
|
||
SaveConfig();
|
||
}
|
||
}
|
||
|
||
private void SaveConfig()
|
||
{
|
||
// 保存JSON配置
|
||
string dir = Path.GetDirectoryName(CONFIG_PATH);
|
||
if (!Directory.Exists(dir))
|
||
{
|
||
Directory.CreateDirectory(dir);
|
||
}
|
||
|
||
string json = JsonUtility.ToJson(config, true);
|
||
File.WriteAllText(CONFIG_PATH, json);
|
||
|
||
// 生成Thrift Bytes配置
|
||
SaveConfigToThriftBytes();
|
||
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存配置到Thrift Bytes格式
|
||
/// </summary>
|
||
private void SaveConfigToThriftBytes()
|
||
{
|
||
try
|
||
{
|
||
var startTime = System.Diagnostics.Stopwatch.StartNew();
|
||
|
||
// 确保输出目录存在
|
||
string dir = Path.GetDirectoryName(BYTES_PATH);
|
||
if (!Directory.Exists(dir))
|
||
{
|
||
Directory.CreateDirectory(dir);
|
||
}
|
||
|
||
// 创建Thrift数据结构
|
||
var thriftConfig = new Byway.Thrift.Data.ArtAtlasConfig
|
||
{
|
||
Atlases = new List<Byway.Thrift.Data.ArtAtlasInfo>(),
|
||
Version = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||
GenerateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
|
||
};
|
||
|
||
// 转换所有Atlas
|
||
foreach (var atlas in config.Atlases)
|
||
{
|
||
var thriftAtlas = new Byway.Thrift.Data.ArtAtlasInfo
|
||
{
|
||
Name = atlas.Name,
|
||
SpritePaths = new List<string>(atlas.SpritePaths)
|
||
};
|
||
thriftConfig.Atlases.Add(thriftAtlas);
|
||
}
|
||
|
||
// 序列化到bytes
|
||
byte[] bytesData;
|
||
using (var memoryStream = new MemoryStream())
|
||
{
|
||
using (var transport = new TStreamTransport(null, memoryStream, new TConfiguration()))
|
||
{
|
||
using (var protocol = new TBinaryProtocol(transport))
|
||
{
|
||
thriftConfig.WriteAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult();
|
||
}
|
||
}
|
||
bytesData = memoryStream.ToArray();
|
||
}
|
||
|
||
// 写入文件
|
||
File.WriteAllBytes(BYTES_PATH, bytesData);
|
||
|
||
startTime.Stop();
|
||
|
||
Debug.Log($"[AtlasBuilderEditor] ✅ Atlas Bytes生成成功!\n" +
|
||
$" 路径: {BYTES_PATH}\n" +
|
||
$" 图集数量: {thriftConfig.Atlases.Count}\n" +
|
||
$" 文件大小: {bytesData.Length / 1024f:F2} KB\n" +
|
||
$" 耗时: {startTime.ElapsedMilliseconds} ms");
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogError($"[AtlasBuilderEditor] ❌ 生成Atlas Bytes失败: {ex.Message}\n{ex.StackTrace}");
|
||
}
|
||
}
|
||
|
||
private void RefreshSpriteTree()
|
||
{
|
||
spriteRoot = new SpriteFolder("Art_SubModule", null, 0);
|
||
BuildSpriteTree(SPRITE_ROOT_PATH, spriteRoot);
|
||
RefreshAssignedCache();
|
||
}
|
||
|
||
private void BuildSpriteTree(string path, SpriteFolder parent)
|
||
{
|
||
if (!Directory.Exists(path)) return;
|
||
|
||
// 添加子文件夹
|
||
foreach (string dir in Directory.GetDirectories(path))
|
||
{
|
||
string folderName = Path.GetFileName(dir);
|
||
var subFolder = new SpriteFolder(folderName, parent, parent.Depth + 1);
|
||
parent.SubFolders.Add(subFolder);
|
||
BuildSpriteTree(dir, subFolder);
|
||
}
|
||
|
||
// 添加Sprite文件
|
||
var guids = AssetDatabase.FindAssets("t:Sprite", new[] { path });
|
||
var spritePaths = guids.Select(guid => AssetDatabase.GUIDToAssetPath(guid))
|
||
.Where(p => Path.GetDirectoryName(p).Replace("\\", "/") == path.Replace("\\", "/"))
|
||
.OrderBy(p => p);
|
||
|
||
foreach (string spritePath in spritePaths)
|
||
{
|
||
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spritePath);
|
||
if (sprite != null)
|
||
{
|
||
parent.Sprites.Add(new SpriteAsset
|
||
{
|
||
Name = sprite.name,
|
||
Path = spritePath,
|
||
Depth = parent.Depth + 1,
|
||
Parent = parent
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
private void RefreshAssignedCache()
|
||
{
|
||
cachedAssignedSprites.Clear();
|
||
cachedAssignedFolders.Clear();
|
||
|
||
HashSet<string> allAssignedPaths = new HashSet<string>();
|
||
foreach (var atlas in config.Atlases)
|
||
{
|
||
foreach (string path in atlas.SpritePaths)
|
||
{
|
||
allAssignedPaths.Add(path);
|
||
}
|
||
}
|
||
|
||
MarkAssignedRecursive(spriteRoot, allAssignedPaths);
|
||
}
|
||
|
||
private bool MarkAssignedRecursive(SpriteFolder folder, HashSet<string> assignedPaths)
|
||
{
|
||
bool hasAssigned = false;
|
||
|
||
foreach (var subFolder in folder.SubFolders)
|
||
{
|
||
if (MarkAssignedRecursive(subFolder, assignedPaths))
|
||
{
|
||
hasAssigned = true;
|
||
}
|
||
}
|
||
|
||
foreach (var sprite in folder.Sprites)
|
||
{
|
||
if (assignedPaths.Contains(sprite.Path))
|
||
{
|
||
cachedAssignedSprites.Add(sprite);
|
||
hasAssigned = true;
|
||
}
|
||
}
|
||
|
||
if (hasAssigned)
|
||
{
|
||
cachedAssignedFolders.Add(folder);
|
||
}
|
||
|
||
return hasAssigned;
|
||
}
|
||
|
||
// ==================== 辅助方法 ====================
|
||
|
||
private bool IsSelectedFolder(SpriteFolder folder)
|
||
{
|
||
foreach (var subFolder in folder.SubFolders)
|
||
{
|
||
if (!IsSelectedFolder(subFolder))
|
||
return false;
|
||
}
|
||
|
||
foreach (var sprite in folder.Sprites)
|
||
{
|
||
if (!selectedSpriteAssets.Contains(sprite))
|
||
return false;
|
||
}
|
||
|
||
return folder.SubFolders.Count > 0 || folder.Sprites.Count > 0;
|
||
}
|
||
|
||
private void SetSelectedFolder(SpriteFolder folder, bool select)
|
||
{
|
||
if (select)
|
||
{
|
||
selectedSpriteFolders.Add(folder);
|
||
foreach (var sprite in folder.Sprites)
|
||
{
|
||
selectedSpriteAssets.Add(sprite);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
selectedSpriteFolders.Remove(folder);
|
||
foreach (var sprite in folder.Sprites)
|
||
{
|
||
selectedSpriteAssets.Remove(sprite);
|
||
}
|
||
}
|
||
|
||
foreach (var subFolder in folder.SubFolders)
|
||
{
|
||
SetSelectedFolder(subFolder, select);
|
||
}
|
||
}
|
||
|
||
private void SetSelectedSprite(SpriteAsset sprite, bool select)
|
||
{
|
||
if (select)
|
||
{
|
||
selectedSpriteAssets.Add(sprite);
|
||
}
|
||
else
|
||
{
|
||
selectedSpriteAssets.Remove(sprite);
|
||
}
|
||
}
|
||
|
||
private bool IsAssignedSprite(SpriteAsset sprite)
|
||
{
|
||
return cachedAssignedSprites.Contains(sprite);
|
||
}
|
||
|
||
private bool IsAssignedFolder(SpriteFolder folder)
|
||
{
|
||
return cachedAssignedFolders.Contains(folder);
|
||
}
|
||
|
||
private HashSet<SpriteAsset> GetSelectedSprites()
|
||
{
|
||
if (!hideAssignedSprites)
|
||
{
|
||
return selectedSpriteAssets;
|
||
}
|
||
|
||
HashSet<SpriteAsset> result = new HashSet<SpriteAsset>();
|
||
foreach (var sprite in selectedSpriteAssets)
|
||
{
|
||
if (!IsAssignedSprite(sprite))
|
||
{
|
||
result.Add(sprite);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private bool HasVisibleSprites(SpriteFolder folder)
|
||
{
|
||
// 检查当前文件夹的Sprite
|
||
foreach (var sprite in folder.Sprites)
|
||
{
|
||
if (!hideAssignedSprites || !IsAssignedSprite(sprite))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 递归检查子文件夹
|
||
foreach (var subFolder in folder.SubFolders)
|
||
{
|
||
if (HasVisibleSprites(subFolder))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ==================== 数据类 ====================
|
||
|
||
public class SpriteFolder
|
||
{
|
||
public string Name;
|
||
public SpriteFolder Parent;
|
||
public int Depth;
|
||
public List<SpriteFolder> SubFolders = new List<SpriteFolder>();
|
||
public List<SpriteAsset> Sprites = new List<SpriteAsset>();
|
||
|
||
public SpriteFolder(string name, SpriteFolder parent, int depth)
|
||
{
|
||
Name = name;
|
||
Parent = parent;
|
||
Depth = depth;
|
||
}
|
||
}
|
||
|
||
public class SpriteAsset
|
||
{
|
||
public string Name;
|
||
public string Path;
|
||
public int Depth;
|
||
public SpriteFolder Parent;
|
||
}
|
||
}
|