/*
* @Author: Claude
* @Description: TMP 表情图集自动化生成编辑器,将碎图打包为 TMP_SpriteAsset
* @Date: 2026年04月03日
* @Modify:
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.TextCore;
namespace Logic.Editor
{
///
/// TMP 表情图集自动化生成器
/// 功能流程:检查碎图 → 合并大图 → 设置切分 → 生成 TMP_SpriteAsset
///
public class SpriteAssetEditorWindow : EditorWindow
{
// ============================================================
// 常量
// ============================================================
private static readonly Regex ValidNameRegex = new Regex(@"^[a-z0-9_]+$", RegexOptions.Compiled);
private static readonly int[] AtlasSizeOptions = { 1024, 2048, 4096 };
private static readonly string[] AtlasSizeLabels = { "1024", "2048", "4096" };
private const string DefaultSourceFolder = "Assets/Resources/TH1UI/Icon/Emoji";
private const string DefaultOutputFolder = "Assets/Resources/SpriteAsset";
// ============================================================
// 配置字段
// ============================================================
private DefaultAsset _sourceFolder;
private DefaultAsset _outputFolder;
private string _assetName = "EmojiSpriteAsset";
private int _atlasSizeIndex = 1; // 默认 2048
private int _padding = 2;
private bool _checkSize;
private int _expectedWidth = 128;
private int _expectedHeight = 128;
// ============================================================
// UI 状态
// ============================================================
private Vector2 _barPosition;
private Vector2 _logScrollPosition;
private string _logContent = "";
private GUIStyle _boxStyle;
private GUIStyle _logStyle;
// ============================================================
// 数据结构
// ============================================================
///
/// 通过验证的碎图信息
///
public class ValidSpriteInfo
{
public string FilePath; // 完整文件路径
public string AssetPath; // Assets/ 相对路径
public string Name; // 文件名(不含后缀)
public Texture2D Texture; // 加载后的纹理引用
}
// ============================================================
// 编辑器入口
// ============================================================
[MenuItem("Tools/表情图集生成器")]
private static void ShowWindow()
{
var window = CreateWindow();
window.titleContent = new GUIContent("表情图集生成器");
window.minSize = new Vector2(500, 600);
window.Show();
}
private void OnEnable()
{
// 首次打开时自动填入默认文件夹
if (_sourceFolder == null)
_sourceFolder = AssetDatabase.LoadAssetAtPath(DefaultSourceFolder);
if (_outputFolder == null)
_outputFolder = AssetDatabase.LoadAssetAtPath(DefaultOutputFolder);
}
// ============================================================
// GUI 绘制
// ============================================================
private void OnGUI()
{
// 初始化样式
if (_boxStyle == null)
{
_boxStyle = InspectorUtils.GetHelpBoxStyle();
InspectorUtils.AddBorder(_boxStyle, new Color(0.5f, 0.5f, 0.5f));
}
if (_logStyle == null)
{
_logStyle = new GUIStyle(EditorStyles.textArea)
{
richText = true,
wordWrap = true,
fontSize = 11
};
}
_barPosition = EditorGUILayout.BeginScrollView(_barPosition);
// ---- 标题 ----
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("TMP 表情图集生成器", EditorStyles.boldLabel);
InspectorUtils.DrawDivider(Color.gray, 1);
EditorGUILayout.Space(4);
// ---- 源文件夹 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("基本设置", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("源文件夹:");
_sourceFolder = (DefaultAsset)EditorGUILayout.ObjectField(
_sourceFolder, typeof(DefaultAsset), false, GUILayout.Width(300));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("输出文件夹:");
_outputFolder = (DefaultAsset)EditorGUILayout.ObjectField(
_outputFolder, typeof(DefaultAsset), false, GUILayout.Width(300));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("资产名称:");
_assetName = EditorGUILayout.TextField(_assetName, GUILayout.Width(300));
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ---- 图集设置 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("图集设置", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("最大尺寸:");
_atlasSizeIndex = EditorGUILayout.Popup(_atlasSizeIndex, AtlasSizeLabels, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich("图元间距:");
_padding = EditorGUILayout.IntField(_padding, GUILayout.Width(100));
_padding = Mathf.Clamp(_padding, 0, 16);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
// 尺寸检查
EditorGUILayout.BeginHorizontal();
_checkSize = InspectorUtils.InspectorToggleVertical("检查图片尺寸", _checkSize);
EditorGUILayout.EndHorizontal();
if (_checkSize)
{
EditorGUILayout.BeginHorizontal();
InspectorUtils.InspectorTextWidthRich(" 期望宽度:");
_expectedWidth = EditorGUILayout.IntField(_expectedWidth, GUILayout.Width(100));
InspectorUtils.InspectorTextWidthRich(" 期望高度:");
_expectedHeight = EditorGUILayout.IntField(_expectedHeight, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ---- 操作按钮 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if (InspectorUtils.InspectorButtonWithTextWidth("检查图片资源"))
{
OnClickValidate();
}
EditorGUILayout.Space(10);
if (InspectorUtils.InspectorButtonWithTextWidth("一键生成 SpriteAsset"))
{
OnClickGenerate();
}
EditorGUILayout.Space(10);
if (InspectorUtils.InspectorButtonWithTextWidth("清空日志"))
{
_logContent = "";
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
EditorGUILayout.EndVertical();
EditorGUILayout.Space(4);
// ---- 日志输出 ----
EditorGUILayout.BeginVertical(_boxStyle);
EditorGUILayout.LabelField("日志输出", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
_logScrollPosition = EditorGUILayout.BeginScrollView(
_logScrollPosition, GUILayout.Height(250));
EditorGUILayout.TextArea(_logContent, _logStyle, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
// ============================================================
// 按钮回调
// ============================================================
///
/// 仅检查图片资源
///
private void OnClickValidate()
{
_logContent = "";
AppendLog("===== 开始检查图片资源 =====");
string sourcePath = GetFolderPath(_sourceFolder, "源文件夹");
if (sourcePath == null) return;
var result = ValidateImages(sourcePath);
if (result != null)
{
AppendLog($"\n检查完成,共 {result.Count} 张有效图片");
}
}
///
/// 一键生成完整流程
///
private void OnClickGenerate()
{
_logContent = "";
AppendLog("===== 开始一键生成 SpriteAsset =====");
// 校验路径
string sourcePath = GetFolderPath(_sourceFolder, "源文件夹");
if (sourcePath == null) return;
string outputPath = GetFolderPath(_outputFolder, "输出文件夹");
if (outputPath == null) return;
if (string.IsNullOrWhiteSpace(_assetName))
{
AppendLog("错误:资产名称不能为空");
EditorUtility.DisplayDialog("错误", "资产名称不能为空。", "确定");
return;
}
// Step 1: 检查图片
AppendLog("\n--- Step 1: 检查图片资源 ---");
var validSprites = ValidateImages(sourcePath);
if (validSprites == null || validSprites.Count == 0)
{
AppendLog("没有有效图片,流程终止");
EditorUtility.DisplayDialog("错误", "没有找到有效的图片资源,流程终止。", "确定");
return;
}
// Step 2: 合并大图
AppendLog("\n--- Step 2: 合并大图 ---");
int atlasSize = AtlasSizeOptions[_atlasSizeIndex];
var packResult = PackAtlasTexture(validSprites, outputPath, atlasSize);
if (packResult == null)
{
AppendLog("合并大图失败,流程终止");
return;
}
// Step 3: 设置切分
AppendLog("\n--- Step 3: 设置大图切分属性 ---");
string atlasAssetPath = packResult.Value.atlasAssetPath;
var names = validSprites.Select(s => s.Name).ToArray();
bool sliceOk = SetupTextureImporter(
atlasAssetPath,
packResult.Value.rects,
names,
packResult.Value.atlasWidth,
packResult.Value.atlasHeight);
if (!sliceOk)
{
AppendLog("设置切分失败,流程终止");
return;
}
// Step 4: 生成 TMP_SpriteAsset
AppendLog("\n--- Step 4: 生成 TMP_SpriteAsset ---");
bool assetOk = GenerateTMPSpriteAsset(atlasAssetPath, outputPath);
if (!assetOk)
{
AppendLog("生成 SpriteAsset 失败");
return;
}
// Step 5: 自动设置 TMP Settings 的 Default Sprite Asset
AppendLog("\n--- Step 5: 设置 TMP Settings ---");
string generatedAssetPath = $"{outputPath}/{_assetName}.asset";
SetTMPDefaultSpriteAsset(generatedAssetPath);
AppendLog("\n===== 全部流程完成!=====");
EditorUtility.DisplayDialog("成功",
$"SpriteAsset 已生成并自动绑定!\n\n" +
$"图集:{atlasAssetPath}\n" +
$"资产:{generatedAssetPath}\n\n" +
$"已自动设置为 TMP Settings → Default Sprite Asset。\n" +
$"可直接在 TMP 文本中使用 标签。",
"确定");
}
// ============================================================
// Step 1: 检查与收集图片资源
// ============================================================
///
/// 验证源文件夹中的图片,返回通过验证的图片列表
///
public List ValidateImages(string sourceFolderPath)
{
if (!Directory.Exists(sourceFolderPath))
{
AppendLog($"错误:源文件夹不存在 - {sourceFolderPath}");
return null;
}
var allFiles = Directory.GetFiles(sourceFolderPath);
if (allFiles.Length == 0)
{
AppendLog("警告:源文件夹为空,没有找到任何文件");
return new List();
}
var validList = new List();
int errorCount = 0;
foreach (var filePath in allFiles)
{
string fileName = Path.GetFileName(filePath);
string nameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
string extension = Path.GetExtension(filePath).ToLower();
// 跳过 .meta 文件
if (extension == ".meta") continue;
// 检查后缀
if (extension != ".png")
{
AppendLog($" 跳过非 PNG 文件:{fileName}");
errorCount++;
continue;
}
// 检查文件名合法性
if (!IsValidSpriteName(nameWithoutExt))
{
AppendLog($" 文件名不合法:{fileName}(仅允许小写英文、数字、下划线)");
errorCount++;
continue;
}
// 检查重名
if (validList.Any(s => s.Name == nameWithoutExt))
{
AppendLog($" 文件名重复:{fileName}");
errorCount++;
continue;
}
// 转换为 Assets/ 相对路径
string assetPath = FilePathToAssetPath(filePath);
if (string.IsNullOrEmpty(assetPath))
{
AppendLog($" 无法转换为 Asset 路径:{filePath}");
errorCount++;
continue;
}
// 加载纹理并检查尺寸
var texture = AssetDatabase.LoadAssetAtPath(assetPath);
if (texture == null)
{
AppendLog($" 无法加载纹理:{assetPath}");
errorCount++;
continue;
}
if (_checkSize)
{
if (texture.width != _expectedWidth || texture.height != _expectedHeight)
{
AppendLog($" 尺寸不符:{fileName}({texture.width}x{texture.height},期望 {_expectedWidth}x{_expectedHeight})");
errorCount++;
continue;
}
}
validList.Add(new ValidSpriteInfo
{
FilePath = filePath,
AssetPath = assetPath,
Name = nameWithoutExt,
Texture = texture
});
AppendLog($" 通过:{fileName}({texture.width}x{texture.height})");
}
AppendLog($"\n检查结果:{validList.Count} 张有效,{errorCount} 张无效");
return validList;
}
///
/// 检查 Sprite 名称是否合法(仅允许小写英文、数字、下划线)
///
public static bool IsValidSpriteName(string name)
{
if (string.IsNullOrEmpty(name)) return false;
return ValidNameRegex.IsMatch(name);
}
// ============================================================
// Step 2: 自动合并大图
// ============================================================
///
/// Pack 结果
///
public struct PackResult
{
public string atlasAssetPath;
public Rect[] rects;
public int atlasWidth;
public int atlasHeight;
}
///
/// 将碎图合并为一张大图并保存
///
public PackResult? PackAtlasTexture(List sprites, string outputFolderPath, int maxAtlasSize)
{
try
{
// 确保所有碎图可读
AppendLog(" 设置碎图为可读状态...");
foreach (var sprite in sprites)
{
EnsureTextureReadable(sprite.AssetPath);
}
// 重新加载纹理(Reimport 后引用可能变化)
var textures = new Texture2D[sprites.Count];
for (int i = 0; i < sprites.Count; i++)
{
textures[i] = AssetDatabase.LoadAssetAtPath(sprites[i].AssetPath);
if (textures[i] == null)
{
AppendLog($" 重新加载纹理失败:{sprites[i].AssetPath}");
return null;
}
}
// 创建图集纹理并打包
AppendLog(" 开始 PackTextures...");
var atlas = new Texture2D(maxAtlasSize, maxAtlasSize, TextureFormat.RGBA32, false);
Rect[] rects = atlas.PackTextures(textures, _padding, maxAtlasSize);
if (rects == null || rects.Length != sprites.Count)
{
AppendLog(" PackTextures 失败:返回 Rect 数量不匹配");
return null;
}
int atlasWidth = atlas.width;
int atlasHeight = atlas.height;
AppendLog($" 图集尺寸:{atlasWidth} x {atlasHeight}");
// 导出 PNG —— 先拷贝到干净的 RGBA32 纹理,防止压缩格式导致 EncodeToPNG 返回 null
string atlasFileName = $"{_assetName}_Atlas.png";
string outputAssetPath = $"{outputFolderPath}/{atlasFileName}";
string outputFullPath = AssetPathToFilePath(outputAssetPath);
var readableAtlas = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);
readableAtlas.SetPixels(atlas.GetPixels());
readableAtlas.Apply();
byte[] pngData = readableAtlas.EncodeToPNG();
DestroyImmediate(readableAtlas);
DestroyImmediate(atlas);
if (pngData == null)
{
AppendLog(" EncodeToPNG 失败:无法编码图集纹理");
return null;
}
File.WriteAllBytes(outputFullPath, pngData);
AssetDatabase.Refresh();
AppendLog($" 图集已保存:{outputAssetPath}");
// 打印每张碎图在图集中的位置
for (int i = 0; i < sprites.Count; i++)
{
var r = rects[i];
int px = Mathf.RoundToInt(r.x * atlasWidth);
int py = Mathf.RoundToInt(r.y * atlasHeight);
int pw = Mathf.RoundToInt(r.width * atlasWidth);
int ph = Mathf.RoundToInt(r.height * atlasHeight);
AppendLog($" {sprites[i].Name} → ({px}, {py}, {pw}x{ph})");
}
return new PackResult
{
atlasAssetPath = outputAssetPath,
rects = rects,
atlasWidth = atlasWidth,
atlasHeight = atlasHeight
};
}
catch (Exception e)
{
AppendLog($" PackTextures 异常:{e.Message}");
Debug.LogException(e);
return null;
}
}
// ============================================================
// Step 3: 自动设置大图切分属性
// ============================================================
///
/// 设置大图的 TextureImporter,完成 Sprite Multiple 切分
///
public bool SetupTextureImporter(string atlasAssetPath, Rect[] normalizedRects, string[] names,
int atlasWidth, int atlasHeight)
{
try
{
var importer = AssetImporter.GetAtPath(atlasAssetPath) as TextureImporter;
if (importer == null)
{
AppendLog($" 无法获取 TextureImporter:{atlasAssetPath}");
return false;
}
// 设置贴图类型
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.isReadable = true;
importer.mipmapEnabled = false;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.maxTextureSize = Mathf.Max(atlasWidth, atlasHeight);
// 构建切片数据
var spriteSheet = new SpriteMetaData[names.Length];
for (int i = 0; i < names.Length; i++)
{
var nr = normalizedRects[i];
// 将归一化 Rect 转为像素坐标(左下角坐标系)
var pixelRect = new Rect(
Mathf.RoundToInt(nr.x * atlasWidth),
Mathf.RoundToInt(nr.y * atlasHeight),
Mathf.RoundToInt(nr.width * atlasWidth),
Mathf.RoundToInt(nr.height * atlasHeight)
);
spriteSheet[i] = new SpriteMetaData
{
name = names[i],
rect = pixelRect,
alignment = (int)SpriteAlignment.Center,
pivot = new Vector2(0.5f, 0.5f)
};
AppendLog($" 切片[{i}]:{names[i]} → rect({pixelRect.x}, {pixelRect.y}, {pixelRect.width}, {pixelRect.height})");
}
importer.spritesheet = spriteSheet;
importer.SaveAndReimport();
AppendLog(" TextureImporter 设置完成,已刷新资源");
return true;
}
catch (Exception e)
{
AppendLog($" 设置 TextureImporter 异常:{e.Message}");
Debug.LogException(e);
return false;
}
}
// ============================================================
// Step 4: 自动生成 TMP_SpriteAsset
// ============================================================
///
/// 基于切分好的图集生成 TMP_SpriteAsset
///
public bool GenerateTMPSpriteAsset(string atlasAssetPath, string outputFolderPath)
{
try
{
// 加载图集纹理
var atlasTexture = AssetDatabase.LoadAssetAtPath(atlasAssetPath);
if (atlasTexture == null)
{
AppendLog($" 无法加载图集纹理:{atlasAssetPath}");
return false;
}
// 加载所有切分后的 Sprite
var sprites = AssetDatabase.LoadAllAssetsAtPath(atlasAssetPath)
.OfType().OrderBy(s => s.name).ToList();
if (sprites.Count == 0)
{
AppendLog(" 未找到切分后的 Sprite,请确认 Step 3 是否正确执行");
return false;
}
AppendLog($" 找到 {sprites.Count} 个 Sprite 切片");
// ====================================================================
// 严格按照 TMP 官方 TMP_SpriteAssetMenu.CreateSpriteAsset 的顺序构建
// ====================================================================
// 1. 先创建资产并持久化到磁盘(官方要求 CreateAsset 在设置属性之前)
var spriteAsset = ScriptableObject.CreateInstance();
string assetPath = $"{outputFolderPath}/{_assetName}.asset";
AssetDatabase.CreateAsset(spriteAsset, assetPath);
// 2. 设置版本号和哈希(version setter 是 internal,需要反射)
const BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Instance;
typeof(TMP_SpriteAsset).GetField("m_Version", bf)?.SetValue(spriteAsset, "1.1.0");
spriteAsset.hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteAsset.name);
// 3. 设置图集纹理引用
spriteAsset.spriteSheet = atlasTexture;
// 4. 构建 GlyphTable 和 CharacterTable(使用原始像素值,不做任何缩放)
var spriteGlyphTable = new List();
var spriteCharacterTable = new List();
for (int i = 0; i < sprites.Count; i++)
{
var sprite = sprites[i];
var rect = sprite.rect;
// GlyphMetrics 使用原始像素值
// bearingX = 0(不向左偏移,避免覆盖前一字符)
// bearingY = height(图标底部对齐基线,整体在基线上方,与文字对齐)
var spriteGlyph = new TMP_SpriteGlyph();
spriteGlyph.index = (uint)i;
spriteGlyph.sprite = sprite;
spriteGlyph.metrics = new GlyphMetrics(
rect.width, // width: 原始像素宽度
rect.height, // height: 原始像素高度
0, // bearingX: 无水平偏移
rect.height * 0.9f, // bearingY: 底部略低于基线,视觉居中
rect.width // advance: 光标前进量 = 图标宽度
);
spriteGlyph.glyphRect = new GlyphRect(
(int)rect.x,
(int)rect.y,
(int)rect.width,
(int)rect.height
);
spriteGlyph.scale = 1.0f;
spriteGlyph.atlasIndex = 0;
spriteGlyphTable.Add(spriteGlyph);
// SpriteCharacter: unicode 统一用 0xFFFE(官方占位符),通过 name 查找
var spriteCharacter = new TMP_SpriteCharacter(0xFFFE, spriteGlyph);
spriteCharacter.name = sprite.name;
spriteCharacter.scale = 1.0f;
spriteCharacterTable.Add(spriteCharacter);
AppendLog($" [{i}] {sprite.name} - rect({rect.x}, {rect.y}, {rect.width}x{rect.height})");
}
// 5. 通过反射写入表数据(setter 是 internal)
typeof(TMP_SpriteAsset).GetField("m_SpriteGlyphTable", bf)?.SetValue(spriteAsset, spriteGlyphTable);
typeof(TMP_SpriteAsset).GetField("m_SpriteCharacterTable", bf)?.SetValue(spriteAsset, spriteCharacterTable);
// 6. 创建材质并作为子资产嵌入(必须在 CreateAsset 之后)
Shader spriteShader = Shader.Find("TextMeshPro/Sprite");
if (spriteShader == null)
{
AppendLog(" 错误:找不到 TextMeshPro/Sprite shader");
return false;
}
var material = new Material(spriteShader);
material.SetTexture(ShaderUtilities.ID_MainTex, spriteAsset.spriteSheet);
spriteAsset.material = material;
material.hideFlags = HideFlags.HideInHierarchy;
AssetDatabase.AddObjectToAsset(material, spriteAsset);
// 7. 更新查找表(构建 name → character 哈希字典)
spriteAsset.UpdateLookupTables();
// 8. 标脏并保存
EditorUtility.SetDirty(spriteAsset);
AssetDatabase.SaveAssets();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(spriteAsset));
AppendLog($" TMP_SpriteAsset 已保存:{assetPath}");
AppendLog($" 共 {spriteCharacterTable.Count} 个 Sprite 字符映射");
return true;
}
catch (Exception e)
{
AppendLog($" 生成 TMP_SpriteAsset 异常:{e.Message}");
Debug.LogException(e);
return false;
}
}
// ============================================================
// Step 5: 自动设置 TMP Settings
// ============================================================
///
/// 将生成的 SpriteAsset 自动设置为 TMP Settings 的 Default Sprite Asset
///
private void SetTMPDefaultSpriteAsset(string spriteAssetPath)
{
try
{
var spriteAsset = AssetDatabase.LoadAssetAtPath(spriteAssetPath);
if (spriteAsset == null)
{
AppendLog($" 无法加载 SpriteAsset:{spriteAssetPath}");
return;
}
var tmpSettings = TMP_Settings.instance;
if (tmpSettings == null)
{
AppendLog(" 未找到 TMP Settings 实例,请先通过 Window → TextMeshPro → Settings 创建");
return;
}
// TMP_Settings.defaultSpriteAsset 的 setter 是 internal,通过 SerializedObject 修改
string settingsPath = AssetDatabase.GetAssetPath(tmpSettings);
var so = new SerializedObject(tmpSettings);
so.Update();
var prop = so.FindProperty("m_defaultSpriteAsset");
if (prop != null)
{
prop.objectReferenceValue = spriteAsset;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(tmpSettings);
AssetDatabase.SaveAssets();
AppendLog($" 已自动设置 TMP Settings → Default Sprite Asset = {spriteAsset.name}");
AppendLog($" TMP Settings 路径:{settingsPath}");
}
else
{
AppendLog(" 警告:未找到 m_defaultSpriteAsset 属性,请手动在 TMP Settings 中绑定");
}
}
catch (Exception e)
{
AppendLog($" 自动设置 TMP Settings 失败:{e.Message},请手动绑定");
Debug.LogException(e);
}
}
// ============================================================
// 工具方法
// ============================================================
///
/// 确保纹理为可读状态
///
private void EnsureTextureReadable(string assetPath)
{
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer == null) return;
bool needsReimport = false;
if (!importer.isReadable)
{
importer.isReadable = true;
needsReimport = true;
}
// 必须为未压缩格式,否则 PackTextures 产出压缩图集,EncodeToPNG 会返回 null
if (importer.textureCompression != TextureImporterCompression.Uncompressed)
{
importer.textureCompression = TextureImporterCompression.Uncompressed;
needsReimport = true;
}
if (needsReimport)
{
importer.SaveAndReimport();
AppendLog($" 已设置可读+未压缩:{Path.GetFileName(assetPath)}");
}
}
///
/// 获取文件夹的 Assets/ 相对路径
///
private string GetFolderPath(DefaultAsset folder, string label)
{
if (folder == null)
{
AppendLog($"错误:请指定{label}");
EditorUtility.DisplayDialog("错误", $"请先指定{label}。", "确定");
return null;
}
string path = AssetDatabase.GetAssetPath(folder);
if (string.IsNullOrEmpty(path) || !AssetDatabase.IsValidFolder(path))
{
AppendLog($"错误:{label}路径无效 - {path}");
EditorUtility.DisplayDialog("错误", $"{label}路径无效。", "确定");
return null;
}
return path;
}
///
/// 将文件系统绝对路径转换为 Assets/ 相对路径
///
private static string FilePathToAssetPath(string filePath)
{
string normalized = filePath.Replace('\\', '/');
string dataPath = Application.dataPath.Replace('\\', '/');
if (normalized.StartsWith(dataPath))
{
return "Assets" + normalized.Substring(dataPath.Length);
}
// 尝试查找 "Assets/" 子串
int idx = normalized.IndexOf("Assets/", StringComparison.OrdinalIgnoreCase);
if (idx >= 0)
{
return normalized.Substring(idx);
}
return null;
}
///
/// 将 Assets/ 相对路径转换为文件系统绝对路径
///
private static string AssetPathToFilePath(string assetPath)
{
string dataPath = Application.dataPath.Replace('\\', '/');
// Application.dataPath 以 "Assets" 结尾
string projectRoot = dataPath.Substring(0, dataPath.Length - "Assets".Length);
return projectRoot + assetPath;
}
///
/// 追加日志到输出区
///
public void AppendLog(string message)
{
_logContent += message + "\n";
Debug.Log($"[SpriteAsset] {message}");
Repaint();
}
}
}