Files
Cielonos/Assets/Scripts/SLSUtilities/UI/Editor/InputGlyphsSpriteAssetCreator.cs
SoulliesOfficial 6d7ebc5825 Passion & UI
2026-06-12 17:11:39 -04:00

417 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.TextCore;
namespace Cielonos.Editor
{
/// <summary>
/// 一键式 Editor 工具:将 Assets/Sprites/Icon/InputIcons/ 下的所有按键图标
/// 打包为 atlas 纹理,并自动生成 TMP_SpriteAsset 供 TMP 富文本使用。
/// 使用方式Tools → Cielonos → Create InputGlyphs Sprite Asset
/// </summary>
public static class InputGlyphsSpriteAssetCreator
{
private const string KeyboardFolder = "Assets/Sprites/Icon/InputIcons/keyboard";
private const string MouseFolder = "Assets/Sprites/Icon/InputIcons/mouse";
private const string AtlasOutputFolder = "Assets/Scripts/SLSUtilities/UI/SpriteAssets";
private const string AssetOutputFolder = "Assets/TextMesh Pro/Resources/Sprite Assets";
private const string AtlasFileName = "InputGlyphs_Atlas.png";
private const string AssetFileName = "InputGlyphs.asset";
/// <summary>打包前将每张源图缩放到此尺寸(像素),降低 atlas 体积。</summary>
private const int SpriteSize = 128;
/// <summary>Atlas 最大边长。</summary>
private const int MaxAtlasSize = 2048;
/// <summary>打包时各图之间的像素间距。</summary>
private const int Padding = 2;
/// <summary>鼠标文件名 → Token 名映射表。</summary>
private static readonly Dictionary<string, string> MouseNameMap = new()
{
{ "mouse-left", "LMB" },
{ "mouse-right", "RMB" },
{ "mouse-middle", "MMB" },
{ "mouse-g1", "Mouse4" },
{ "mouse-g2", "Mouse5" },
{ "mouse-move-hor", "MouseMoveH" },
{ "mouse-move-vert", "MouseMoveV" },
};
private struct SpriteEntry
{
public string filePath;
public string tokenName;
}
// ================================================================
// 入口
// ================================================================
[MenuItem("Tools/Cielonos/Create InputGlyphs Sprite Asset")]
public static void Execute()
{
var entries = new List<SpriteEntry>();
CollectEntries(KeyboardFolder, entries, MapKeyboardName);
CollectEntries(MouseFolder, entries, MapMouseName);
if (entries.Count == 0)
{
Debug.LogError("[InputGlyphs] InputIcons 文件夹中未找到任何图片。");
return;
}
Debug.Log($"[InputGlyphs] 发现 {entries.Count} 张图标,开始打包...");
// ── 1. 加载并缩放所有图标 ──
LoadAndResizeResult loaded = LoadAndResize(entries);
// ── 2. 打包为 atlas ──
var atlas = new Texture2D(MaxAtlasSize, MaxAtlasSize, TextureFormat.RGBA32, false);
Rect[] rects = atlas.PackTextures(loaded.Textures.ToArray(), Padding, MaxAtlasSize, false);
if (rects == null)
{
Debug.LogError("[InputGlyphs] PackTextures 失败,请增大 MaxAtlasSize。");
CleanupTextures(loaded.Textures);
return;
}
// ── 3. 保存 atlas PNG 到磁盘 ──
string atlasPath = $"{AtlasOutputFolder}/{AtlasFileName}";
SaveAtlasToDisk(atlas, atlasPath);
// ── 4. 配置 atlas 导入器,写入每个子 Sprite 的元数据 ──
ConfigureAtlasImporter(atlasPath, atlas, rects, loaded.Names);
// ── 5. 从已导入的 atlas 构建 TMP_SpriteAsset放入 Resources 以便 TMP 按名查找) ──
string assetPath = $"{AssetOutputFolder}/{AssetFileName}";
BuildSpriteAsset(atlasPath, assetPath);
// ── 6. 清理临时纹理 ──
CleanupTextures(loaded.Textures);
UnityEngine.Object.DestroyImmediate(atlas);
Debug.Log($"[InputGlyphs] SpriteAsset 已创建:{assetPath}{entries.Count} 个图标)。" +
$"\n在 TMP 富文本中使用:<sprite=\"InputGlyphs\" name=\"Q\"> 或 <sprite name=\"Q\">");
}
// ================================================================
// 收集文件
// ================================================================
private static void CollectEntries(
string folder,
List<SpriteEntry> entries,
Func<string, string> nameMapper)
{
string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { folder });
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
string fileName = Path.GetFileNameWithoutExtension(assetPath);
entries.Add(new SpriteEntry
{
filePath = Path.GetFullPath(assetPath),
tokenName = nameMapper(fileName)
});
}
}
/// <summary>
/// 键盘文件名 → Token 名。
/// 规则单字母大写单数字保留Fn 键大写,连字符转 PascalCase其余首字母大写。
/// </summary>
private static string MapKeyboardName(string fileName)
{
// 单字母 → 大写
if (fileName.Length == 1 && char.IsLetter(fileName[0]))
return fileName.ToUpperInvariant();
// 单数字 → 原样
if (fileName.Length == 1 && char.IsDigit(fileName[0]))
return fileName;
// 功能键 f1-f12 → 全大写
if (fileName.Length >= 2 && fileName[0] == 'f' &&
int.TryParse(fileName.Substring(1), out _))
return fileName.ToUpperInvariant();
// 含连字符 → PascalCase (arrow-down → ArrowDown)
if (fileName.Contains('-'))
{
string[] parts = fileName.Split('-');
var sb = new StringBuilder();
foreach (string part in parts)
{
if (part.Length > 0)
sb.Append(char.ToUpperInvariant(part[0]))
.Append(part, 1, part.Length - 1);
}
return sb.ToString();
}
// 其余 → 首字母大写
return char.ToUpperInvariant(fileName[0]) + fileName.Substring(1);
}
/// <summary>
/// 鼠标文件名 → Token 名。通过静态映射表查找。
/// </summary>
private static string MapMouseName(string fileName)
{
return MouseNameMap.TryGetValue(fileName, out string mapped) ? mapped : fileName;
}
// ================================================================
// 加载 & 缩放
// ================================================================
private struct LoadAndResizeResult
{
public List<Texture2D> Textures;
public List<string> Names;
}
private static LoadAndResizeResult LoadAndResize(List<SpriteEntry> entries)
{
var textures = new List<Texture2D>(entries.Count);
var names = new List<string>(entries.Count);
foreach (SpriteEntry entry in entries)
{
byte[] bytes = File.ReadAllBytes(entry.filePath);
var src = new Texture2D(2, 2, TextureFormat.RGBA32, false);
ImageConversion.LoadImage(src, bytes);
Texture2D dst = Resize(src, SpriteSize, SpriteSize);
UnityEngine.Object.DestroyImmediate(src);
textures.Add(dst);
names.Add(entry.tokenName);
}
return new LoadAndResizeResult { Textures = textures, Names = names };
}
private static Texture2D Resize(Texture2D source, int width, int height)
{
RenderTexture rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
rt.filterMode = FilterMode.Bilinear;
Graphics.Blit(source, rt);
RenderTexture prev = RenderTexture.active;
RenderTexture.active = rt;
var dst = new Texture2D(width, height, TextureFormat.RGBA32, false);
dst.ReadPixels(new Rect(0, 0, width, height), 0, 0);
dst.Apply();
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
return dst;
}
// ================================================================
// Atlas 保存 & 导入
// ================================================================
private static void SaveAtlasToDisk(Texture2D atlas, string atlasPath)
{
string dir = Path.GetDirectoryName(atlasPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllBytes(atlasPath, atlas.EncodeToPNG());
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceUpdate);
}
private static void ConfigureAtlasImporter(
string atlasPath,
Texture2D atlas,
Rect[] rects,
List<string> names)
{
var importer = AssetImporter.GetAtPath(atlasPath) as TextureImporter;
if (importer == null)
{
Debug.LogError($"[InputGlyphs] 无法获取 TextureImporter{atlasPath}");
return;
}
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.isReadable = false;
importer.mipmapEnabled = false;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.maxTextureSize = MaxAtlasSize;
int atlasW = atlas.width;
int atlasH = atlas.height;
var spritesheet = new SpriteMetaData[rects.Length];
for (int i = 0; i < rects.Length; i++)
{
float x = rects[i].x * atlasW;
float y = rects[i].y * atlasH;
float w = rects[i].width * atlasW;
float h = rects[i].height * atlasH;
spritesheet[i] = new SpriteMetaData
{
name = names[i],
rect = new Rect(x, y, w, h),
alignment = (int)SpriteAlignment.Center,
pivot = new Vector2(0.5f, 0.5f)
};
}
importer.spritesheet = spritesheet;
importer.SaveAndReimport();
}
// ================================================================
// TMP_SpriteAsset 构建
// ================================================================
private static void BuildSpriteAsset(string atlasPath, string assetPath)
{
// 加载持久化后的 atlas 纹理
Texture2D atlasTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasPath);
if (atlasTexture == null)
{
Debug.LogError($"[InputGlyphs] 无法加载 atlas{atlasPath}");
return;
}
// 从 atlas 加载所有子 Sprite
Sprite[] sprites = AssetDatabase.LoadAllAssetsAtPath(atlasPath)
.OfType<Sprite>()
.OrderByDescending(s => s.rect.y)
.ThenBy(s => s.rect.x)
.ToArray();
if (sprites.Length == 0)
{
Debug.LogError("[InputGlyphs] Atlas 中未找到子 Sprite请检查导入设置。");
return;
}
// 创建 TMP_SpriteAsset 实例
TMP_SpriteAsset spriteAsset = ScriptableObject.CreateInstance<TMP_SpriteAsset>();
string dir = Path.GetDirectoryName(assetPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
// 如果已存在旧资产则删除
if (AssetDatabase.LoadAssetAtPath<TMP_SpriteAsset>(assetPath) != null)
AssetDatabase.DeleteAsset(assetPath);
AssetDatabase.CreateAsset(spriteAsset, assetPath);
// 设置 sprite sheet 引用
spriteAsset.spriteSheet = atlasTexture;
// 通过反射设置版本号internal setter
SetFieldValue(spriteAsset, "m_Version", "1.1.0");
// 设置 hash code
spriteAsset.hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteAsset.name);
// 构建 Glyph 和 Character 表
var glyphTable = new List<TMP_SpriteGlyph>(sprites.Length);
var charTable = new List<TMP_SpriteCharacter>(sprites.Length);
for (int i = 0; i < sprites.Length; i++)
{
Sprite sprite = sprites[i];
var glyph = new TMP_SpriteGlyph(
index: (uint)i,
metrics: new GlyphMetrics(
sprite.rect.width,
sprite.rect.height,
0f,
sprite.rect.height * 0.8f,
sprite.rect.width),
glyphRect: new GlyphRect(sprite.rect),
scale: 1.0f,
atlasIndex: 0,
sprite: sprite
);
glyphTable.Add(glyph);
var character = new TMP_SpriteCharacter(0xFFFE, glyph)
{
name = sprite.name,
scale = 1.0f
};
charTable.Add(character);
}
// 通过反射设置内部字段setter 为 internal
SetFieldValue(spriteAsset, "m_GlyphTable", glyphTable);
SetFieldValue(spriteAsset, "m_SpriteCharacterTable", charTable);
// 创建并嵌入 Material
Shader shader = Shader.Find("TextMeshPro/Sprite");
if (shader == null)
{
Debug.LogWarning("[InputGlyphs] Shader 'TextMeshPro/Sprite' 未找到。");
return;
}
var material = new Material(shader);
material.SetTexture(ShaderUtilities.ID_MainTex, atlasTexture);
material.name = spriteAsset.name + " Material";
spriteAsset.material = material;
AssetDatabase.AddObjectToAsset(material, spriteAsset);
// 重建查找表并保存
spriteAsset.UpdateLookupTables();
EditorUtility.SetDirty(spriteAsset);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(assetPath);
}
// ================================================================
// 工具方法
// ================================================================
private static void SetFieldValue(object target, string fieldName, object value)
{
FieldInfo field = target.GetType().GetField(
fieldName,
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
if (field != null)
{
field.SetValue(target, value);
}
else
{
Debug.LogWarning($"[InputGlyphs] 反射未找到字段 '{fieldName}'(类型:{target.GetType().Name})。");
}
}
private static void CleanupTextures(List<Texture2D> textures)
{
foreach (Texture2D tex in textures)
{
if (tex != null)
UnityEngine.Object.DestroyImmediate(tex);
}
}
}
}