#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.U2D; using UnityEngine; using UnityEngine.U2D; public static class EditorSpriteSaveInfo { private static readonly HashSet DirtyAtlasNames = new HashSet(StringComparer.Ordinal); private static readonly Dictionary> AtlasMap = new Dictionary>(StringComparer.Ordinal); private static readonly List ProcessBuffer = new List(); private static readonly List PendingV2ImporterPaths = new List(); private static bool _initialized; private static bool _isProcessing; private static bool _isScanning; private static bool _processScheduled; private static AtlasConfiguration Config => AtlasConfiguration.Instance; static EditorSpriteSaveInfo() { Initialize(); } private static string NormalizedSourceRoot => NormalizePath(Config.sourceAtlasRoot).TrimEnd('/'); public static bool PrepareSpriteImporter(TextureImporter importer, string assetPath) { if (importer == null || !ShouldProcess(assetPath)) return false; var modified = false; if (importer.textureType != TextureImporterType.Sprite) { importer.textureType = TextureImporterType.Sprite; modified = true; } if (importer.spriteImportMode != SpriteImportMode.Single) { importer.spriteImportMode = SpriteImportMode.Single; modified = true; } var settings = new TextureImporterSettings(); importer.ReadTextureSettings(settings); if (settings.spriteGenerateFallbackPhysicsShape) { settings.spriteGenerateFallbackPhysicsShape = false; importer.SetTextureSettings(settings); modified = true; } return modified; } public static void ConvertToSprite(string assetPath) { if (!ShouldProcess(assetPath)) return; var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter; if (importer == null) return; if (PrepareSpriteImporter(importer, assetPath)) importer.SaveAndReimport(); } public static void OnImportSprite(string assetPath) { if (!ShouldProcess(assetPath)) return; EnsureInitialized(); var atlasName = GetAtlasName(assetPath); if (string.IsNullOrEmpty(atlasName)) return; AddSpritePath(atlasName, assetPath); MarkDirty(atlasName); MarkParentAtlasesDirty(assetPath); QueueProcessDirtyAtlases(); } public static void OnDeleteSprite(string assetPath) { if (!ShouldProcess(assetPath)) return; EnsureInitialized(); var atlasName = GetAtlasName(assetPath); if (string.IsNullOrEmpty(atlasName)) return; if (AtlasMap.TryGetValue(atlasName, out var spritePaths)) { spritePaths.Remove(assetPath); if (spritePaths.Count == 0) AtlasMap.Remove(atlasName); } MarkDirty(atlasName); MarkParentAtlasesDirty(assetPath); QueueProcessDirtyAtlases(); } [MenuItem("AlicizaX/Extension/图集工具/ForceGenerateAll")] public static void ForceGenerateAll() { RebuildCache(markDirty: true); ProcessDirtyAtlases(force: true); } public static void ClearCache() { DirtyAtlasNames.Clear(); AtlasMap.Clear(); ProcessBuffer.Clear(); PendingV2ImporterPaths.Clear(); _initialized = false; _processScheduled = false; } public static void MarkParentAtlasesDirty(string assetPath) { var currentPath = NormalizePath(Path.GetDirectoryName(assetPath)); var rootPath = NormalizedSourceRoot; while (!string.IsNullOrEmpty(currentPath) && currentPath.StartsWith(rootPath + "/", StringComparison.Ordinal)) { var atlasName = GetAtlasNameForDirectory(currentPath); if (!string.IsNullOrEmpty(atlasName)) MarkDirty(atlasName); currentPath = NormalizePath(Path.GetDirectoryName(currentPath)); } } public static bool ShouldProcess(string assetPath) { return IsImageFile(assetPath) && !IsExcluded(assetPath) && IsUnderSourceRoot(assetPath); } private static void Initialize() { if (_initialized) return; RebuildCache(markDirty: false); } private static void EnsureInitialized() { if (!_initialized) Initialize(); } private static void RebuildCache(bool markDirty) { DirtyAtlasNames.Clear(); AtlasMap.Clear(); ScanExistingSprites(); if (markDirty) { foreach (var atlasName in AtlasMap.Keys) DirtyAtlasNames.Add(atlasName); MarkExistingOutputAtlasesDirty(); } _initialized = true; } private static void ScanExistingSprites() { var sourceRoot = NormalizedSourceRoot; if (string.IsNullOrEmpty(sourceRoot)) return; _isScanning = true; try { var guids = AssetDatabase.FindAssets("t:Sprite", new[] { sourceRoot }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); if (!ShouldProcess(path)) continue; var atlasName = GetAtlasName(path); if (!string.IsNullOrEmpty(atlasName)) AddSpritePath(atlasName, path); } } finally { _isScanning = false; } } private static void MarkExistingOutputAtlasesDirty() { var outputDir = NormalizePath(Config.outputAtlasDir).TrimEnd('/'); if (string.IsNullOrEmpty(outputDir) || !AssetDatabase.IsValidFolder(outputDir)) return; var guids = AssetDatabase.FindAssets("t:SpriteAtlas", new[] { outputDir }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var atlasName = Path.GetFileNameWithoutExtension(path); if (!string.IsNullOrEmpty(atlasName)) DirtyAtlasNames.Add(atlasName); } } private static void QueueProcessDirtyAtlases() { if (_isScanning || _processScheduled || DirtyAtlasNames.Count == 0) return; _processScheduled = true; EditorApplication.delayCall += DelayedProcessDirtyAtlases; } private static void DelayedProcessDirtyAtlases() { _processScheduled = false; if (_isProcessing || DirtyAtlasNames.Count == 0) return; if (EditorApplication.isCompiling || EditorApplication.isUpdating) { QueueProcessDirtyAtlases(); return; } ProcessDirtyAtlases(); } private static void ProcessDirtyAtlases(bool force = false) { if (DirtyAtlasNames.Count == 0) return; EnsureOutputDirectory(); _isProcessing = true; try { while (DirtyAtlasNames.Count > 0) { ProcessBuffer.Clear(); ProcessBuffer.AddRange(DirtyAtlasNames); DirtyAtlasNames.Clear(); PendingV2ImporterPaths.Clear(); AssetDatabase.StartAssetEditing(); try { foreach (var atlasName in ProcessBuffer) { if (force || ShouldUpdateAtlas(atlasName)) GenerateAtlas(atlasName); } } finally { AssetDatabase.StopAssetEditing(); } ApplyPendingV2ImportSettings(); } AssetDatabase.SaveAssets(); } finally { ProcessBuffer.Clear(); PendingV2ImporterPaths.Clear(); _isProcessing = false; if (DirtyAtlasNames.Count > 0) QueueProcessDirtyAtlases(); } } private static void GenerateAtlas(string atlasName) { var outputPath = GetAtlasOutputPath(atlasName); var packables = LoadValidSprites(atlasName); if (packables.Count == 0) { DeleteAtlas(outputPath); DeleteAtlas(GetLegacyAtlasOutputPath(atlasName)); return; } DeleteAtlas(GetLegacyAtlasOutputPath(atlasName)); var spriteObjects = packables.ToArray(); if (Config.enableV2) { GenerateAtlasV2(outputPath, spriteObjects); } else { GenerateAtlasV1(outputPath, spriteObjects); } if (Config.enableLogging) Debug.Log($"Generated atlas: {atlasName} ({spriteObjects.Length} sprites)"); } private static void GenerateAtlasV2(string outputPath, UnityEngine.Object[] spriteObjects) { var atlas = AssetDatabase.LoadAssetAtPath(outputPath); var spriteAtlasAsset = atlas == null ? new SpriteAtlasAsset() : SpriteAtlasAsset.Load(outputPath); if (atlas != null) { var oldPackables = atlas.GetPackables(); if (oldPackables != null && oldPackables.Length > 0) spriteAtlasAsset.Remove(oldPackables); } #if !UNITY_2022_1_OR_NEWER ConfigureAtlasV2Settings(spriteAtlasAsset); #endif spriteAtlasAsset.Add(spriteObjects); SpriteAtlasAsset.Save(spriteAtlasAsset, outputPath); #if UNITY_2022_1_OR_NEWER PendingV2ImporterPaths.Add(outputPath); #endif } private static void GenerateAtlasV1(string outputPath, UnityEngine.Object[] spriteObjects) { var atlas = AssetDatabase.LoadAssetAtPath(outputPath); if (atlas == null) { atlas = new SpriteAtlas(); ConfigureAtlasSettings(atlas); atlas.Add(spriteObjects); atlas.SetIsVariant(false); AssetDatabase.CreateAsset(atlas, outputPath); return; } var oldPackables = atlas.GetPackables(); if (oldPackables != null && oldPackables.Length > 0) atlas.Remove(oldPackables); ConfigureAtlasSettings(atlas); atlas.Add(spriteObjects); atlas.SetIsVariant(false); EditorUtility.SetDirty(atlas); } private static List LoadValidSprites(string atlasName) { var sprites = new List(); if (!AtlasMap.TryGetValue(atlasName, out var spritePaths) || spritePaths.Count == 0) return sprites; foreach (var assetPath in spritePaths) { var sprite = AssetDatabase.LoadAssetAtPath(assetPath); if (sprite != null) { sprites.Add(sprite); continue; } var subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath); foreach (var subAsset in subAssets) { if (subAsset is Sprite subSprite) sprites.Add(subSprite); } } return sprites; } private static void ApplyPendingV2ImportSettings() { #if UNITY_2022_1_OR_NEWER foreach (var outputPath in PendingV2ImporterPaths) { var importer = AssetImporter.GetAtPath(outputPath) as SpriteAtlasImporter; if (importer == null) continue; if (ConfigureAtlasV2Settings(importer)) { AssetDatabase.WriteImportSettingsIfDirty(outputPath); importer.SaveAndReimport(); } else { AssetDatabase.ImportAsset(outputPath, ImportAssetOptions.ForceSynchronousImport); } } #endif } #if UNITY_2022_1_OR_NEWER private static bool ConfigureAtlasV2Settings(SpriteAtlasImporter atlasImporter) { var modified = false; modified |= SetPlatform(atlasImporter, "Android", Config.androidFormat); modified |= SetPlatform(atlasImporter, "iPhone", Config.iosFormat); modified |= SetPlatform(atlasImporter, "WebGL", Config.webglFormat); var packingSettings = atlasImporter.packingSettings; if (packingSettings.padding != Config.padding || packingSettings.enableRotation != Config.enableRotation || packingSettings.blockOffset != Config.blockOffset || packingSettings.enableTightPacking != Config.tightPacking || !packingSettings.enableAlphaDilation) { atlasImporter.packingSettings = new SpriteAtlasPackingSettings { padding = Config.padding, enableRotation = Config.enableRotation, blockOffset = Config.blockOffset, enableTightPacking = Config.tightPacking, enableAlphaDilation = true }; modified = true; } return modified; } private static bool SetPlatform(SpriteAtlasImporter atlasImporter, string platform, TextureImporterFormat format) { var settings = atlasImporter.GetPlatformSettings(platform); if (settings == null) return false; if (settings.overridden && settings.format == format && settings.compressionQuality == Config.compressionQuality) { return false; } settings.overridden = true; settings.format = format; settings.compressionQuality = Config.compressionQuality; atlasImporter.SetPlatformSettings(settings); return true; } #else private static void ConfigureAtlasV2Settings(SpriteAtlasAsset spriteAtlasAsset) { SetPlatform(spriteAtlasAsset, "Android", Config.androidFormat); SetPlatform(spriteAtlasAsset, "iPhone", Config.iosFormat); SetPlatform(spriteAtlasAsset, "WebGL", Config.webglFormat); spriteAtlasAsset.SetPackingSettings(new SpriteAtlasPackingSettings { padding = Config.padding, enableRotation = Config.enableRotation, blockOffset = Config.blockOffset, enableTightPacking = Config.tightPacking, enableAlphaDilation = true }); } private static void SetPlatform(SpriteAtlasAsset spriteAtlasAsset, string platform, TextureImporterFormat format) { var settings = spriteAtlasAsset.GetPlatformSettings(platform); if (settings == null) return; settings.overridden = true; settings.format = format; settings.compressionQuality = Config.compressionQuality; spriteAtlasAsset.SetPlatformSettings(settings); } #endif private static void ConfigureAtlasSettings(SpriteAtlas atlas) { SetPlatform(atlas, "Android", Config.androidFormat); SetPlatform(atlas, "iPhone", Config.iosFormat); SetPlatform(atlas, "WebGL", Config.webglFormat); atlas.SetPackingSettings(new SpriteAtlasPackingSettings { padding = Config.padding, enableRotation = Config.enableRotation, blockOffset = Config.blockOffset, enableTightPacking = Config.tightPacking }); } private static void SetPlatform(SpriteAtlas atlas, string platform, TextureImporterFormat format) { var settings = atlas.GetPlatformSettings(platform); settings.overridden = true; settings.format = format; settings.compressionQuality = Config.compressionQuality; atlas.SetPlatformSettings(settings); } private static string GetAtlasName(string assetPath) { var normalizedPath = NormalizePath(assetPath); var rootPath = NormalizedSourceRoot; if (!normalizedPath.StartsWith(rootPath + "/", StringComparison.Ordinal)) return null; var relativePath = normalizedPath.Substring(rootPath.Length + 1).Split('/'); if (relativePath.Length < 2) return null; var atlasNamePart = string.Join("_", relativePath, 0, relativePath.Length - 1); var rootFolderName = Path.GetFileName(rootPath); return $"{rootFolderName}_{atlasNamePart}"; } private static string GetAtlasNameForDirectory(string directoryPath) { var normalizedPath = NormalizePath(directoryPath); var rootPath = NormalizedSourceRoot; if (!normalizedPath.StartsWith(rootPath + "/", StringComparison.Ordinal)) return null; var relativePath = normalizedPath.Substring(rootPath.Length + 1).Split('/'); if (relativePath.Length == 0) return null; var atlasNamePart = string.Join("_", relativePath); var rootFolderName = Path.GetFileName(rootPath); return $"{rootFolderName}_{atlasNamePart}"; } private static void AddSpritePath(string atlasName, string assetPath) { if (!AtlasMap.TryGetValue(atlasName, out var spritePaths)) { spritePaths = new HashSet(StringComparer.Ordinal); AtlasMap[atlasName] = spritePaths; } spritePaths.Add(assetPath); } private static bool IsExcluded(string path) { var normalizedPath = NormalizePath(path); var excludeFolders = Config.excludeFolders; if (excludeFolders != null) { foreach (var folder in excludeFolders) { if (!string.IsNullOrEmpty(folder) && normalizedPath.StartsWith(NormalizePath(folder), StringComparison.Ordinal)) { return true; } } } var excludeKeywords = Config.excludeKeywords; if (excludeKeywords != null) { foreach (var keyword in excludeKeywords) { if (!string.IsNullOrEmpty(keyword) && normalizedPath.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } } return false; } private static bool IsImageFile(string path) { var extension = Path.GetExtension(path); if (string.IsNullOrEmpty(extension)) return false; switch (extension.ToLowerInvariant()) { case ".png": case ".jpg": case ".jpeg": return true; default: return false; } } private static bool IsUnderSourceRoot(string assetPath) { var normalizedPath = NormalizePath(assetPath); var rootPath = NormalizedSourceRoot; return !string.IsNullOrEmpty(rootPath) && normalizedPath.StartsWith(rootPath + "/", StringComparison.Ordinal); } private static void MarkDirty(string atlasName) { if (!string.IsNullOrEmpty(atlasName)) DirtyAtlasNames.Add(atlasName); } private static bool ShouldUpdateAtlas(string atlasName) { return !string.IsNullOrEmpty(atlasName); } private static void DeleteAtlas(string assetPath) { if (!string.IsNullOrEmpty(assetPath) && !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(assetPath))) AssetDatabase.DeleteAsset(assetPath); } private static void EnsureOutputDirectory() { var outputPath = NormalizePath(Config.outputAtlasDir).TrimEnd('/'); if (string.IsNullOrEmpty(outputPath) || AssetDatabase.IsValidFolder(outputPath)) return; var pathParts = outputPath.Split('/'); if (pathParts.Length == 0 || pathParts[0] != "Assets") return; var currentPath = pathParts[0]; for (var i = 1; i < pathParts.Length; i++) { var nextPath = $"{currentPath}/{pathParts[i]}"; if (!AssetDatabase.IsValidFolder(nextPath)) AssetDatabase.CreateFolder(currentPath, pathParts[i]); currentPath = nextPath; } } private static string GetAtlasOutputPath(string atlasName) { var extension = Config.enableV2 ? ".spriteatlasv2" : ".spriteatlas"; return $"{NormalizePath(Config.outputAtlasDir).TrimEnd('/')}/{atlasName}{extension}"; } private static string GetLegacyAtlasOutputPath(string atlasName) { var legacyExtension = Config.enableV2 ? ".spriteatlas" : ".spriteatlasv2"; return $"{NormalizePath(Config.outputAtlasDir).TrimEnd('/')}/{atlasName}{legacyExtension}"; } private static string NormalizePath(string path) { return string.IsNullOrEmpty(path) ? string.Empty : path.Replace("\\", "/"); } } #endif