887 lines
29 KiB
C#
887 lines
29 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.IO;
|
||
using System.Text;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
|
||
/// <summary>
|
||
/// SpriteMetaRestorer Editor 窗口
|
||
///
|
||
/// 功能:将独立 .asset Sprite 文件的分割信息还原到 PNG.meta 的 spriteSheet.sprites 中。
|
||
/// 使用方法:
|
||
/// 1. 把 PNG 图片拖到"目标 PNG"槽位
|
||
/// 2. 把对应的 .asset Sprite 文件拖到列表区域(支持多选拖入)
|
||
/// 3. 点击"写入 Meta"完成还原
|
||
/// 4. 可选:勾选"写入后删除已融合的 .asset 文件"
|
||
///
|
||
/// 作者:AI Assistant
|
||
/// </summary>
|
||
public class SpriteMetaRestorer : EditorWindow
|
||
{
|
||
#region 字段
|
||
|
||
/// <summary>目标 PNG 纹理</summary>
|
||
private Texture2D targetTexture;
|
||
|
||
/// <summary>待处理的 Sprite .asset 文件列表</summary>
|
||
private List<SpriteAssetInfo> assetList = new List<SpriteAssetInfo>();
|
||
|
||
/// <summary>拖入区域的滚动位置</summary>
|
||
private Vector2 scrollPos;
|
||
|
||
/// <summary>处理结果日志</summary>
|
||
private string logText = "";
|
||
private Vector2 logScrollPos;
|
||
|
||
/// <summary>是否显示日志区域</summary>
|
||
private bool showLog = false;
|
||
|
||
/// <summary>写入完成后是否删除已融合的 .asset 文件及其 .meta</summary>
|
||
private bool deleteAssetsAfterWrite = true;
|
||
|
||
/// <summary>是否允许写入与 PNG 不匹配的 Sprite(textureGuid 不同)</summary>
|
||
private bool allowMismatchedSprites = false;
|
||
|
||
|
||
#endregion
|
||
|
||
#region 数据结构
|
||
|
||
/// <summary>单个 Sprite .asset 的解析信息</summary>
|
||
private class SpriteAssetInfo
|
||
{
|
||
/// <summary>.asset 文件路径(Assets/...)</summary>
|
||
public string assetPath;
|
||
|
||
/// <summary>Sprite 名称(m_Name)</summary>
|
||
public string name;
|
||
|
||
/// <summary>裁切矩形</summary>
|
||
public Rect rect;
|
||
|
||
/// <summary>轴心点</summary>
|
||
public Vector2 pivot;
|
||
|
||
/// <summary>边框</summary>
|
||
public Vector4 border;
|
||
|
||
/// <summary>spriteID(来自 .asset.meta 的 guid)</summary>
|
||
public string spriteID;
|
||
|
||
/// <summary>该 .asset 引用的纹理 guid,用于校验是否与目标 PNG 匹配</summary>
|
||
public string textureGuid;
|
||
|
||
/// <summary>解析是否成功</summary>
|
||
public bool valid;
|
||
|
||
public SpriteAssetInfo()
|
||
{
|
||
valid = false;
|
||
spriteID = "";
|
||
textureGuid = "";
|
||
rect = new Rect(0, 0, 0, 0);
|
||
pivot = new Vector2(0.5f, 0.5f);
|
||
border = new Vector4(0, 0, 0, 0);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 菜单入口
|
||
|
||
[MenuItem("Tools/Sprite Meta Restorer")]
|
||
private static void ShowWindow()
|
||
{
|
||
// 获取已有窗口或创建新窗口
|
||
var window = GetWindow<SpriteMetaRestorer>("Sprite Meta Restorer");
|
||
window.minSize = new Vector2(420, 500);
|
||
window.Show();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region GUI 绘制
|
||
|
||
private void OnGUI()
|
||
{
|
||
DrawHeader();
|
||
DrawTargetPNGSlot();
|
||
DrawAssetDropZone();
|
||
DrawActionBar();
|
||
DrawLogArea();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 顶部标题说明
|
||
/// </summary>
|
||
private void DrawHeader()
|
||
{
|
||
EditorGUILayout.Space(8);
|
||
EditorGUILayout.HelpBox(
|
||
"将 Sprite .asset 文件的分割信息还原到 PNG.meta 的 spriteSheet.sprites 中。\n" +
|
||
"1. 拖入目标 PNG 图片\n" +
|
||
"2. 拖入对应的 .asset 文件(支持多选)\n" +
|
||
"3. 点击「写入 Meta」",
|
||
MessageType.Info
|
||
);
|
||
EditorGUILayout.Space(4);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 目标 PNG 拖拽槽位
|
||
/// </summary>
|
||
private void DrawTargetPNGSlot()
|
||
{
|
||
EditorGUILayout.LabelField("目标 PNG", EditorStyles.boldLabel);
|
||
|
||
// 使用 ObjectField 支持拖拽和点击选择
|
||
Rect dropRect = EditorGUILayout.GetControlRect(GUILayout.Height(60));
|
||
GUI.Box(dropRect, "", GUI.skin.box);
|
||
|
||
var centeredStyle = new GUIStyle(GUI.skin.label)
|
||
{
|
||
alignment = TextAnchor.MiddleCenter,
|
||
fontSize = 12
|
||
};
|
||
|
||
if (targetTexture != null)
|
||
{
|
||
// 显示已选择的 PNG 信息
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
int texW = targetTexture.width;
|
||
int texH = targetTexture.height;
|
||
GUI.Label(dropRect, $"{Path.GetFileName(pngPath)}\n{texW} x {texH} px", centeredStyle);
|
||
}
|
||
else
|
||
{
|
||
GUI.Label(dropRect, "拖入 PNG 图片 或 点击选择", centeredStyle);
|
||
}
|
||
|
||
// 处理拖拽事件
|
||
if (Event.current.type == EventType.DragUpdated && dropRect.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
Event.current.Use();
|
||
}
|
||
else if (Event.current.type == EventType.DragPerform && dropRect.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.AcceptDrag();
|
||
if (DragAndDrop.objectReferences.Length > 0)
|
||
{
|
||
var obj = DragAndDrop.objectReferences[0] as Texture2D;
|
||
if (obj != null)
|
||
{
|
||
string path = AssetDatabase.GetAssetPath(obj);
|
||
// 确保是 PNG 文件而非内部纹理
|
||
if (path.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
targetTexture = obj;
|
||
assetList.Clear();
|
||
logText = "";
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[SpriteMetaRestorer] 请拖入 PNG 文件,不是内部纹理");
|
||
}
|
||
}
|
||
}
|
||
Event.current.Use();
|
||
}
|
||
|
||
// 点击选择
|
||
targetTexture = (Texture2D)EditorGUI.ObjectField(
|
||
new Rect(dropRect.x, dropRect.y, dropRect.width, dropRect.height),
|
||
targetTexture, typeof(Texture2D), false
|
||
);
|
||
|
||
// 检查选择后是否为 PNG
|
||
if (targetTexture != null)
|
||
{
|
||
string p = AssetDatabase.GetAssetPath(targetTexture);
|
||
if (!p.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
targetTexture = null;
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.Space(8);
|
||
}
|
||
|
||
/// <summary>
|
||
/// .asset 文件拖拽列表区域
|
||
/// </summary>
|
||
private void DrawAssetDropZone()
|
||
{
|
||
EditorGUILayout.LabelField($"Sprite Assets ({assetList.Count} 个)", EditorStyles.boldLabel);
|
||
|
||
// 列表区域
|
||
Rect listRect = EditorGUILayout.GetControlRect(false, GUILayout.Height(200));
|
||
GUI.Box(listRect, "");
|
||
|
||
// 空状态提示
|
||
if (assetList.Count == 0 && targetTexture != null)
|
||
{
|
||
// 创建带灰色的 GUIStyle
|
||
var hintStyle = new GUIStyle(GUI.skin.label);
|
||
hintStyle.alignment = TextAnchor.MiddleCenter;
|
||
hintStyle.fontSize = 12;
|
||
hintStyle.normal.textColor = Color.gray;
|
||
GUI.Label(listRect, "拖入 .asset Sprite 文件到这里\n(支持 Project 窗口多选拖入)", hintStyle);
|
||
}
|
||
|
||
// 处理拖拽
|
||
if (Event.current.type == EventType.DragUpdated && listRect.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
Event.current.Use();
|
||
}
|
||
else if (Event.current.type == EventType.DragPerform && listRect.Contains(Event.current.mousePosition))
|
||
{
|
||
DragAndDrop.AcceptDrag();
|
||
AddDraggedAssets(DragAndDrop.objectReferences);
|
||
Event.current.Use();
|
||
}
|
||
|
||
// 绘制列表(带滚动)
|
||
if (assetList.Count > 0)
|
||
{
|
||
var innerRect = new Rect(listRect.x + 4, listRect.y + 4, listRect.width - 8, listRect.height - 8);
|
||
scrollPos = GUI.BeginScrollView(innerRect, scrollPos, new Rect(0, 0, innerRect.width - 16, assetList.Count * 22));
|
||
|
||
for (int i = 0; i < assetList.Count; i++)
|
||
{
|
||
var info = assetList[i];
|
||
Rect rowRect = new Rect(0, i * 22, innerRect.width, 20);
|
||
|
||
// 背景色:有效=正常,无效=红底
|
||
Color origBg = GUI.backgroundColor;
|
||
GUI.backgroundColor = info.valid ? Color.green : Color.red;
|
||
|
||
// 名称
|
||
float nameW = rowRect.width * 0.35f;
|
||
GUI.Label(new Rect(rowRect.x, rowRect.y, nameW, 20), info.name);
|
||
|
||
// 坐标
|
||
float coordW = rowRect.width * 0.35f;
|
||
string coordText = $"({info.rect.x:F0},{info.rect.y:F0},{info.rect.width:F0}x{info.rect.height:F0})";
|
||
GUI.Label(new Rect(rowRect.x + nameW, rowRect.y, coordW, 20), coordText);
|
||
|
||
// 匹配状态
|
||
float statusW = rowRect.width * 0.15f;
|
||
if (targetTexture != null)
|
||
{
|
||
string pngGuid = GetAssetGuid(AssetDatabase.GetAssetPath(targetTexture) + ".meta");
|
||
bool matched = info.textureGuid == pngGuid;
|
||
// 正确方式:通过 GUIStyle.normal.textColor 设置颜色
|
||
var statusStyle = new GUIStyle(GUI.skin.label);
|
||
statusStyle.fontSize = 10;
|
||
statusStyle.normal.textColor = matched ? Color.green : Color.red;
|
||
GUI.Label(new Rect(rowRect.x + nameW + coordW, rowRect.y, statusW, 20),
|
||
matched ? "✓ 匹配" : "✗ 不匹配", statusStyle);
|
||
}
|
||
|
||
// 删除按钮
|
||
float btnW = rowRect.width * 0.12f;
|
||
if (GUI.Button(new Rect(rowRect.x + rowRect.width - btnW, rowRect.y, btnW, 20), "删除"))
|
||
{
|
||
assetList.RemoveAt(i);
|
||
i--;
|
||
}
|
||
|
||
GUI.backgroundColor = origBg;
|
||
}
|
||
|
||
GUI.EndScrollView();
|
||
}
|
||
|
||
EditorGUILayout.Space(6);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 操作按钮区
|
||
/// </summary>
|
||
private void DrawActionBar()
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
// 写入按钮
|
||
GUI.enabled = targetTexture != null && assetList.Count > 0;
|
||
if (GUILayout.Button("写入 Meta", GUILayout.Height(32)))
|
||
{
|
||
WriteSpritesToMeta();
|
||
}
|
||
GUI.enabled = true;
|
||
|
||
// 清空按钮
|
||
if (GUILayout.Button("清空列表", GUILayout.Height(32)))
|
||
{
|
||
assetList.Clear();
|
||
logText = "";
|
||
}
|
||
|
||
// 清空全部
|
||
if (GUILayout.Button("全部重置", GUILayout.Height(32)))
|
||
{
|
||
targetTexture = null;
|
||
assetList.Clear();
|
||
logText = "";
|
||
}
|
||
|
||
// 新增:删除所有与 PNG 不匹配的 Sprite
|
||
if (targetTexture != null && assetList.Count > 0)
|
||
{
|
||
if (GUILayout.Button("删除不匹配的 Sprite", GUILayout.Height(32)))
|
||
{
|
||
RemoveMismatchedSprites();
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
// 自动检测按钮
|
||
EditorGUILayout.Space(4);
|
||
if (GUILayout.Button("自动检测同目录 .asset 文件", GUILayout.Height(28)))
|
||
{
|
||
AutoDetectAssets();
|
||
}
|
||
|
||
// 删除 asset 选项
|
||
EditorGUILayout.Space(6);
|
||
deleteAssetsAfterWrite = EditorGUILayout.Toggle(
|
||
new GUIContent("写入后删除已融合的 .asset 文件",
|
||
"勾选后,写入 Meta 成功会自动删除列表中的 .asset 及其 .meta 文件"),
|
||
deleteAssetsAfterWrite
|
||
);
|
||
|
||
// 新增:是否允许写入与 PNG 不匹配的 Sprite
|
||
allowMismatchedSprites = EditorGUILayout.Toggle(
|
||
new GUIContent("允许写入与 PNG 不匹配的 Sprite",
|
||
"勾选:即使 Sprite 引用的纹理 guid 与当前 PNG 不同,也照样按其 rect 写入。\n不勾选:写入时会跳过这些不匹配的 Sprite。"),
|
||
allowMismatchedSprites
|
||
);
|
||
|
||
EditorGUILayout.Space(4);
|
||
}
|
||
|
||
|
||
/// <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(150));
|
||
EditorGUILayout.TextArea(logText, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true));
|
||
EditorGUILayout.EndScrollView();
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 拖拽处理
|
||
|
||
/// <summary>
|
||
/// 处理从 Project 窗口拖入的对象
|
||
/// </summary>
|
||
private void AddDraggedAssets(Object[] objects)
|
||
{
|
||
int added = 0;
|
||
int skipped = 0;
|
||
|
||
foreach (var obj in objects)
|
||
{
|
||
string path = AssetDatabase.GetAssetPath(obj);
|
||
|
||
// 只处理 .asset 文件
|
||
if (string.IsNullOrEmpty(path) ||
|
||
!path.EndsWith(".asset", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
// 检查是否已存在
|
||
if (assetList.Any(a => a.assetPath == path))
|
||
{
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
// 解析 .asset 文件
|
||
var info = ParseSpriteAsset(path);
|
||
if (info != null && info.valid)
|
||
{
|
||
assetList.Add(info);
|
||
added++;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[SpriteMetaRestorer] 无法解析: {path}");
|
||
skipped++;
|
||
}
|
||
}
|
||
|
||
if (added > 0)
|
||
Debug.Log($"[SpriteMetaRestorer] 添加 {added} 个 .asset,跳过 {skipped} 个");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 自动检测
|
||
|
||
/// <summary>
|
||
/// 自动检测目标 PNG 同目录下的所有 Sprite 类型 .asset 文件
|
||
/// </summary>
|
||
private void AutoDetectAssets()
|
||
{
|
||
if (targetTexture == null)
|
||
{
|
||
EditorUtility.DisplayDialog("提示", "请先选择目标 PNG", "确定");
|
||
return;
|
||
}
|
||
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
string pngDir = Path.GetDirectoryName(pngPath).Replace("\\", "/");
|
||
string pngGuid = GetAssetGuid(pngPath + ".meta");
|
||
|
||
if (string.IsNullOrEmpty(pngGuid))
|
||
{
|
||
EditorUtility.DisplayDialog("错误", "无法获取 PNG 的 guid", "确定");
|
||
return;
|
||
}
|
||
|
||
// 查找同目录下所有 .asset 文件
|
||
string[] assetFiles = Directory.GetFiles(pngDir, "*.asset");
|
||
int added = 0;
|
||
|
||
foreach (string file in assetFiles)
|
||
{
|
||
string assetPath = file.Replace("\\", "/");
|
||
|
||
// 跳过已存在的
|
||
if (assetList.Any(a => a.assetPath == assetPath))
|
||
continue;
|
||
|
||
// 解析并检查是否为 Sprite 类型且匹配当前 PNG
|
||
var info = ParseSpriteAsset(assetPath);
|
||
if (info != null && info.valid)
|
||
{
|
||
// 检查 textureGuid 是否匹配
|
||
if (info.textureGuid == pngGuid)
|
||
{
|
||
assetList.Add(info);
|
||
added++;
|
||
}
|
||
}
|
||
}
|
||
|
||
Debug.Log($"[SpriteMetaRestorer] 自动检测到 {added} 个匹配的 .asset 文件");
|
||
Repaint();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region .asset 解析
|
||
|
||
/// <summary>
|
||
/// 解析单个 Sprite .asset 文件,提取名称、rect、pivot、border、textureGuid
|
||
/// </summary>
|
||
private SpriteAssetInfo ParseSpriteAsset(string assetPath)
|
||
{
|
||
string fullPath = Path.GetFullPath(assetPath);
|
||
if (!File.Exists(fullPath))
|
||
return null;
|
||
|
||
string text = File.ReadAllText(fullPath, Encoding.UTF8);
|
||
|
||
// 检查是否是 Sprite 类型(!u!213),而非 MonoBehaviour(!u!114)
|
||
// Split by --- and check second document
|
||
string[] docs = text.Split(new[] { "---" }, System.StringSplitOptions.None);
|
||
if (docs.Length < 2 || !docs[1].Contains("!u!213"))
|
||
return null;
|
||
|
||
var info = new SpriteAssetInfo();
|
||
info.assetPath = assetPath;
|
||
|
||
// 提取 m_Name
|
||
info.name = ExtractField(text, "m_Name");
|
||
if (string.IsNullOrEmpty(info.name))
|
||
info.name = Path.GetFileNameWithoutExtension(assetPath);
|
||
|
||
// 提取 m_Rect
|
||
info.rect = ExtractRect(text);
|
||
|
||
// 提取 m_Pivot
|
||
info.pivot = ExtractVec2(text, "m_Pivot");
|
||
|
||
// 提取 m_Border
|
||
info.border = ExtractVec4(text, "m_Border");
|
||
|
||
// 提取 texture guid
|
||
info.textureGuid = ExtractTextureGuid(text);
|
||
|
||
// 提取 .asset.meta 的 guid 作为 spriteID
|
||
string metaPath = assetPath + ".meta";
|
||
if (File.Exists(metaPath))
|
||
{
|
||
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
|
||
info.spriteID = GetGuidFromText(metaText);
|
||
}
|
||
|
||
// 基本校验
|
||
info.valid = info.rect.width > 0 && info.rect.height > 0;
|
||
|
||
return info;
|
||
}
|
||
|
||
/// <summary>提取 m_Name 字段值</summary>
|
||
private string ExtractField(string text, string fieldName)
|
||
{
|
||
int idx = text.IndexOf(fieldName + ":");
|
||
if (idx < 0) return "";
|
||
int start = idx + fieldName.Length;
|
||
// 跳过冒号后的空格
|
||
while (start < text.Length && (text[start] == ':' || text[start] == ' ' || text[start] == '\t'))
|
||
start++;
|
||
int end = start;
|
||
while (end < text.Length && text[end] != '\n' && text[end] != '\r')
|
||
end++;
|
||
return text.Substring(start, end - start).Trim();
|
||
}
|
||
|
||
/// <summary>提取 m_Rect 块</summary>
|
||
private Rect ExtractRect(string text)
|
||
{
|
||
string marker = "m_Rect:";
|
||
int idx = text.IndexOf(marker);
|
||
if (idx < 0) return new Rect(0, 0, 0, 0);
|
||
|
||
// 找到 marker 之后的 x/y/width/height
|
||
string block = text.Substring(idx);
|
||
float x = ExtractFloatAfter(block, "x:");
|
||
float y = ExtractFloatAfter(block, "y:");
|
||
float w = ExtractFloatAfter(block, "width:");
|
||
float h = ExtractFloatAfter(block, "height:");
|
||
|
||
return new Rect(x, y, w, h);
|
||
}
|
||
|
||
/// <summary>提取 m_Pivot 二维向量</summary>
|
||
private Vector2 ExtractVec2(string text, string fieldName)
|
||
{
|
||
string marker = fieldName + ":";
|
||
int idx = text.IndexOf(marker);
|
||
if (idx < 0) return new Vector2(0.5f, 0.5f);
|
||
|
||
string block = text.Substring(idx);
|
||
float x = ExtractFloatAfter(block, "x:");
|
||
float y = ExtractFloatAfter(block, "y:");
|
||
return new Vector2(x, y);
|
||
}
|
||
|
||
/// <summary>提取 m_Border 四维向量</summary>
|
||
private Vector4 ExtractVec4(string text, string fieldName)
|
||
{
|
||
string marker = fieldName + ":";
|
||
int idx = text.IndexOf(marker);
|
||
if (idx < 0) return new Vector4(0, 0, 0, 0);
|
||
|
||
string block = text.Substring(idx);
|
||
float x = ExtractFloatAfter(block, "x:");
|
||
float y = ExtractFloatAfter(block, "y:");
|
||
float z = ExtractFloatAfter(block, "z:");
|
||
float w = ExtractFloatAfter(block, "w:");
|
||
return new Vector4(x, y, z, w);
|
||
}
|
||
|
||
/// <summary>提取 m_RD.texture 中的 guid</summary>
|
||
private string ExtractTextureGuid(string text)
|
||
{
|
||
int idx = text.IndexOf("texture:");
|
||
if (idx < 0) return "";
|
||
string block = text.Substring(idx);
|
||
|
||
int guidIdx = block.IndexOf("guid:");
|
||
if (guidIdx < 0) return "";
|
||
|
||
int start = guidIdx + 5;
|
||
int end = start;
|
||
while (end < block.Length && block[end] != ',' && block[end] != '}' && block[end] != '\n')
|
||
end++;
|
||
return block.Substring(start, end - start).Trim();
|
||
}
|
||
|
||
/// <summary>在文本中提取指定字段后的浮点数</summary>
|
||
private float ExtractFloatAfter(string text, string fieldName)
|
||
{
|
||
int idx = text.IndexOf(fieldName);
|
||
if (idx < 0) return 0f;
|
||
|
||
int start = idx + fieldName.Length;
|
||
// 跳过到数字
|
||
while (start < text.Length && !char.IsDigit(text[start]) && text[start] != '-' && text[start] != '.')
|
||
start++;
|
||
|
||
int end = start;
|
||
while (end < text.Length && (char.IsDigit(text[end]) || text[end] == '.' || text[end] == '-'
|
||
|| text[end] == 'e' || text[end] == 'E' || text[end] == '+'))
|
||
end++;
|
||
|
||
float result;
|
||
if (float.TryParse(text.Substring(start, end - start), System.Globalization.NumberStyles.Float,
|
||
System.Globalization.CultureInfo.InvariantCulture, out result))
|
||
return result;
|
||
return 0f;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Meta 文件操作
|
||
/// <summary>
|
||
/// 删除所有与当前 PNG 不匹配的 Sprite(textureGuid 不同)
|
||
/// </summary>
|
||
private void RemoveMismatchedSprites()
|
||
{
|
||
if (targetTexture == null || assetList.Count == 0)
|
||
return;
|
||
|
||
string pngGuid = GetAssetGuid(AssetDatabase.GetAssetPath(targetTexture) + ".meta");
|
||
if (string.IsNullOrEmpty(pngGuid))
|
||
return;
|
||
|
||
int before = assetList.Count;
|
||
assetList.RemoveAll(a => a.textureGuid != pngGuid);
|
||
int removed = before - assetList.Count;
|
||
|
||
if (removed > 0)
|
||
{
|
||
Debug.Log($"[SpriteMetaRestorer] 已删除 {removed} 个与目标 PNG 不匹配的 Sprite");
|
||
Repaint();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将解析到的 sprite 列表写入 .png.meta 的 spriteSheet.sprites 块
|
||
/// </summary>
|
||
private void WriteSpritesToMeta()
|
||
{
|
||
if (targetTexture == null || assetList.Count == 0)
|
||
return;
|
||
|
||
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
|
||
string metaPath = pngPath + ".meta";
|
||
|
||
if (!File.Exists(metaPath))
|
||
{
|
||
EditorUtility.DisplayDialog("错误", $"找不到 meta 文件:\n{metaPath}", "确定");
|
||
return;
|
||
}
|
||
|
||
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
|
||
StringBuilder sb = new StringBuilder();
|
||
|
||
string pngGuid = GetAssetGuid(metaPath);
|
||
|
||
// 构建 sprites YAML 文本
|
||
StringBuilder spritesYaml = new StringBuilder();
|
||
List<string> nameFileIdEntries = new List<string>();
|
||
|
||
foreach (var info in assetList)
|
||
{
|
||
if (!info.valid)
|
||
continue;
|
||
|
||
// ★★★ 新增:不匹配 PNG 时的处理逻辑 ★★★
|
||
bool matched = info.textureGuid == pngGuid;
|
||
if (!matched && !allowMismatchedSprites)
|
||
{
|
||
sb.AppendLine($" [跳过] {info.name}(textureGuid 不匹配当前 PNG)");
|
||
continue;
|
||
}
|
||
|
||
// ★★★ 原有写入逻辑保持不变 ★★★
|
||
spritesYaml.AppendLine(" - serializedVersion: 2");
|
||
spritesYaml.AppendLine($" name: {info.name}");
|
||
spritesYaml.AppendLine(" rect:");
|
||
spritesYaml.AppendLine(" serializedVersion: 2");
|
||
spritesYaml.AppendLine($" x: {FormatFloat(info.rect.x)}");
|
||
spritesYaml.AppendLine($" y: {FormatFloat(info.rect.y)}");
|
||
spritesYaml.AppendLine($" width: {FormatFloat(info.rect.width)}");
|
||
spritesYaml.AppendLine($" height: {FormatFloat(info.rect.height)}");
|
||
spritesYaml.AppendLine(" alignment: 0");
|
||
spritesYaml.AppendLine($" pivot: {{x: {FormatFloat(info.pivot.x)}, y: {FormatFloat(info.pivot.y)}}}");
|
||
spritesYaml.AppendLine($" border: {{x: {FormatFloat(info.border.x)}, y: {FormatFloat(info.border.y)}, z: {FormatFloat(info.border.z)}, w: {FormatFloat(info.border.w)}}}");
|
||
spritesYaml.AppendLine(" outline: []");
|
||
spritesYaml.AppendLine(" physicsShape: []");
|
||
spritesYaml.AppendLine(" tessellationDetail: -1");
|
||
spritesYaml.AppendLine(" bones: []");
|
||
spritesYaml.AppendLine($" spriteID: {info.spriteID}");
|
||
spritesYaml.AppendLine(" internalID: 0");
|
||
spritesYaml.AppendLine(" vertices: []");
|
||
spritesYaml.AppendLine(" indices: ");
|
||
spritesYaml.AppendLine(" edges: []");
|
||
spritesYaml.AppendLine(" weights: []");
|
||
|
||
nameFileIdEntries.Add($" {info.name}: 0");
|
||
|
||
sb.AppendLine($" [+] {info.name} rect=({info.rect.x:F0},{info.rect.y:F0},{info.rect.width:F0}x{info.rect.height:F0})");
|
||
}
|
||
|
||
string spritesBlock = spritesYaml.ToString().TrimEnd('\n', '\r');
|
||
string spritesContent = " sprites:\n" + spritesBlock + "\n";
|
||
|
||
// 替换 meta 中的 sprites 块(保持你原来的逻辑)
|
||
string result;
|
||
if (metaText.Contains("sprites: []"))
|
||
{
|
||
result = ReplaceFirst(metaText, " sprites: []", spritesContent);
|
||
}
|
||
else
|
||
{
|
||
int idx = metaText.IndexOf(" sprites:");
|
||
if (idx >= 0)
|
||
{
|
||
int endIdx = FindBlockEnd(metaText, idx);
|
||
result = metaText.Substring(0, idx) + spritesContent + metaText.Substring(endIdx);
|
||
}
|
||
else
|
||
{
|
||
result = metaText;
|
||
}
|
||
}
|
||
|
||
// 更新 nameFileIdTable(保持你原来的逻辑)
|
||
if (nameFileIdEntries.Count > 0)
|
||
{
|
||
string tableBlock = " nameFileIdTable:\n" + string.Join("\n", nameFileIdEntries) + "\n";
|
||
int tblIdx = result.IndexOf(" nameFileIdTable:");
|
||
if (tblIdx >= 0)
|
||
{
|
||
int tblEndIdx = FindBlockEnd(result, tblIdx);
|
||
result = result.Substring(0, tblIdx) + tableBlock + result.Substring(tblEndIdx);
|
||
}
|
||
}
|
||
|
||
File.WriteAllText(metaPath, result, Encoding.UTF8);
|
||
AssetDatabase.ImportAsset(pngPath);
|
||
|
||
int validCount = assetList.Count(a => a.valid);
|
||
sb.AppendLine($"\n[完成] 已写入 {validCount} 个 Sprite 到:");
|
||
sb.AppendLine($" {metaPath}");
|
||
sb.AppendLine("\n→ Unity 已自动 Reimport,请在 Inspector 中确认。");
|
||
|
||
logText = sb.ToString();
|
||
showLog = true;
|
||
}
|
||
|
||
|
||
/// <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;
|
||
// 跳过 " sprites:" 这一行
|
||
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;
|
||
}
|
||
|
||
// 以 " "(6空格)开头 → 子属性,继续
|
||
if (line.StartsWith(" "))
|
||
{
|
||
i = nextNewline + 1;
|
||
continue;
|
||
}
|
||
|
||
// 以 " "(4空格)开头但不是 "- " → 下一个同级字段,块结束
|
||
if (line.StartsWith(" ") && !line.StartsWith(" - ") && !line.StartsWith(" "))
|
||
{
|
||
return i;
|
||
}
|
||
|
||
// 其他情况(缩进更少),块结束
|
||
return i;
|
||
}
|
||
|
||
return text.Length;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 格式化浮点数:整数值不带小数点
|
||
/// </summary>
|
||
private string FormatFloat(float v)
|
||
{
|
||
if (v == Mathf.RoundToInt(v))
|
||
return Mathf.RoundToInt(v).ToString();
|
||
return v.ToString("G7", System.Globalization.CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 工具方法
|
||
|
||
/// <summary>
|
||
/// 从 meta 文本提取 guid
|
||
/// </summary>
|
||
private string GetAssetGuid(string metaPath)
|
||
{
|
||
if (!File.Exists(metaPath)) return "";
|
||
string text = File.ReadAllText(metaPath, Encoding.UTF8);
|
||
return GetGuidFromText(text);
|
||
}
|
||
|
||
private string GetGuidFromText(string text)
|
||
{
|
||
int idx = text.IndexOf("guid:");
|
||
if (idx < 0) return "";
|
||
int start = idx + 5;
|
||
int end = start;
|
||
while (end < text.Length && text[end] != '\n' && text[end] != '\r')
|
||
end++;
|
||
return text.Substring(start, end - start).Trim();
|
||
}
|
||
|
||
#endregion
|
||
}
|