1190 lines
45 KiB
C#
1190 lines
45 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine.TextCore;
|
||
|
||
/// <summary>
|
||
/// FntSpriteSplitter Editor 窗口
|
||
///
|
||
/// 功能:将字体定义文件中的字符分割为对应 PNG 的多个 Sprite,
|
||
/// 写入 PNG.meta 的 spriteSheet.sprites 中。
|
||
///
|
||
/// 支持三种输入格式:
|
||
/// 1. .fnt - BMFont 文本格式(标准)
|
||
/// 2. .bytes - 内容为 BMFont .fnt 文本格式(Unity 用 .bytes 后缀作为 TextAsset 加载)
|
||
/// 3. .asset - Unity 原生 Font 资源(!u!128,m_CharacterRects 列表 + 归一化 UV)
|
||
///
|
||
/// 坐标系说明:
|
||
/// - BMFont 像素坐标原点在左上(y 向下递增),Unity Sprite rect 原点在左下(y 向上递增),
|
||
/// BMFont 格式需要翻转 y:y_unity = textureHeight - y_fnt - height
|
||
/// - Unity Font 的 UV 是归一化坐标(原点左下,y 向上),与 Sprite rect 一致,无需翻转
|
||
///
|
||
/// 使用方法:
|
||
/// 1. 把字体文件拖到「字体文件」槽位(.fnt / .bytes / .asset 均可)
|
||
/// 2. 自动识别格式并解析字符;BMFont 格式还会自动从 page file 引用对应 PNG
|
||
/// 3. 在「目标 PNG」槽位确认或手动指定 PNG
|
||
/// 4. 点击「写入 Meta」生成分割 Sprite
|
||
///
|
||
/// 作者:AI Assistant
|
||
/// </summary>
|
||
public class FntSpriteSplitter : EditorWindow
|
||
{
|
||
#region 字段
|
||
|
||
/// <summary>选中的字体文件路径(Assets/... 形式)</summary>
|
||
private string fontAssetPath = "";
|
||
|
||
/// <summary>当前文件格式</summary>
|
||
private FontFormat fontFormat = FontFormat.Unknown;
|
||
|
||
/// <summary>目标 PNG 纹理</summary>
|
||
private Texture2D targetTexture;
|
||
|
||
/// <summary>.fnt 中声明的贴图尺寸(scaleW / scaleH);.asset 格式为 0(用 PNG 实际尺寸)</summary>
|
||
private int fontTextureW = 0;
|
||
private int fontTextureH = 0;
|
||
|
||
/// <summary>解析出的字符列表</summary>
|
||
private List<FontCharInfo> charList = new List<FontCharInfo>();
|
||
|
||
/// <summary>字符列表滚动位置</summary>
|
||
private Vector2 scrollPos;
|
||
|
||
/// <summary>处理结果日志</summary>
|
||
private string logText = "";
|
||
private Vector2 logScrollPos;
|
||
private bool showLog = false;
|
||
|
||
/// <summary>命名方式:true=按索引,false=按字符含义</summary>
|
||
private bool nameByIndex = true;
|
||
|
||
/// <summary>写入前是否清空原有 sprites</summary>
|
||
private bool clearBeforeWrite = true;
|
||
|
||
/// <summary>命名前缀(默认使用 PNG 文件名,可手动修改)</summary>
|
||
private string namePrefix = "";
|
||
|
||
#endregion
|
||
|
||
#region 数据结构
|
||
|
||
/// <summary>字体文件格式</summary>
|
||
private enum FontFormat
|
||
{
|
||
Unknown,
|
||
BmFontFnt, // .fnt 或 .bytes,BMFont 文本格式
|
||
UnityFontAsset // .asset,Unity 原生 Font(!u!128)
|
||
}
|
||
|
||
/// <summary>单个字符的解析信息(统一格式)</summary>
|
||
private class FontCharInfo
|
||
{
|
||
public int id; // 字符 unicode id
|
||
public int x; // 在 PNG 中的像素 x(左下原点)
|
||
public int y; // 在 PNG 中的像素 y(左下原点,已翻转)
|
||
public int width; // 像素宽
|
||
public int height; // 像素高
|
||
public int advance; // 字符前进宽度(仅供信息显示)
|
||
|
||
/// <summary>Unity 坐标系下的 rect(已翻转 y,可直接写入 meta)</summary>
|
||
public Rect unityRect;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 菜单入口
|
||
|
||
[MenuItem("Tools/FNT Sprite Splitter")]
|
||
private static void ShowWindow()
|
||
{
|
||
var window = GetWindow<FntSpriteSplitter>("FNT Sprite Splitter");
|
||
window.minSize = new Vector2(440, 560);
|
||
window.Show();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region GUI 绘制
|
||
|
||
private void OnGUI()
|
||
{
|
||
DrawHeader();
|
||
DrawFontSlot();
|
||
DrawPngSlot();
|
||
DrawCharList();
|
||
DrawOptions();
|
||
DrawActionBar();
|
||
DrawLogArea();
|
||
}
|
||
|
||
/// <summary>顶部标题说明</summary>
|
||
private void DrawHeader()
|
||
{
|
||
EditorGUILayout.Space(8);
|
||
EditorGUILayout.HelpBox(
|
||
"将字体定义文件中的字符分割为 PNG 的多个 Sprite。\n" +
|
||
"支持三种格式:\n" +
|
||
" .fnt - BMFont 文本格式\n" +
|
||
" .bytes - 内容为 BMFont .fnt 文本格式\n" +
|
||
" .asset - Unity 原生 Font 资源(!u!128)\n" +
|
||
"1. 拖入字体文件(BMFont 格式会自动识别对应 PNG)\n" +
|
||
"2. 点击「写入 Meta」生成分割 Sprite\n" +
|
||
"3. 点击「生成 TMP_SpriteAsset」生成可直接用于 <sprite=索引> 的数字精灵资源",
|
||
MessageType.Info
|
||
);
|
||
EditorGUILayout.Space(4);
|
||
}
|
||
|
||
/// <summary>字体文件拖拽槽位</summary>
|
||
private void DrawFontSlot()
|
||
{
|
||
EditorGUILayout.LabelField("字体文件", EditorStyles.boldLabel);
|
||
|
||
Rect dropRect = EditorGUILayout.GetControlRect(GUILayout.Height(56));
|
||
GUI.Box(dropRect, "", GUI.skin.box);
|
||
|
||
var centeredStyle = new GUIStyle(GUI.skin.label)
|
||
{
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontSize = 12
|
||
};
|
||
|
||
if (!string.IsNullOrEmpty(fontAssetPath))
|
||
{
|
||
string fileName = Path.GetFileName(fontAssetPath);
|
||
string formatName = FormatToDisplay(fontFormat);
|
||
string texInfo = (fontTextureW > 0 && fontTextureH > 0)
|
||
? $" 贴图 {fontTextureW}x{fontTextureH} 字符 {charList.Count} 个"
|
||
: $" 字符 {charList.Count} 个";
|
||
GUI.Label(dropRect, $"{fileName}\n[{formatName}]{texInfo}", centeredStyle);
|
||
}
|
||
else
|
||
{
|
||
GUI.Label(dropRect, "拖入 .fnt / .bytes / .asset 文件\n或 点击选择", centeredStyle);
|
||
}
|
||
|
||
// 拖拽事件
|
||
if (Event.current.type == EventType.DragUpdated && dropRect.Contains(Event.current.mousePosition))
|
||
{
|
||
bool valid = false;
|
||
foreach (var p in DragAndDrop.paths)
|
||
{
|
||
if (IsSupportedFontFile(p))
|
||
{
|
||
valid = true;
|
||
break;
|
||
}
|
||
}
|
||
DragAndDrop.visualMode = valid ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;
|
||
Event.current.Use();
|
||
}
|
||
else if (Event.current.type == EventType.DragPerform && dropRect.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.AcceptDrag();
|
||
foreach (var p in DragAndDrop.paths)
|
||
{
|
||
if (IsSupportedFontFile(p))
|
||
{
|
||
LoadFontFile(p);
|
||
break;
|
||
}
|
||
}
|
||
Event.current.Use();
|
||
}
|
||
|
||
// 点击选择按钮
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("选择字体文件", GUILayout.Height(24)))
|
||
{
|
||
string startDir = string.IsNullOrEmpty(fontAssetPath)
|
||
? Application.dataPath
|
||
: Path.GetDirectoryName(Path.GetFullPath(fontAssetPath));
|
||
string picked = EditorUtility.OpenFilePanelWithFilters(
|
||
"选择字体文件", startDir,
|
||
new[] { "Font files", "fnt,bytes,asset", "All files", "*" });
|
||
if (!string.IsNullOrEmpty(picked))
|
||
{
|
||
string assetPath = "Assets" + picked.Substring(Application.dataPath.Length).Replace('\\', '/');
|
||
LoadFontFile(assetPath);
|
||
}
|
||
}
|
||
|
||
if (GUILayout.Button("清空", GUILayout.Height(24), GUILayout.Width(60)))
|
||
{
|
||
ClearAll();
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.Space(6);
|
||
}
|
||
|
||
/// <summary>目标 PNG 显示槽位(可手动覆盖)</summary>
|
||
private void DrawPngSlot()
|
||
{
|
||
EditorGUILayout.LabelField("目标 PNG", EditorStyles.boldLabel);
|
||
|
||
Rect dropRect = EditorGUILayout.GetControlRect(GUILayout.Height(48));
|
||
GUI.Box(dropRect, "", GUI.skin.box);
|
||
|
||
var centeredStyle = new GUIStyle(GUI.skin.label)
|
||
{
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontSize = 12
|
||
};
|
||
|
||
if (targetTexture != null)
|
||
{
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
int texW = targetTexture.width;
|
||
int texH = targetTexture.height;
|
||
|
||
// 检查 PNG 实际尺寸与字体声明是否一致
|
||
string warning = "";
|
||
if (fontTextureW > 0 && (texW != fontTextureW || texH != fontTextureH))
|
||
{
|
||
warning = $"\n<color=red>⚠ 尺寸不匹配!字体声明 {fontTextureW}x{fontTextureH}</color>";
|
||
}
|
||
else if (fontFormat == FontFormat.UnityFontAsset)
|
||
{
|
||
warning = "\n<color=yellow>Unity Font 模式:UV 已按 PNG 实际尺寸换算</color>";
|
||
}
|
||
|
||
var richStyle = new GUIStyle(centeredStyle) { richText = true };
|
||
GUI.Label(dropRect, $"{Path.GetFileName(pngPath)}\n{texW} x {texH} px{warning}", richStyle);
|
||
}
|
||
else
|
||
{
|
||
GUI.Label(dropRect, "拖入 PNG 或 点击选择(BMFont 格式可自动识别)", centeredStyle);
|
||
}
|
||
|
||
// 拖拽事件
|
||
if (Event.current.type == EventType.DragUpdated && dropRect.Contains(Event.current.mousePosition))
|
||
{
|
||
bool valid = false;
|
||
foreach (var p in DragAndDrop.paths)
|
||
{
|
||
if (p.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
valid = true;
|
||
break;
|
||
}
|
||
}
|
||
DragAndDrop.visualMode = valid ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;
|
||
Event.current.Use();
|
||
}
|
||
else if (Event.current.type == EventType.DragPerform && dropRect.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.AcceptDrag();
|
||
foreach (var p in DragAndDrop.paths)
|
||
{
|
||
if (p.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(p);
|
||
if (tex != null)
|
||
{
|
||
targetTexture = tex;
|
||
namePrefix = Path.GetFileNameWithoutExtension(p);
|
||
RecomputeRectsForPngSize();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
Event.current.Use();
|
||
}
|
||
|
||
// 点击选择
|
||
targetTexture = (Texture2D)EditorGUI.ObjectField(
|
||
new Rect(dropRect.x, dropRect.y, dropRect.width, dropRect.height),
|
||
targetTexture, typeof(Texture2D), false);
|
||
|
||
if (targetTexture != null)
|
||
{
|
||
string p = AssetDatabase.GetAssetPath(targetTexture);
|
||
if (!p.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
targetTexture = null;
|
||
}
|
||
else if (string.IsNullOrEmpty(namePrefix))
|
||
{
|
||
namePrefix = Path.GetFileNameWithoutExtension(p);
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.Space(6);
|
||
}
|
||
|
||
/// <summary>字符列表</summary>
|
||
private void DrawCharList()
|
||
{
|
||
EditorGUILayout.LabelField($"字符列表 ({charList.Count} 个)", EditorStyles.boldLabel);
|
||
|
||
Rect listRect = EditorGUILayout.GetControlRect(false, GUILayout.Height(180));
|
||
GUI.Box(listRect, "");
|
||
|
||
if (charList.Count == 0)
|
||
{
|
||
var hintStyle = new GUIStyle(GUI.skin.label)
|
||
{
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontSize = 12,
|
||
normal = { textColor = Color.gray }
|
||
};
|
||
GUI.Label(listRect, "拖入字体文件后会显示字符列表", hintStyle);
|
||
}
|
||
else
|
||
{
|
||
var innerRect = new Rect(listRect.x + 4, listRect.y + 4, listRect.width - 8, listRect.height - 8);
|
||
float headerH = 20;
|
||
float rowH = 20;
|
||
float contentH = headerH + charList.Count * rowH;
|
||
scrollPos = GUI.BeginScrollView(innerRect, scrollPos, new Rect(0, 0, innerRect.width - 16, contentH));
|
||
|
||
var headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 10 };
|
||
float col1 = innerRect.width * 0.10f; // index
|
||
float col2 = innerRect.width * 0.15f; // char
|
||
float col3 = innerRect.width * 0.35f; // rect (unity)
|
||
float col4 = innerRect.width * 0.30f; // size
|
||
GUI.Label(new Rect(2, 0, col1, headerH), "#", headerStyle);
|
||
GUI.Label(new Rect(col1, 0, col2, headerH), "char", headerStyle);
|
||
GUI.Label(new Rect(col1 + col2, 0, col3, headerH), "unity(x,y,w,h)", headerStyle);
|
||
GUI.Label(new Rect(col1 + col2 + col3, 0, col4, headerH), "size", headerStyle);
|
||
|
||
for (int i = 0; i < charList.Count; i++)
|
||
{
|
||
var ch = charList[i];
|
||
float y = headerH + i * rowH;
|
||
|
||
// 交替背景色
|
||
if (i % 2 == 0)
|
||
{
|
||
var origColor = GUI.color;
|
||
GUI.color = new Color(0.85f, 0.85f, 0.85f, 0.3f);
|
||
GUI.DrawTexture(new Rect(0, y, innerRect.width, rowH), EditorGUIUtility.whiteTexture);
|
||
GUI.color = origColor;
|
||
}
|
||
|
||
var rowStyle = new GUIStyle(GUI.skin.label) { fontSize = 10 };
|
||
GUI.Label(new Rect(2, y, col1, rowH), i.ToString(), rowStyle);
|
||
GUI.Label(new Rect(col1, y, col2, rowH), CharToLabel(ch.id), rowStyle);
|
||
GUI.Label(new Rect(col1 + col2, y, col3, rowH),
|
||
$"({(int)ch.unityRect.x},{(int)ch.unityRect.y},{(int)ch.unityRect.width}x{(int)ch.unityRect.height})",
|
||
rowStyle);
|
||
GUI.Label(new Rect(col1 + col2 + col3, y, col4, rowH),
|
||
$"{ch.width}x{ch.height}", rowStyle);
|
||
}
|
||
|
||
GUI.EndScrollView();
|
||
}
|
||
|
||
EditorGUILayout.Space(6);
|
||
}
|
||
|
||
/// <summary>选项区域</summary>
|
||
private void DrawOptions()
|
||
{
|
||
EditorGUILayout.LabelField("选项", EditorStyles.boldLabel);
|
||
|
||
nameByIndex = EditorGUILayout.Toggle(
|
||
new GUIContent("按索引命名",
|
||
"勾选:PNG名_0, PNG名_1, ...\n不勾选:按字符含义命名(0,1,...,plus,comma,dot,a,b,...)"),
|
||
nameByIndex);
|
||
|
||
clearBeforeWrite = EditorGUILayout.Toggle(
|
||
new GUIContent("写入前清空原有 sprites",
|
||
"勾选:先清空 meta 中已有的 sprites 列表再写入\n不勾选:追加到现有 sprites 末尾"),
|
||
clearBeforeWrite);
|
||
|
||
namePrefix = EditorGUILayout.TextField(
|
||
new GUIContent("命名前缀",
|
||
"Sprite 名称前缀。按索引命名时为「前缀_索引」,按字符命名时为「前缀_字符」。留空则使用 PNG 文件名。"),
|
||
namePrefix);
|
||
|
||
EditorGUILayout.Space(4);
|
||
}
|
||
|
||
/// <summary>操作按钮区</summary>
|
||
private void DrawActionBar()
|
||
{
|
||
bool canWrite = !string.IsNullOrEmpty(fontAssetPath) && targetTexture != null && charList.Count > 0;
|
||
|
||
GUI.enabled = canWrite;
|
||
if (GUILayout.Button("写入 Meta", GUILayout.Height(32)))
|
||
{
|
||
WriteSpritesToMeta();
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
if (GUILayout.Button("清空 Meta 中所有 sprites", GUILayout.Height(24)))
|
||
{
|
||
if (targetTexture != null &&
|
||
EditorUtility.DisplayDialog("确认清空",
|
||
$"将清空 {targetTexture.name}.png.meta 中所有 sprite 条目,确认继续?",
|
||
"确认", "取消"))
|
||
{
|
||
ClearSpritesInMeta();
|
||
}
|
||
}
|
||
|
||
if (GUILayout.Button("全部重置", GUILayout.Height(24)))
|
||
{
|
||
ClearAll();
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
// 生成 TMP_SpriteAsset
|
||
EditorGUILayout.Space(4);
|
||
GUI.enabled = canWrite;
|
||
if (GUILayout.Button("生成 TMP_SpriteAsset", GUILayout.Height(28)))
|
||
{
|
||
GenerateTMPSpriteAsset();
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
EditorGUILayout.Space(6);
|
||
}
|
||
|
||
/// <summary>日志区域</summary>
|
||
private void DrawLogArea()
|
||
{
|
||
if (string.IsNullOrEmpty(logText)) return;
|
||
|
||
showLog = EditorGUILayout.Foldout(showLog, "处理日志");
|
||
if (showLog)
|
||
{
|
||
EditorGUILayout.BeginVertical(GUI.skin.box);
|
||
logScrollPos = EditorGUILayout.BeginScrollView(logScrollPos, GUILayout.Height(140));
|
||
EditorGUILayout.TextArea(logText, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true));
|
||
EditorGUILayout.EndScrollView();
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 字体文件加载与解析
|
||
|
||
/// <summary>判断文件是否为支持的字格式</summary>
|
||
private bool IsSupportedFontFile(string path)
|
||
{
|
||
if (string.IsNullOrEmpty(path)) return false;
|
||
string ext = Path.GetExtension(path).ToLowerInvariant();
|
||
if (ext == ".fnt" || ext == ".bytes") return true;
|
||
if (ext == ".asset")
|
||
{
|
||
// .asset 需要检查内容是否为 Unity Font (!u!128)
|
||
try
|
||
{
|
||
string text = File.ReadAllText(Path.GetFullPath(path), Encoding.UTF8);
|
||
return text.Contains("!u!128") && text.Contains("m_CharacterRects");
|
||
}
|
||
catch { return false; }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>格式枚举转显示文本</summary>
|
||
private string FormatToDisplay(FontFormat fmt)
|
||
{
|
||
switch (fmt)
|
||
{
|
||
case FontFormat.BmFontFnt: return "BMFont 文本";
|
||
case FontFormat.UnityFontAsset: return "Unity Font (.asset)";
|
||
default: return "未知格式";
|
||
}
|
||
}
|
||
|
||
/// <summary>加载并解析字体文件</summary>
|
||
private void LoadFontFile(string assetPath)
|
||
{
|
||
if (string.IsNullOrEmpty(assetPath) || !File.Exists(Path.GetFullPath(assetPath)))
|
||
{
|
||
EditorUtility.DisplayDialog("错误", $"找不到文件:\n{assetPath}", "确定");
|
||
return;
|
||
}
|
||
|
||
fontAssetPath = assetPath;
|
||
charList.Clear();
|
||
logText = "";
|
||
|
||
string ext = Path.GetExtension(assetPath).ToLowerInvariant();
|
||
string text = File.ReadAllText(Path.GetFullPath(assetPath), Encoding.UTF8);
|
||
|
||
// 识别格式
|
||
if (ext == ".asset" && text.Contains("!u!128"))
|
||
{
|
||
fontFormat = FontFormat.UnityFontAsset;
|
||
ParseUnityFontAsset(text);
|
||
}
|
||
else if (ext == ".fnt" || ext == ".bytes" || text.Contains("common") && text.Contains("scaleW"))
|
||
{
|
||
fontFormat = FontFormat.BmFontFnt;
|
||
ParseBmFontText(text);
|
||
}
|
||
else
|
||
{
|
||
EditorUtility.DisplayDialog("格式不支持",
|
||
$"无法识别文件格式:{assetPath}\n支持 .fnt / .bytes (BMFont文本) / .asset (Unity Font)", "确定");
|
||
fontFormat = FontFormat.Unknown;
|
||
return;
|
||
}
|
||
|
||
// 自动查找对应 PNG(仅 BMFont 格式有 page file 引用,Unity Font 的纹理引用通常为空)
|
||
if (fontFormat == FontFormat.BmFontFnt)
|
||
{
|
||
AutoFindPngFromBmFontPage(text, assetPath);
|
||
}
|
||
|
||
RecomputeRectsForPngSize();
|
||
Repaint();
|
||
}
|
||
|
||
/// <summary>解析 BMFont 文本格式(.fnt 或 .bytes)</summary>
|
||
private void ParseBmFontText(string text)
|
||
{
|
||
// 解析 common 行:scaleW 和 scaleH
|
||
var commonMatch = Regex.Match(text, @"^common\s+.*?scaleW=(\d+)\s+scaleH=(\d+)",
|
||
RegexOptions.Multiline);
|
||
if (!commonMatch.Success)
|
||
{
|
||
EditorUtility.DisplayDialog("解析失败", "找不到 common 行或 scaleW/scaleH 字段", "确定");
|
||
fontTextureW = 0;
|
||
fontTextureH = 0;
|
||
return;
|
||
}
|
||
fontTextureW = int.Parse(commonMatch.Groups[1].Value);
|
||
fontTextureH = int.Parse(commonMatch.Groups[2].Value);
|
||
|
||
// 解析所有 char 行
|
||
charList.Clear();
|
||
foreach (Match m in Regex.Matches(text,
|
||
@"^char\s+id=(-?\d+)\s+x=(-?\d+)\s+y=(-?\d+)\s+width=(\d+)\s+height=(\d+)" +
|
||
@"\s+xoffset=(-?\d+)\s+yoffset=(-?\d+)\s+xadvance=(-?\d+)",
|
||
RegexOptions.Multiline))
|
||
{
|
||
var info = new FontCharInfo
|
||
{
|
||
id = int.Parse(m.Groups[1].Value),
|
||
x = int.Parse(m.Groups[2].Value),
|
||
y = int.Parse(m.Groups[3].Value),
|
||
width = int.Parse(m.Groups[4].Value),
|
||
height = int.Parse(m.Groups[5].Value),
|
||
advance = int.Parse(m.Groups[8].Value),
|
||
};
|
||
|
||
// y 翻转:BMFont 左上原点 → Unity 左下原点
|
||
int flipY = fontTextureH - info.y - info.height;
|
||
info.unityRect = new Rect(info.x, flipY, info.width, info.height);
|
||
|
||
charList.Add(info);
|
||
}
|
||
}
|
||
|
||
/// <summary>解析 Unity 原生 Font .asset(!u!128)</summary>
|
||
private void ParseUnityFontAsset(string text)
|
||
{
|
||
// Unity Font 没有 scaleW/scaleH,UV 是归一化的,需要靠 PNG 实际尺寸换算
|
||
fontTextureW = 0;
|
||
fontTextureH = 0;
|
||
|
||
// 每个 m_CharacterRects 列表项格式:
|
||
// - serializedVersion: 2
|
||
// index: 43
|
||
// uv:
|
||
// serializedVersion: 2
|
||
// x: 0.578125
|
||
// y: 0.28125
|
||
// width: 0.15625
|
||
// height: 0.15625
|
||
// vert:
|
||
// serializedVersion: 2
|
||
// x: 0
|
||
// y: 5
|
||
// width: 10
|
||
// height: -10
|
||
// advance: 10
|
||
// flipped: 0
|
||
|
||
charList.Clear();
|
||
|
||
// 匹配每个字符块(从 index: 到下一个 index: 或 m_KerningValues 之前)
|
||
var charBlockPattern = new Regex(
|
||
@"index:\s*(-?\d+)\s*\n\s*uv:\s*\n\s*serializedVersion:\s*\d+\s*\n" +
|
||
@"\s*x:\s*([-0-9.]+)\s*\n\s*y:\s*([-0-9.]+)\s*\n" +
|
||
@"\s*width:\s*([-0-9.]+)\s*\n\s*height:\s*([-0-9.]+)\s*\n" +
|
||
@"\s*vert:\s*\n\s*serializedVersion:\s*\d+\s*\n" +
|
||
@"\s*x:\s*([-0-9.]+)\s*\n\s*y:\s*([-0-9.]+)\s*\n" +
|
||
@"\s*width:\s*([-0-9.]+)\s*\n\s*height:\s*([-0-9.]+)\s*\n" +
|
||
@"\s*advance:\s*(-?[-0-9.]+)",
|
||
RegexOptions.Multiline);
|
||
|
||
foreach (Match m in charBlockPattern.Matches(text))
|
||
{
|
||
int charId = int.Parse(m.Groups[1].Value);
|
||
float uvX = float.Parse(m.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||
float uvY = float.Parse(m.Groups[3].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||
float uvW = float.Parse(m.Groups[4].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||
float uvH = float.Parse(m.Groups[5].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||
// vert 字段在 UV 原点已是左下时不需要用于 rect 计算,width/height 可作为像素尺寸参考
|
||
// 但 Unity Font 的 vert.width/height 是字符绘制尺寸,应与 uv*textureSize 一致
|
||
// 这里使用 vert 的 width/height 作为像素宽高(更可靠)
|
||
float vertW = float.Parse(m.Groups[8].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||
float vertH = float.Parse(m.Groups[9].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||
|
||
// UV 是归一化坐标,原点左下,y 向上 → 与 Unity Sprite rect 一致,不需要翻转 y
|
||
// 实际像素坐标需在拿到 PNG 实际尺寸后才能计算,这里先存 UV,等 RecomputeRectsForPngSize 转换
|
||
// 为简化处理,这里直接用 PNG 实际尺寸换算(若 targetTexture 已设置)
|
||
int pixX, pixY, pixW, pixH;
|
||
if (targetTexture != null)
|
||
{
|
||
int texW = targetTexture.width;
|
||
int texH = targetTexture.height;
|
||
pixX = Mathf.RoundToInt(uvX * texW);
|
||
pixY = Mathf.RoundToInt(uvY * texH);
|
||
pixW = Mathf.RoundToInt(uvW * texW);
|
||
pixH = Mathf.RoundToInt(uvH * texH);
|
||
}
|
||
else
|
||
{
|
||
// PNG 未设置时暂存 0,等 PNG 加载后重算
|
||
pixX = 0; pixY = 0; pixW = Mathf.RoundToInt(vertW); pixH = Mathf.RoundToInt(Mathf.Abs(vertH));
|
||
}
|
||
|
||
var info = new FontCharInfo
|
||
{
|
||
id = charId,
|
||
x = pixX,
|
||
y = pixY,
|
||
width = pixW,
|
||
height = pixH,
|
||
advance = int.Parse(m.Groups[10].Value),
|
||
};
|
||
info.unityRect = new Rect(info.x, info.y, info.width, info.height);
|
||
charList.Add(info);
|
||
}
|
||
}
|
||
|
||
/// <summary>当 PNG 实际尺寸变化时,重新计算字符 rect(主要用于 Unity Font 模式)</summary>
|
||
private void RecomputeRectsForPngSize()
|
||
{
|
||
if (fontFormat != FontFormat.UnityFontAsset || targetTexture == null) return;
|
||
|
||
// 重新解析(因为 Unity Font 的 UV 需要乘以 PNG 尺寸)
|
||
if (!string.IsNullOrEmpty(fontAssetPath) && File.Exists(Path.GetFullPath(fontAssetPath)))
|
||
{
|
||
string text = File.ReadAllText(Path.GetFullPath(fontAssetPath), Encoding.UTF8);
|
||
ParseUnityFontAsset(text);
|
||
}
|
||
}
|
||
|
||
/// <summary>从 BMFont page file 自动查找 PNG</summary>
|
||
private void AutoFindPngFromBmFontPage(string text, string fntAssetPath)
|
||
{
|
||
var pageMatch = Regex.Match(text, @"^page\s+id=\d+\s+file=""([^""]+)""",
|
||
RegexOptions.Multiline);
|
||
if (!pageMatch.Success) return;
|
||
|
||
string pageFileName = pageMatch.Groups[1].Value;
|
||
|
||
// 优先从 .fnt 同目录查找
|
||
string fntDir = Path.GetDirectoryName(fntAssetPath).Replace('\\', '/');
|
||
string candidatePath = fntDir + "/" + pageFileName;
|
||
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(candidatePath);
|
||
if (tex == null)
|
||
{
|
||
// 在整个项目中按文件名搜索
|
||
string nameWithoutExt = Path.GetFileNameWithoutExtension(pageFileName);
|
||
string[] guids = AssetDatabase.FindAssets(nameWithoutExt + " t:Texture2D");
|
||
foreach (string g in guids)
|
||
{
|
||
string p = AssetDatabase.GUIDToAssetPath(g);
|
||
if (p.EndsWith("/" + pageFileName, System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
tex = AssetDatabase.LoadAssetAtPath<Texture2D>(p);
|
||
if (tex != null) break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tex != null)
|
||
{
|
||
targetTexture = tex;
|
||
namePrefix = Path.GetFileNameWithoutExtension(pageFileName);
|
||
}
|
||
}
|
||
|
||
/// <summary>把字符 id 映射成可读的名称标签</summary>
|
||
private string CharToLabel(int charId)
|
||
{
|
||
switch (charId)
|
||
{
|
||
case 43: return "+";
|
||
case 44: return ",";
|
||
case 45: return "-";
|
||
case 46: return ".";
|
||
case 47: return "/";
|
||
case 58: return ":";
|
||
case 33: return "!";
|
||
case 63: return "?";
|
||
case 40: return "(";
|
||
case 41: return ")";
|
||
case 91: return "[";
|
||
case 93: return "]";
|
||
case 123: return "{";
|
||
case 125: return "}";
|
||
case 120: return "x";
|
||
}
|
||
if (charId >= 48 && charId <= 57) return (charId - 48).ToString();
|
||
if (charId >= 65 && charId <= 90) return ((char)charId).ToString();
|
||
if (charId >= 97 && charId <= 122) return ((char)charId).ToString();
|
||
return $"u{charId}";
|
||
}
|
||
|
||
/// <summary>把字符 id 映射成可作为 sprite 名称的英文标识符</summary>
|
||
private string CharToName(int charId)
|
||
{
|
||
switch (charId)
|
||
{
|
||
case 43: return "plus";
|
||
case 44: return "comma";
|
||
case 45: return "minus";
|
||
case 46: return "dot";
|
||
case 47: return "slash";
|
||
case 58: return "colon";
|
||
case 33: return "excl";
|
||
case 63: return "quest";
|
||
case 40: return "lparen";
|
||
case 41: return "rparen";
|
||
case 91: return "lbracket";
|
||
case 93: return "rbracket";
|
||
case 123: return "lbrace";
|
||
case 125: return "rbrace";
|
||
case 120: return "x";
|
||
}
|
||
if (charId >= 48 && charId <= 57) return (charId - 48).ToString();
|
||
if (charId >= 65 && charId <= 90) return ((char)charId).ToString();
|
||
if (charId >= 97 && charId <= 122) return ((char)charId).ToString();
|
||
return $"u{charId}";
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Meta 写入
|
||
|
||
/// <summary>主入口:把字符列表写入 PNG.meta</summary>
|
||
private void WriteSpritesToMeta()
|
||
{
|
||
if (targetTexture == null || charList.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "请先加载字体文件并选择目标 PNG", "确定");
|
||
return;
|
||
}
|
||
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
string metaPath = pngPath + ".meta";
|
||
if (!File.Exists(metaPath))
|
||
{
|
||
EditorUtility.DisplayDialog("错误", $"找不到 meta 文件:\n{metaPath}", "确定");
|
||
return;
|
||
}
|
||
|
||
// 对 BMFont 格式,使用 PNG 实际尺寸做 Y 翻转(如果与 fnt 声明不同,按实际尺寸为准)
|
||
if (fontFormat == FontFormat.BmFontFnt)
|
||
{
|
||
int texW = targetTexture.width;
|
||
int texH = targetTexture.height;
|
||
if (texW != fontTextureW || texH != fontTextureH)
|
||
{
|
||
Debug.LogWarning($"[FntSpriteSplitter] PNG 实际尺寸 {texW}x{texH} 与 fnt 声明 {fontTextureW}x{fontTextureH} 不一致,按实际尺寸翻转 y");
|
||
foreach (var ch in charList)
|
||
{
|
||
// 重新基于实际 texH 翻转:先还原 fnt 中的 y,再翻转
|
||
// fnt_y = fontTextureH - unity_y - height → 重新 unity_y = texH - fnt_y - height
|
||
int fntY = fontTextureH - (int)ch.unityRect.y - ch.height;
|
||
int flipY = texH - fntY - ch.height;
|
||
ch.unityRect = new Rect(ch.x, flipY, ch.width, ch.height);
|
||
}
|
||
}
|
||
}
|
||
else if (fontFormat == FontFormat.UnityFontAsset)
|
||
{
|
||
// Unity Font 模式:UV 已根据 PNG 实际尺寸换算,无需额外处理
|
||
RecomputeRectsForPngSize();
|
||
}
|
||
|
||
string prefix = string.IsNullOrEmpty(namePrefix)
|
||
? Path.GetFileNameWithoutExtension(pngPath)
|
||
: namePrefix;
|
||
|
||
StringBuilder sb = new StringBuilder();
|
||
StringBuilder spritesYaml = new StringBuilder();
|
||
List<string> nameFileIdEntries = new List<string>();
|
||
|
||
for (int i = 0; i < charList.Count; i++)
|
||
{
|
||
var ch = charList[i];
|
||
string spriteName = nameByIndex
|
||
? $"{prefix}_{i}"
|
||
: $"{prefix}_{CharToName(ch.id)}";
|
||
|
||
spritesYaml.AppendLine(" - serializedVersion: 2");
|
||
spritesYaml.AppendLine($" name: {spriteName}");
|
||
spritesYaml.AppendLine(" rect:");
|
||
spritesYaml.AppendLine(" serializedVersion: 2");
|
||
spritesYaml.AppendLine($" x: {(int)ch.unityRect.x}");
|
||
spritesYaml.AppendLine($" y: {(int)ch.unityRect.y}");
|
||
spritesYaml.AppendLine($" width: {(int)ch.unityRect.width}");
|
||
spritesYaml.AppendLine($" height: {(int)ch.unityRect.height}");
|
||
spritesYaml.AppendLine(" alignment: 0");
|
||
spritesYaml.AppendLine(" pivot: {x: 0.5, y: 0.5}");
|
||
spritesYaml.AppendLine(" border: {x: 0, y: 0, z: 0, w: 0}");
|
||
spritesYaml.AppendLine(" outline: []");
|
||
spritesYaml.AppendLine(" physicsShape: []");
|
||
spritesYaml.AppendLine(" tessellationDetail: -1");
|
||
spritesYaml.AppendLine(" bones: []");
|
||
spritesYaml.AppendLine($" spriteID: {GenerateSpriteID(spriteName)}");
|
||
spritesYaml.AppendLine(" internalID: 0");
|
||
spritesYaml.AppendLine(" vertices: []");
|
||
spritesYaml.AppendLine(" indices: ");
|
||
spritesYaml.AppendLine(" edges: []");
|
||
spritesYaml.AppendLine(" weights: []");
|
||
|
||
nameFileIdEntries.Add($" {spriteName}: 0");
|
||
|
||
sb.AppendLine($" [+] {spriteName} rect=({(int)ch.unityRect.x},{(int)ch.unityRect.y},{(int)ch.unityRect.width}x{(int)ch.unityRect.height})");
|
||
}
|
||
|
||
string spritesContent = " sprites:\n" + spritesYaml.ToString().TrimEnd('\n', '\r') + "\n";
|
||
string tableContent = " nameFileIdTable:\n" +
|
||
string.Join("\n", nameFileIdEntries) + "\n";
|
||
|
||
// 读取 meta
|
||
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
|
||
string result = metaText;
|
||
|
||
// 替换 sprites 块
|
||
if (clearBeforeWrite || metaText.Contains("sprites: []"))
|
||
{
|
||
if (metaText.Contains(" sprites: []"))
|
||
{
|
||
result = ReplaceFirst(result, " sprites: []", spritesContent);
|
||
}
|
||
else
|
||
{
|
||
int idx = result.IndexOf(" sprites:");
|
||
if (idx >= 0)
|
||
{
|
||
int endIdx = FindBlockEnd(result, idx);
|
||
result = result.Substring(0, idx) + spritesContent + result.Substring(endIdx);
|
||
}
|
||
else
|
||
{
|
||
// 没有 sprites 字段,找 spriteSheet: 后插入
|
||
int sheetIdx = result.IndexOf(" spriteSheet:");
|
||
if (sheetIdx >= 0)
|
||
{
|
||
int lineEnd = result.IndexOf('\n', sheetIdx);
|
||
result = result.Substring(0, lineEnd + 1) + spritesContent + result.Substring(lineEnd + 1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 追加模式:在现有 sprites 块末尾插入新条目
|
||
int idx = result.IndexOf(" sprites:");
|
||
if (idx >= 0)
|
||
{
|
||
int blockEnd = FindBlockEnd(result, idx);
|
||
string newEntries = spritesYaml.ToString();
|
||
result = result.Substring(0, blockEnd) + newEntries + result.Substring(blockEnd);
|
||
}
|
||
else
|
||
{
|
||
result = ReplaceFirst(result, " sprites: []", spritesContent);
|
||
}
|
||
}
|
||
|
||
// 替换 nameFileIdTable 块
|
||
int tblIdx = result.IndexOf(" nameFileIdTable:");
|
||
if (tblIdx >= 0)
|
||
{
|
||
if (clearBeforeWrite || result.Contains(" nameFileIdTable: {}"))
|
||
{
|
||
int tblEndIdx = FindBlockEnd(result, tblIdx);
|
||
result = result.Substring(0, tblIdx) + tableContent + result.Substring(tblEndIdx);
|
||
}
|
||
else
|
||
{
|
||
int tblEndIdx = FindBlockEnd(result, tblIdx);
|
||
string extra = string.Join("\n", nameFileIdEntries) + "\n";
|
||
result = result.Substring(0, tblEndIdx) + extra + result.Substring(tblEndIdx);
|
||
}
|
||
}
|
||
|
||
File.WriteAllText(metaPath, result, new UTF8Encoding(false));
|
||
AssetDatabase.ImportAsset(pngPath, ImportAssetOptions.ForceUpdate);
|
||
|
||
sb.AppendLine($"\n[完成] 已写入 {charList.Count} 个 Sprite 到:");
|
||
sb.AppendLine($" {metaPath}");
|
||
sb.AppendLine("\n→ Unity 已自动 Reimport,请在 Inspector / Sprite Editor 中确认。");
|
||
|
||
logText = sb.ToString();
|
||
showLog = true;
|
||
Debug.Log("[FntSpriteSplitter]\n" + logText);
|
||
}
|
||
|
||
/// <summary>清空 PNG.meta 中所有 sprites</summary>
|
||
private void ClearSpritesInMeta()
|
||
{
|
||
if (targetTexture == null) return;
|
||
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
string metaPath = pngPath + ".meta";
|
||
if (!File.Exists(metaPath)) return;
|
||
|
||
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
|
||
|
||
string result = metaText;
|
||
int idx = result.IndexOf(" sprites:");
|
||
if (idx >= 0)
|
||
{
|
||
int endIdx = FindBlockEnd(result, idx);
|
||
result = result.Substring(0, idx) + " sprites: []\n" + result.Substring(endIdx);
|
||
}
|
||
|
||
int tblIdx = result.IndexOf(" nameFileIdTable:");
|
||
if (tblIdx >= 0)
|
||
{
|
||
int tblEndIdx = FindBlockEnd(result, tblIdx);
|
||
result = result.Substring(0, tblIdx) + " nameFileIdTable: {}\n" + result.Substring(tblEndIdx);
|
||
}
|
||
|
||
File.WriteAllText(metaPath, result, new UTF8Encoding(false));
|
||
AssetDatabase.ImportAsset(pngPath, ImportAssetOptions.ForceUpdate);
|
||
|
||
logText = $"[清空] 已清空 {Path.GetFileName(metaPath)} 中所有 sprites";
|
||
showLog = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 基于已解析的字符列表生成 TMP_SpriteAsset。
|
||
/// 字符表 m_Unicode 使用字体中的 u8 字符 id,m_GlyphIndex 对应 glyph 表下标,
|
||
/// 因此既支持 <sprite=索引> 也支持 <sprite=字符名> 引用。
|
||
/// glyph 的 sprite 引用会从 PNG 的子 Sprite 中按「前缀_下标」匹配(需先执行「写入 Meta」分割)。
|
||
/// </summary>
|
||
private void GenerateTMPSpriteAsset()
|
||
{
|
||
if (targetTexture == null || charList.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "请先加载字体文件并选择目标 PNG", "确定");
|
||
return;
|
||
}
|
||
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
string prefix = string.IsNullOrEmpty(namePrefix)
|
||
? Path.GetFileNameWithoutExtension(pngPath)
|
||
: namePrefix;
|
||
|
||
// 从 PNG 加载所有子 Sprite(需先分割过 meta)
|
||
var allAssets = AssetDatabase.LoadAllAssetsAtPath(pngPath);
|
||
Dictionary<string, Sprite> spriteByName = new Dictionary<string, Sprite>();
|
||
foreach (var a in allAssets)
|
||
{
|
||
if (a is Sprite spr)
|
||
spriteByName[spr.name] = spr;
|
||
}
|
||
|
||
// 创建 SpriteAsset
|
||
var spriteAsset = ScriptableObject.CreateInstance<TMP_SpriteAsset>();
|
||
spriteAsset.name = prefix + "_SpriteAsset";
|
||
spriteAsset.spriteSheet = targetTexture;
|
||
|
||
// 设置版本号,避免 getter/UpdateLookupTables 触发 UpgradeSpriteAsset
|
||
// (默认 m_Version 为空,UpgradeSpriteAsset 会访问未初始化的 spriteInfoList 导致 NullReferenceException)
|
||
var versionField = typeof(TMP_SpriteAsset).GetField("m_Version",
|
||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
||
if (versionField != null)
|
||
versionField.SetValue(spriteAsset, "1.1.0");
|
||
|
||
// 创建 Sprite 材质(TextMeshPro/Sprite shader)
|
||
Shader shader = Shader.Find("TextMeshPro/Sprite");
|
||
if (shader == null)
|
||
shader = Shader.Find("TextMeshPro/Mobile/Distance Field");
|
||
Material mat = new Material(shader);
|
||
mat.name = "TextMeshPro/Sprite";
|
||
mat.mainTexture = targetTexture;
|
||
spriteAsset.material = mat;
|
||
|
||
var glyphTable = new List<TMP_SpriteGlyph>();
|
||
var charTable = new List<TMP_SpriteCharacter>();
|
||
|
||
for (int i = 0; i < charList.Count; i++)
|
||
{
|
||
var ch = charList[i];
|
||
|
||
// glyph:按子 Sprite 命名「前缀_下标」查找引用
|
||
string spriteName = prefix + "_" + i;
|
||
Sprite spr;
|
||
spriteByName.TryGetValue(spriteName, out spr);
|
||
|
||
var glyph = new TMP_SpriteGlyph();
|
||
glyph.index = (uint)i;
|
||
glyph.metrics = new GlyphMetrics(
|
||
ch.width, ch.height,
|
||
-ch.width / 2f, ch.height / 2f,
|
||
ch.width);
|
||
glyph.glyphRect = new GlyphRect(
|
||
(int)ch.unityRect.x, (int)ch.unityRect.y,
|
||
(int)ch.unityRect.width, (int)ch.unityRect.height);
|
||
glyph.scale = 1f;
|
||
glyph.atlasIndex = 0;
|
||
glyph.sprite = spr;
|
||
glyphTable.Add(glyph);
|
||
|
||
// character:m_Unicode 使用 u8 字符 id,m_Name 为字符本身
|
||
var character = new TMP_SpriteCharacter();
|
||
character.unicode = (uint)ch.id;
|
||
character.glyphIndex = (uint)i;
|
||
character.scale = 1f;
|
||
character.name = CharToName(ch.id);
|
||
charTable.Add(character);
|
||
}
|
||
|
||
// setter 为 internal,改用公开 getter 返回的列表直接填充
|
||
spriteAsset.spriteGlyphTable.Clear();
|
||
spriteAsset.spriteGlyphTable.AddRange(glyphTable);
|
||
spriteAsset.spriteCharacterTable.Clear();
|
||
spriteAsset.spriteCharacterTable.AddRange(charTable);
|
||
spriteAsset.spriteInfoList = new List<TMP_Sprite>();
|
||
spriteAsset.fallbackSpriteAssets = new List<TMP_SpriteAsset>();
|
||
spriteAsset.UpdateLookupTables();
|
||
|
||
// 保存:材质作为 SpriteAsset 的子资源,结构与现有 asset 一致
|
||
string dir = Path.GetDirectoryName(pngPath).Replace('\\', '/');
|
||
string assetPath = dir + "/" + spriteAsset.name + ".asset";
|
||
assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath);
|
||
|
||
AssetDatabase.CreateAsset(spriteAsset, assetPath);
|
||
AssetDatabase.AddObjectToAsset(mat, spriteAsset);
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
|
||
|
||
int missingSprite = 0;
|
||
foreach (var g in glyphTable)
|
||
if (g.sprite == null)
|
||
missingSprite++;
|
||
|
||
StringBuilder sb = new StringBuilder();
|
||
sb.AppendLine($"[完成] 已生成 TMP_SpriteAsset:");
|
||
sb.AppendLine($" {assetPath}");
|
||
int[] charCodes = new int[charList.Count];
|
||
for (int i = 0; i < charList.Count; i++) charCodes[i] = charList[i].id;
|
||
sb.AppendLine($" 字符 id 码: {string.Join(",", charCodes)}");
|
||
sb.AppendLine($" 字符表: {charTable.Count} 个, glyph 表: {glyphTable.Count} 个");
|
||
if (missingSprite > 0)
|
||
sb.AppendLine($" ⚠ {missingSprite} 个 glyph 未匹配到子 Sprite(请先执行「写入 Meta」分割)");
|
||
sb.AppendLine("\n→ 使用方法: tmp.spriteAsset = 该资源; text = \"<sprite=0><sprite=1>...\"");
|
||
|
||
logText = sb.ToString();
|
||
showLog = true;
|
||
Debug.Log("[FntSpriteSplitter] 生成 TMP_SpriteAsset\n" + logText);
|
||
|
||
EditorGUIUtility.PingObject(spriteAsset);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 工具方法
|
||
|
||
/// <summary>重置所有状态</summary>
|
||
private void ClearAll()
|
||
{
|
||
fontAssetPath = "";
|
||
fontFormat = FontFormat.Unknown;
|
||
targetTexture = null;
|
||
charList.Clear();
|
||
fontTextureW = 0;
|
||
fontTextureH = 0;
|
||
namePrefix = "";
|
||
logText = "";
|
||
Repaint();
|
||
}
|
||
|
||
/// <summary>替换字符串中第一次出现的 oldText</summary>
|
||
private string ReplaceFirst(string text, string oldText, string newText)
|
||
{
|
||
int idx = text.IndexOf(oldText);
|
||
if (idx < 0) return text;
|
||
return text.Substring(0, idx) + newText + text.Substring(idx + oldText.Length);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 blockStartIndex 开始,找到当前 YAML 块的结束位置。
|
||
/// 块以 " keyName:" 开头,后续行要么以 " - " 继续列表项,
|
||
/// 要么以更多缩进(6+ 空格)作为子属性。
|
||
/// 遇到同级缩进(4 空格 + 非 "-" 非空格)的行时结束。
|
||
/// </summary>
|
||
private int FindBlockEnd(string text, int blockStartIndex)
|
||
{
|
||
int i = blockStartIndex;
|
||
int lineEnd = text.IndexOf('\n', i);
|
||
if (lineEnd < 0) return text.Length;
|
||
i = lineEnd + 1;
|
||
|
||
while (i < text.Length)
|
||
{
|
||
int nextNewline = text.IndexOf('\n', i);
|
||
if (nextNewline < 0) nextNewline = text.Length;
|
||
string line = text.Substring(i, nextNewline - i);
|
||
|
||
if (line.Trim().Length == 0)
|
||
{
|
||
i = nextNewline + 1;
|
||
continue;
|
||
}
|
||
|
||
if (line.StartsWith(" - "))
|
||
{
|
||
i = nextNewline + 1;
|
||
continue;
|
||
}
|
||
|
||
if (line.StartsWith(" "))
|
||
{
|
||
i = nextNewline + 1;
|
||
continue;
|
||
}
|
||
|
||
if (line.StartsWith(" ") && !line.StartsWith(" - ") && !line.StartsWith(" "))
|
||
{
|
||
return i;
|
||
}
|
||
|
||
return i;
|
||
}
|
||
|
||
return text.Length;
|
||
}
|
||
|
||
/// <summary>基于 sprite 名称生成稳定的 32 位 hex spriteID</summary>
|
||
private string GenerateSpriteID(string name)
|
||
{
|
||
using (var md5 = System.Security.Cryptography.MD5.Create())
|
||
{
|
||
byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes("fnt-sprite-" + name));
|
||
var sb = new StringBuilder();
|
||
foreach (byte b in bytes) sb.Append(b.ToString("x2"));
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|