Files
5.24BuYu/Assets/Framework/Tool/Sprite/Editor/SpriteMetaRestorer.cs
T
2026-05-22 19:30:02 +08:00

887 lines
29 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 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
}