/* * @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(); } } }