417 lines
16 KiB
C#
417 lines
16 KiB
C#
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|