com.wallstop-studios.unity-helpers 2.0.0-rc73.18 → 2.0.0-rc73.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/Editor/{Sprites/AnimationCopier.cs → AnimationCopier.cs} +209 -84
  2. package/Editor/{Sprites/AnimationCreator.cs → AnimationCreator.cs} +100 -29
  3. package/Editor/AnimationEventEditor.cs +23 -10
  4. package/Editor/CustomEditors/MatchColliderToSpriteEditor.cs +1 -1
  5. package/Editor/FitTextureSizeWindow.cs +53 -14
  6. package/Editor/PrefabChecker.cs +18 -11
  7. package/Editor/SpriteAtlasGenerator.cs +914 -0
  8. package/Editor/SpriteAtlasGenerator.cs.meta +3 -0
  9. package/Editor/{Sprites/SpriteCropper.cs → SpriteCropper.cs} +143 -172
  10. package/Editor/{Sprites/SpriteSettingsApplier.cs → SpriteSettingsApplier.cs} +77 -12
  11. package/Editor/{Sprites/TextureResizerWizard.cs → TextureResizerWizard.cs} +1 -1
  12. package/Editor/{Sprites/TextureSettingsApplier.cs → TextureSettingsApplier.cs} +1 -1
  13. package/Editor/Utils/DxReadOnlyPropertyDrawer.cs +1 -1
  14. package/Editor/Utils/GUIHorizontalScope.cs +20 -0
  15. package/Editor/Utils/GUIHorizontalScope.cs.meta +3 -0
  16. package/Runtime/Core/DataStructure/Circle.cs +1 -1
  17. package/Runtime/Core/DataStructure/QuadTree.cs +4 -4
  18. package/Runtime/Core/Extension/ColorExtensions.cs +5 -5
  19. package/Runtime/Core/Extension/IEnumerableExtensions.cs +1 -1
  20. package/Runtime/Core/Extension/UnityExtensions.cs +14 -14
  21. package/Runtime/Core/Helper/DirectoryHelper.cs +0 -64
  22. package/Runtime/Core/Helper/Helpers.cs +9 -9
  23. package/Runtime/Core/Helper/Logging/UnityLogTagFormatter.cs +8 -31
  24. package/Runtime/Core/Helper/Partials/ObjectHelpers.cs +4 -5
  25. package/Runtime/Core/Helper/PathHelper.cs +1 -2
  26. package/Runtime/Core/Random/DotNetRandom.cs +1 -1
  27. package/Runtime/Core/Random/SplitMix64.cs +1 -1
  28. package/Runtime/Core/Random/SquirrelRandom.cs +7 -7
  29. package/Runtime/Core/Random/ThreadLocalRandom.cs +1 -1
  30. package/Runtime/Core/Random/WyRandom.cs +1 -1
  31. package/Runtime/Tags/AttributeEffect.cs +0 -1
  32. package/Runtime/Tags/EffectHandler.cs +1 -1
  33. package/Runtime/UI/LayeredImage.cs +161 -309
  34. package/Runtime/Utils/AnimatorEnumStateMachine.cs +1 -1
  35. package/Runtime/Utils/SetTextureImportData.cs +1 -1
  36. package/Runtime/Utils/TextureScale.cs +4 -4
  37. package/Styles/Elements/{Progress/CircularProgressBar.cs → CircularProgressBar.cs} +55 -56
  38. package/Styles/Elements/{Progress/RegularProgressBar.cs → RegularProgressBar.cs} +13 -24
  39. package/Styles/UXML/CircularProgressBar.uxml +11 -0
  40. package/Styles/UXML/CircularProgressBar.uxml.meta +10 -0
  41. package/Styles/UXML/RegularProgressBar.uxml +22 -0
  42. package/Styles/UXML/RegularProgressBar.uxml.meta +10 -0
  43. package/Styles/UXML.meta +3 -0
  44. package/package.json +1 -17
  45. package/Editor/CustomEditors/PersistentDirectoryGUI.cs +0 -796
  46. package/Editor/CustomEditors/PersistentDirectoryGUI.cs.meta +0 -3
  47. package/Editor/CustomEditors/SourceFolderEntryDrawer.cs +0 -275
  48. package/Editor/CustomEditors/SourceFolderEntryDrawer.cs.meta +0 -3
  49. package/Editor/PersistentDirectorySettings.cs +0 -248
  50. package/Editor/PersistentDirectorySettings.cs.meta +0 -3
  51. package/Editor/Sprites/ScriptableSpriteAtlas.cs +0 -95
  52. package/Editor/Sprites/ScriptableSpriteAtlas.cs.meta +0 -3
  53. package/Editor/Sprites/ScriptableSpriteAtlasEditor.cs +0 -960
  54. package/Editor/Sprites/ScriptableSpriteAtlasEditor.cs.meta +0 -3
  55. package/Editor/Sprites.meta +0 -3
  56. package/Styles/Elements/Progress/ArcedProgressBar.cs +0 -345
  57. package/Styles/Elements/Progress/ArcedProgressBar.cs.meta +0 -3
  58. package/Styles/Elements/Progress/GlitchProgressBar.cs +0 -416
  59. package/Styles/Elements/Progress/GlitchProgressBar.cs.meta +0 -3
  60. package/Styles/Elements/Progress/LiquidProgressBar.cs +0 -632
  61. package/Styles/Elements/Progress/LiquidProgressBar.cs.meta +0 -3
  62. package/Styles/Elements/Progress/MarchingAntsProgressBar.cs +0 -722
  63. package/Styles/Elements/Progress/MarchingAntsProgressBar.cs.meta +0 -3
  64. package/Styles/Elements/Progress/WigglyProgressBar.cs +0 -837
  65. package/Styles/Elements/Progress/WigglyProgressBar.cs.meta +0 -3
  66. package/Styles/Elements/Progress.meta +0 -3
  67. package/Styles/USS/ArcedProgressBar.uss +0 -19
  68. package/Styles/USS/ArcedProgressBar.uss.meta +0 -3
  69. package/Styles/USS/WigglyProgressBar.uss +0 -17
  70. package/Styles/USS/WigglyProgressBar.uss.meta +0 -3
  71. /package/Editor/{Sprites/AnimationCopier.cs.meta → AnimationCopier.cs.meta} +0 -0
  72. /package/Editor/{Sprites/AnimationCreator.cs.meta → AnimationCreator.cs.meta} +0 -0
  73. /package/Editor/{Sprites/SpriteCropper.cs.meta → SpriteCropper.cs.meta} +0 -0
  74. /package/Editor/{Sprites/SpriteSettingsApplier.cs.meta → SpriteSettingsApplier.cs.meta} +0 -0
  75. /package/Editor/{Sprites/TextureResizerWizard.cs.meta → TextureResizerWizard.cs.meta} +0 -0
  76. /package/Editor/{Sprites/TextureSettingsApplier.cs.meta → TextureSettingsApplier.cs.meta} +0 -0
  77. /package/Styles/Elements/{Progress/CircularProgressBar.cs.meta → CircularProgressBar.cs.meta} +0 -0
  78. /package/Styles/Elements/{Progress/RegularProgressBar.cs.meta → RegularProgressBar.cs.meta} +0 -0
@@ -0,0 +1,914 @@
1
+ namespace WallstopStudios.UnityHelpers.Editor
2
+ {
3
+ #if UNITY_EDITOR
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.IO;
7
+ using System.Linq;
8
+ using System.Text.RegularExpressions;
9
+ using Core.Extension;
10
+ using Core.Helper;
11
+ using UnityEditor;
12
+ using UnityEditor.U2D;
13
+ using UnityEngine;
14
+ using UnityEngine.U2D;
15
+ using Utils;
16
+ using Object = UnityEngine.Object;
17
+
18
+ public sealed class SpriteAtlasGenerator : EditorWindow
19
+ {
20
+ private const string Name = "Sprite Atlas Generator";
21
+ private const string DefaultPlatformName = "DefaultTexturePlatform";
22
+ private const int MaxAtlasDimension = 8192;
23
+ private const int MaxTextureSize = 16384;
24
+ private const long AtlasAreaBudget = (long)MaxAtlasDimension * MaxAtlasDimension;
25
+ private const int MaxAtlasNameLength = 100;
26
+
27
+ private sealed class AtlasCandidate
28
+ {
29
+ public string OriginalGroupKey { get; }
30
+ public List<Sprite> Sprites { get; }
31
+ public long TotalArea { get; }
32
+ public string CandidateName { get; }
33
+
34
+ public AtlasCandidate(
35
+ string originalGroupKey,
36
+ List<Sprite> sprites,
37
+ string candidateName
38
+ )
39
+ {
40
+ OriginalGroupKey = originalGroupKey;
41
+ Sprites = new List<Sprite>(sprites);
42
+ CandidateName = candidateName;
43
+ TotalArea = 0;
44
+ if (Sprites != null)
45
+ {
46
+ foreach (Sprite sprite in Sprites)
47
+ {
48
+ if (sprite != null && sprite.rect is { width: > 0, height: > 0 })
49
+ {
50
+ TotalArea += (long)(sprite.rect.width * sprite.rect.height);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ private sealed class MergeableAtlas
58
+ {
59
+ public HashSet<string> OriginalGroupKeys { get; }
60
+ public List<Sprite> Sprites { get; }
61
+ public long TotalArea { get; }
62
+ public string RepresentativeInitialName { get; }
63
+
64
+ public MergeableAtlas(
65
+ string originalGroupKey,
66
+ List<Sprite> initialSprites,
67
+ string initialCandidateName,
68
+ long totalArea
69
+ )
70
+ {
71
+ OriginalGroupKeys = new HashSet<string>(StringComparer.Ordinal)
72
+ {
73
+ originalGroupKey,
74
+ };
75
+ Sprites = initialSprites;
76
+ TotalArea = totalArea;
77
+ RepresentativeInitialName = initialCandidateName;
78
+ }
79
+
80
+ private MergeableAtlas(
81
+ HashSet<string> combinedKeys,
82
+ List<Sprite> combinedSprites,
83
+ long combinedArea,
84
+ string representativeName
85
+ )
86
+ {
87
+ OriginalGroupKeys = combinedKeys;
88
+ Sprites = combinedSprites;
89
+ TotalArea = combinedArea;
90
+ RepresentativeInitialName = representativeName;
91
+ }
92
+
93
+ public static MergeableAtlas Merge(MergeableAtlas atlas1, MergeableAtlas atlas2)
94
+ {
95
+ HashSet<string> newKeys = new(atlas1.OriginalGroupKeys, StringComparer.Ordinal);
96
+ newKeys.UnionWith(atlas2.OriginalGroupKeys);
97
+
98
+ List<Sprite> newSprites = new(atlas1.Sprites.Count + atlas2.Sprites.Count);
99
+ newSprites.AddRange(atlas1.Sprites);
100
+ newSprites.AddRange(atlas2.Sprites);
101
+
102
+ long newArea = atlas1.TotalArea + atlas2.TotalArea;
103
+
104
+ return new MergeableAtlas(
105
+ newKeys,
106
+ newSprites,
107
+ newArea,
108
+ atlas1.RepresentativeInitialName
109
+ );
110
+ }
111
+
112
+ public string GenerateFinalName()
113
+ {
114
+ if (!OriginalGroupKeys.Any())
115
+ {
116
+ return "EmptyOrInvalidAtlas";
117
+ }
118
+
119
+ List<string> sortedKeys = OriginalGroupKeys
120
+ .OrderBy(k => k, StringComparer.Ordinal)
121
+ .ToList();
122
+ string name = string.Join("_", sortedKeys);
123
+
124
+ if (name.Length > MaxAtlasNameLength)
125
+ {
126
+ const string suffix = "_etc";
127
+ int actualLength = MaxAtlasNameLength - suffix.Length;
128
+ if (actualLength <= 0)
129
+ {
130
+ return name.Substring(0, MaxAtlasNameLength);
131
+ }
132
+
133
+ name = name.Substring(0, actualLength) + suffix;
134
+ }
135
+ return name;
136
+ }
137
+ }
138
+
139
+ [SerializeField]
140
+ private Object[] _sourceFolders = Array.Empty<Object>();
141
+
142
+ [SerializeField]
143
+ private string _nameRegex = ".*";
144
+
145
+ [SerializeField]
146
+ private string _outputFolder = "Assets/Sprites/Atlases";
147
+
148
+ [SerializeField]
149
+ private int _crunchCompression = -1;
150
+
151
+ [SerializeField]
152
+ private TextureImporterCompression _compressionLevel =
153
+ TextureImporterCompression.Compressed;
154
+
155
+ [SerializeField]
156
+ private bool _optimizeGroupings = true;
157
+
158
+ private int _matchCount;
159
+ private int _totalCount;
160
+ private GUIStyle _impactButtonStyle;
161
+
162
+ [MenuItem("Tools/Wallstop Studios/Unity Helpers/" + Name)]
163
+ public static void ShowWindow() => GetWindow<SpriteAtlasGenerator>("Atlas Generator");
164
+
165
+ private void OnEnable()
166
+ {
167
+ if (_sourceFolders is { Length: > 0 })
168
+ {
169
+ return;
170
+ }
171
+
172
+ Object defaultFolder = AssetDatabase.LoadAssetAtPath<Object>("Assets/Sprites");
173
+ if (defaultFolder != null)
174
+ {
175
+ _sourceFolders = new[] { defaultFolder };
176
+ }
177
+ }
178
+
179
+ private void OnGUI()
180
+ {
181
+ _impactButtonStyle ??= new GUIStyle(GUI.skin.button)
182
+ {
183
+ normal = { textColor = Color.yellow },
184
+ fontStyle = FontStyle.Bold,
185
+ };
186
+ GUILayout.Label("Source Folders", EditorStyles.boldLabel);
187
+ SerializedObject so = new(this);
188
+ so.Update();
189
+ EditorGUILayout.PropertyField(so.FindProperty(nameof(_sourceFolders)), true);
190
+ so.ApplyModifiedProperties();
191
+ GUILayout.Space(8);
192
+ using (new GUIHorizontalScope())
193
+ {
194
+ EditorGUILayout.LabelField("Sprite Name Regex");
195
+ GUILayout.FlexibleSpace();
196
+ _nameRegex = EditorGUILayout.TextField(_nameRegex);
197
+ }
198
+
199
+ GUILayout.Space(4);
200
+ using (new GUIHorizontalScope())
201
+ {
202
+ if (GUILayout.Button("Calculate Matches"))
203
+ {
204
+ UpdateMatchCounts();
205
+ }
206
+ }
207
+
208
+ using (new GUIHorizontalScope())
209
+ {
210
+ EditorGUILayout.LabelField($"Matches: {_matchCount}");
211
+ GUILayout.FlexibleSpace();
212
+ EditorGUILayout.LabelField($"Non-matches: {_totalCount - _matchCount}");
213
+ }
214
+
215
+ GUILayout.Space(4);
216
+ using (new GUIHorizontalScope())
217
+ {
218
+ EditorGUILayout.LabelField("Crunch Compression");
219
+ GUILayout.FlexibleSpace();
220
+ _crunchCompression = EditorGUILayout.IntField(_crunchCompression);
221
+ }
222
+
223
+ GUILayout.Space(4);
224
+ using (new GUIHorizontalScope())
225
+ {
226
+ EditorGUILayout.LabelField("Compression Level", GUILayout.Width(150));
227
+ _compressionLevel = (TextureImporterCompression)
228
+ EditorGUILayout.EnumPopup(_compressionLevel);
229
+ }
230
+
231
+ GUILayout.Space(4);
232
+ using (new GUIHorizontalScope())
233
+ {
234
+ EditorGUILayout.LabelField("Optimize Groupings");
235
+ _optimizeGroupings = EditorGUILayout.Toggle(_optimizeGroupings);
236
+ }
237
+
238
+ GUILayout.Space(12);
239
+ using (new GUIHorizontalScope())
240
+ {
241
+ EditorGUILayout.LabelField("Atlas Output Folder");
242
+ EditorGUILayout.LabelField(_outputFolder, EditorStyles.textField);
243
+ }
244
+
245
+ if (GUILayout.Button("Select Output Folder"))
246
+ {
247
+ string absPath = EditorUtility.OpenFolderPanel(
248
+ "Select Atlas Output Folder",
249
+ Application.dataPath,
250
+ ""
251
+ );
252
+ if (!string.IsNullOrEmpty(absPath))
253
+ {
254
+ if (absPath.StartsWith(Application.dataPath, StringComparison.Ordinal))
255
+ {
256
+ _outputFolder = "Assets" + absPath.Substring(Application.dataPath.Length);
257
+ }
258
+ else
259
+ {
260
+ EditorUtility.DisplayDialog(
261
+ "Invalid Folder",
262
+ "Please select a folder inside the project's Assets directory.",
263
+ "OK"
264
+ );
265
+ }
266
+ }
267
+ }
268
+
269
+ GUILayout.Space(12);
270
+ using (new GUIHorizontalScope())
271
+ {
272
+ if (GUILayout.Button("Generate Atlases", _impactButtonStyle))
273
+ {
274
+ GenerateAtlases();
275
+ }
276
+ }
277
+ }
278
+
279
+ private void UpdateMatchCounts()
280
+ {
281
+ _totalCount = 0;
282
+ _matchCount = 0;
283
+ Regex regex;
284
+ try
285
+ {
286
+ regex = new Regex(_nameRegex);
287
+ }
288
+ catch (ArgumentException ex)
289
+ {
290
+ this.LogError($"Invalid Regex pattern: '{_nameRegex}'. Error: {ex.Message}");
291
+ Repaint();
292
+ return;
293
+ }
294
+
295
+ try
296
+ {
297
+ float total = _sourceFolders.Length;
298
+ foreach (Object obj in _sourceFolders)
299
+ {
300
+ if (obj == null)
301
+ {
302
+ continue;
303
+ }
304
+
305
+ string folderPath = AssetDatabase.GetAssetPath(obj);
306
+ if (
307
+ string.IsNullOrEmpty(folderPath) || !AssetDatabase.IsValidFolder(folderPath)
308
+ )
309
+ {
310
+ this.LogWarn($"Skipping invalid or null source folder entry.");
311
+ continue;
312
+ }
313
+
314
+ string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { folderPath });
315
+ for (int i = 0; i < guids.Length; i++)
316
+ {
317
+ EditorUtility.DisplayProgressBar(
318
+ Name,
319
+ "Calculating...",
320
+ i * 1f / guids.Length / total
321
+ );
322
+
323
+ string guid = guids[i];
324
+ string path = AssetDatabase.GUIDToAssetPath(guid);
325
+
326
+ Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path);
327
+ if (assets == null)
328
+ {
329
+ continue;
330
+ }
331
+
332
+ IEnumerable<Sprite> sprites = assets.OfType<Sprite>();
333
+ foreach (Sprite sp in sprites)
334
+ {
335
+ if (sp == null)
336
+ {
337
+ continue;
338
+ }
339
+
340
+ _totalCount++;
341
+ if (regex.IsMatch(sp.name))
342
+ {
343
+ _matchCount++;
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ finally
350
+ {
351
+ EditorUtility.ClearProgressBar();
352
+ }
353
+
354
+ Repaint();
355
+ }
356
+
357
+ private void GenerateAtlases()
358
+ {
359
+ List<SpriteAtlas> atlases = new();
360
+ int processed = 0;
361
+ AssetDatabase.StartAssetEditing();
362
+ try
363
+ {
364
+ EditorUtility.DisplayProgressBar(Name, "Initializing...", 0f);
365
+ if (string.IsNullOrWhiteSpace(_outputFolder))
366
+ {
367
+ this.LogError($"Invalid output folder.");
368
+ EditorUtility.ClearProgressBar();
369
+ return;
370
+ }
371
+
372
+ if (
373
+ _sourceFolders == null
374
+ || _sourceFolders.Length == 0
375
+ || _sourceFolders.All(f => f == null)
376
+ )
377
+ {
378
+ this.LogError($"No valid source folders specified.");
379
+ EditorUtility.ClearProgressBar();
380
+ return;
381
+ }
382
+
383
+ if (!AssetDatabase.IsValidFolder(_outputFolder))
384
+ {
385
+ try
386
+ {
387
+ string parent = Path.GetDirectoryName(_outputFolder);
388
+ string newFolderName = Path.GetFileName(_outputFolder);
389
+ if (string.IsNullOrEmpty(parent) || string.IsNullOrEmpty(newFolderName))
390
+ {
391
+ this.LogError($"Output folder path '{_outputFolder}' is invalid.");
392
+ EditorUtility.ClearProgressBar();
393
+ return;
394
+ }
395
+ AssetDatabase.CreateFolder(parent, newFolderName);
396
+ AssetDatabase.Refresh();
397
+ if (!AssetDatabase.IsValidFolder(_outputFolder))
398
+ {
399
+ this.LogError($"Failed to create output folder: '{_outputFolder}'");
400
+ EditorUtility.ClearProgressBar();
401
+ return;
402
+ }
403
+ }
404
+ catch (Exception ex)
405
+ {
406
+ this.LogError(
407
+ $"Error creating output folder '{_outputFolder}': {ex.Message}"
408
+ );
409
+ EditorUtility.ClearProgressBar();
410
+ return;
411
+ }
412
+ }
413
+
414
+ EditorUtility.DisplayProgressBar(Name, "Deleting old atlases...", 0.05f);
415
+ string[] existing = AssetDatabase
416
+ .FindAssets("t:SpriteAtlas", new[] { _outputFolder })
417
+ .Select(AssetDatabase.GUIDToAssetPath)
418
+ .Where(p => !string.IsNullOrEmpty(p))
419
+ .ToArray();
420
+
421
+ if (existing.Length > 0)
422
+ {
423
+ List<string> failedPaths = new();
424
+ AssetDatabase.DeleteAssets(existing, failedPaths);
425
+ if (failedPaths.Any())
426
+ {
427
+ this.LogWarn(
428
+ $"Failed to delete {failedPaths.Count} atlases:\n{string.Join("\n", failedPaths)}"
429
+ );
430
+ }
431
+ AssetDatabase.Refresh();
432
+ }
433
+
434
+ EditorUtility.DisplayProgressBar(
435
+ Name,
436
+ "Scanning sprites & initial grouping...",
437
+ 0.1f
438
+ );
439
+ Regex regex;
440
+ try
441
+ {
442
+ regex = new(_nameRegex);
443
+ }
444
+ catch (ArgumentException ex)
445
+ {
446
+ this.LogError(
447
+ $"Invalid Regex pattern for generation: '{_nameRegex}'. Error: {ex.Message}"
448
+ );
449
+ EditorUtility.ClearProgressBar();
450
+ return;
451
+ }
452
+ Dictionary<string, List<Sprite>> groups = new(StringComparer.Ordinal);
453
+
454
+ float sourceFolderIncrement =
455
+ _sourceFolders.Length > 0 ? 0.2f / _sourceFolders.Length : 0f;
456
+ float sourceFolderProgress = 0.1f;
457
+
458
+ foreach (Object sourceDirectory in _sourceFolders)
459
+ {
460
+ if (sourceDirectory == null)
461
+ {
462
+ continue;
463
+ }
464
+
465
+ string folderPath = AssetDatabase.GetAssetPath(sourceDirectory);
466
+ if (!AssetDatabase.IsValidFolder(folderPath))
467
+ {
468
+ this.LogWarn(
469
+ $"Skipping invalid source folder during generation: '{folderPath}'"
470
+ );
471
+ continue;
472
+ }
473
+
474
+ EditorUtility.DisplayProgressBar(
475
+ Name,
476
+ $"Scanning folder '{folderPath}'...",
477
+ sourceFolderProgress
478
+ );
479
+ sourceFolderProgress += sourceFolderIncrement;
480
+ foreach (
481
+ string assetGuid in AssetDatabase.FindAssets(
482
+ "t:Sprite",
483
+ new[] { folderPath }
484
+ )
485
+ )
486
+ {
487
+ string assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
488
+ if (string.IsNullOrEmpty(assetPath))
489
+ {
490
+ continue;
491
+ }
492
+
493
+ Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
494
+ if (allAssets == null)
495
+ {
496
+ continue;
497
+ }
498
+
499
+ foreach (Sprite sub in allAssets.OfType<Sprite>())
500
+ {
501
+ if (sub == null)
502
+ {
503
+ continue;
504
+ }
505
+
506
+ string assetName = sub.name;
507
+ if (!regex.IsMatch(assetName))
508
+ {
509
+ continue;
510
+ }
511
+
512
+ Match match = Regex.Match(assetName, @"^(.+?)(?:_\d+)?$");
513
+ string key = match.Success ? match.Groups[1].Value : assetName;
514
+ groups.GetOrAdd(key).Add(sub);
515
+ }
516
+ }
517
+ }
518
+
519
+ int totalChunks = 0;
520
+ Dictionary<string, List<List<Sprite>>> groupChunks = new();
521
+
522
+ EditorUtility.DisplayProgressBar(Name, "Calculating chunks...", 0.3f);
523
+
524
+ foreach (KeyValuePair<string, List<Sprite>> kv in groups)
525
+ {
526
+ List<Sprite> spritesInGroup = kv
527
+ .Value.Where(s => s != null && s.rect is { width: > 0, height: > 0 })
528
+ .OrderByDescending(s => s.rect.width * s.rect.height)
529
+ .ToList();
530
+ if (!spritesInGroup.Any())
531
+ {
532
+ continue;
533
+ }
534
+
535
+ List<List<Sprite>> chunks = new();
536
+ List<Sprite> current = new();
537
+ long currentArea = 0;
538
+ foreach (Sprite sprite in spritesInGroup)
539
+ {
540
+ long area = (long)(sprite.rect.width * sprite.rect.height);
541
+ if (area > AtlasAreaBudget)
542
+ {
543
+ this.LogWarn(
544
+ $"Sprite '{sprite.name}' ({sprite.rect.width}x{sprite.rect.height}) is larger than max atlas area budget and will be placed in its own atlas chunk."
545
+ );
546
+ continue;
547
+ }
548
+ if (currentArea + area <= AtlasAreaBudget && current.Count < 2000)
549
+ {
550
+ current.Add(sprite);
551
+ currentArea += area;
552
+ }
553
+ else
554
+ {
555
+ if (current.Count > 1)
556
+ {
557
+ chunks.Add(current);
558
+ }
559
+ current = new List<Sprite> { sprite };
560
+ currentArea = area;
561
+ }
562
+ }
563
+
564
+ if (current.Count > 1)
565
+ {
566
+ chunks.Add(current);
567
+ }
568
+
569
+ if (chunks.Count > 0)
570
+ {
571
+ groupChunks[kv.Key] = chunks;
572
+ totalChunks += chunks.Count;
573
+ }
574
+ }
575
+
576
+ if (totalChunks == 0)
577
+ {
578
+ this.Log(
579
+ $"No sprites matched the regex '{_nameRegex}' or formed valid chunks."
580
+ );
581
+ EditorUtility.ClearProgressBar();
582
+ return;
583
+ }
584
+
585
+ List<(string Name, List<Sprite> Sprites)> finalAtlasesData;
586
+ if (_optimizeGroupings)
587
+ {
588
+ EditorUtility.DisplayProgressBar(
589
+ Name,
590
+ "Preparing for merge optimization...",
591
+ 0.4f
592
+ );
593
+
594
+ List<AtlasCandidate> allInitialCandidates = new();
595
+ foreach (
596
+ (
597
+ string originalGroupKey,
598
+ List<List<Sprite>> chunksForThisGroup
599
+ ) in groupChunks
600
+ )
601
+ {
602
+ for (int i = 0; i < chunksForThisGroup.Count; i++)
603
+ {
604
+ if (!chunksForThisGroup[i].Any())
605
+ {
606
+ continue;
607
+ }
608
+
609
+ string candidateName =
610
+ chunksForThisGroup.Count > 1
611
+ ? $"{originalGroupKey}_{i}"
612
+ : originalGroupKey;
613
+ allInitialCandidates.Add(
614
+ new AtlasCandidate(
615
+ originalGroupKey,
616
+ chunksForThisGroup[i],
617
+ candidateName
618
+ )
619
+ );
620
+ }
621
+ }
622
+
623
+ allInitialCandidates = allInitialCandidates
624
+ .OrderByDescending(c => c.TotalArea)
625
+ .ThenBy(c => c.CandidateName, StringComparer.Ordinal)
626
+ .ToList();
627
+
628
+ List<MergeableAtlas> workingAtlases = allInitialCandidates
629
+ .Select(c => new MergeableAtlas(
630
+ c.OriginalGroupKey,
631
+ c.Sprites,
632
+ c.CandidateName,
633
+ c.TotalArea
634
+ ))
635
+ .ToList();
636
+ int passNumber = 0;
637
+ float mergeOptimizationProgressStart = 0.30f;
638
+ float mergeOptimizationProgressRange = 0.50f;
639
+
640
+ while (true)
641
+ {
642
+ passNumber++;
643
+ bool mergedInThisPass = false;
644
+ float currentPassProgress =
645
+ mergeOptimizationProgressStart
646
+ + passNumber * (mergeOptimizationProgressRange / 15.0f);
647
+ EditorUtility.DisplayProgressBar(
648
+ Name,
649
+ $"Optimizing atlas count (Pass {passNumber}, {workingAtlases.Count} atlases)...",
650
+ Mathf.Min(
651
+ currentPassProgress,
652
+ mergeOptimizationProgressStart + mergeOptimizationProgressRange
653
+ )
654
+ );
655
+
656
+ workingAtlases = workingAtlases
657
+ .OrderByDescending(a => a.TotalArea)
658
+ .ThenBy(a => a.RepresentativeInitialName, StringComparer.Ordinal)
659
+ .ToList();
660
+
661
+ bool[] isSubsumed = new bool[workingAtlases.Count];
662
+
663
+ for (int i = 0; i < workingAtlases.Count; i++)
664
+ {
665
+ if (isSubsumed[i])
666
+ {
667
+ continue;
668
+ }
669
+
670
+ MergeableAtlas baseAtlas = workingAtlases[i];
671
+ string baseRepresentativeKey = baseAtlas
672
+ .OriginalGroupKeys.OrderBy(k => k, StringComparer.Ordinal)
673
+ .First();
674
+
675
+ int bestPartnerIndex = -1;
676
+ MergeableAtlas bestPartnerObject = null;
677
+ int currentMinLevenshtein = int.MaxValue;
678
+
679
+ for (int j = i + 1; j < workingAtlases.Count; j++)
680
+ {
681
+ if (isSubsumed[j])
682
+ {
683
+ continue;
684
+ }
685
+
686
+ MergeableAtlas potentialPartner = workingAtlases[j];
687
+ if (
688
+ baseAtlas.TotalArea + potentialPartner.TotalArea
689
+ > AtlasAreaBudget
690
+ )
691
+ {
692
+ continue;
693
+ }
694
+
695
+ string partnerRepresentativeKey = potentialPartner
696
+ .OriginalGroupKeys.OrderBy(k => k, StringComparer.Ordinal)
697
+ .First();
698
+ int distance = baseRepresentativeKey.LevenshteinDistance(
699
+ partnerRepresentativeKey
700
+ );
701
+ bool updateBest = false;
702
+ if (bestPartnerObject == null || distance < currentMinLevenshtein)
703
+ {
704
+ updateBest = true;
705
+ }
706
+ else if (distance == currentMinLevenshtein)
707
+ {
708
+ if (
709
+ potentialPartner.TotalArea > bestPartnerObject.TotalArea
710
+ || (
711
+ potentialPartner.TotalArea
712
+ == bestPartnerObject.TotalArea
713
+ && string.Compare(
714
+ potentialPartner.RepresentativeInitialName,
715
+ bestPartnerObject.RepresentativeInitialName,
716
+ StringComparison.Ordinal
717
+ ) < 0
718
+ )
719
+ )
720
+ {
721
+ updateBest = true;
722
+ }
723
+ }
724
+ if (updateBest)
725
+ {
726
+ currentMinLevenshtein = distance;
727
+ bestPartnerObject = potentialPartner;
728
+ bestPartnerIndex = j;
729
+ }
730
+ }
731
+ if (bestPartnerObject != null)
732
+ {
733
+ workingAtlases[i] = MergeableAtlas.Merge(
734
+ baseAtlas,
735
+ bestPartnerObject
736
+ );
737
+ isSubsumed[bestPartnerIndex] = true;
738
+ mergedInThisPass = true;
739
+ }
740
+ }
741
+ if (!mergedInThisPass)
742
+ {
743
+ break;
744
+ }
745
+
746
+ workingAtlases = workingAtlases.Where((_, k) => !isSubsumed[k]).ToList();
747
+ if (passNumber > 100)
748
+ {
749
+ this.LogWarn(
750
+ $"Merge optimization exceeded 100 passes, aborting merge loop."
751
+ );
752
+ break;
753
+ }
754
+ }
755
+
756
+ finalAtlasesData = workingAtlases
757
+ .Select(a => (Name: a.GenerateFinalName(), Sprites: a.Sprites))
758
+ .OrderBy(a => a.Name)
759
+ .ToList();
760
+ }
761
+ else
762
+ {
763
+ finalAtlasesData = groupChunks
764
+ .SelectMany(chunk =>
765
+ {
766
+ string prefix = chunk.Key;
767
+ List<List<Sprite>> chunks = chunk.Value;
768
+ List<(string, List<Sprite>)> finalChunks = new();
769
+ for (int i = 0; i < chunks.Count; i++)
770
+ {
771
+ string atlasName = chunks.Count > 1 ? $"{prefix}_{i}" : prefix;
772
+ finalChunks.Add((atlasName, chunks[i]));
773
+ }
774
+ return finalChunks;
775
+ })
776
+ .ToList();
777
+ int chunkIndex = 0;
778
+ float atlasCreationProgressStart = 0.45f;
779
+ float atlasCreationProgressRange = 0.5f;
780
+
781
+ foreach ((string prefix, List<List<Sprite>> chunks) in groupChunks)
782
+ {
783
+ for (int i = 0; i < chunks.Count; i++)
784
+ {
785
+ List<Sprite> chunk = chunks[i];
786
+ if (chunk == null || chunk.Count == 0)
787
+ {
788
+ continue;
789
+ }
790
+
791
+ float progress =
792
+ atlasCreationProgressStart
793
+ + atlasCreationProgressRange * (chunkIndex / (float)totalChunks);
794
+
795
+ string atlasName = chunks.Count > 1 ? $"{prefix}_{i}" : prefix;
796
+ EditorUtility.DisplayProgressBar(
797
+ Name,
798
+ $"Creating atlas '{atlasName}' ({i + 1}/{chunks.Count})... Sprites: {chunk.Count}",
799
+ progress
800
+ );
801
+
802
+ chunkIndex++;
803
+ }
804
+ }
805
+ }
806
+
807
+ foreach ((string atlasName, List<Sprite> sprites) in finalAtlasesData)
808
+ {
809
+ SpriteAtlas atlas = new();
810
+ atlases.Add(atlas);
811
+
812
+ SpriteAtlasPackingSettings packingSettings = atlas.GetPackingSettings();
813
+ packingSettings.enableTightPacking = true;
814
+ packingSettings.padding = 4;
815
+ packingSettings.enableRotation = false;
816
+ atlas.SetPackingSettings(packingSettings);
817
+
818
+ SpriteAtlasTextureSettings textureSettings = atlas.GetTextureSettings();
819
+ textureSettings.generateMipMaps = false;
820
+ textureSettings.filterMode = FilterMode.Bilinear;
821
+ textureSettings.readable = true;
822
+ atlas.SetTextureSettings(textureSettings);
823
+
824
+ TextureImporterPlatformSettings platformSettings = atlas.GetPlatformSettings(
825
+ DefaultPlatformName
826
+ );
827
+
828
+ if (platformSettings == null)
829
+ {
830
+ platformSettings = new TextureImporterPlatformSettings
831
+ {
832
+ name = DefaultPlatformName,
833
+ };
834
+ this.LogWarn(
835
+ $"Could not get default platform settings for {atlasName}. Creating new default."
836
+ );
837
+ }
838
+
839
+ platformSettings.overridden = true;
840
+ platformSettings.maxTextureSize = MaxAtlasDimension;
841
+ platformSettings.textureCompression = _compressionLevel;
842
+ platformSettings.format = TextureImporterFormat.Automatic;
843
+
844
+ if (_crunchCompression is >= 0 and <= 100)
845
+ {
846
+ platformSettings.crunchedCompression = true;
847
+ platformSettings.compressionQuality = _crunchCompression;
848
+ }
849
+ else
850
+ {
851
+ if (100 < _crunchCompression)
852
+ {
853
+ this.LogWarn(
854
+ $"Invalid crunch compression: {_crunchCompression}. Using default (off)."
855
+ );
856
+ }
857
+
858
+ platformSettings.crunchedCompression = false;
859
+ platformSettings.compressionQuality = 50;
860
+ }
861
+
862
+ atlas.SetPlatformSettings(platformSettings);
863
+
864
+ Object[] validSprites = sprites
865
+ .Select(sprite => sprite as Object)
866
+ .Where(Objects.NotNull)
867
+ .ToArray();
868
+ if (validSprites.Length == 0)
869
+ {
870
+ this.LogWarn(
871
+ $"Skipping atlas '{atlasName}' as it contained no valid sprites after filtering."
872
+ );
873
+ atlases.Remove(atlas);
874
+ }
875
+ else
876
+ {
877
+ atlas.Add(validSprites);
878
+ atlas.SetIncludeInBuild(true);
879
+ string path = Path.Combine(_outputFolder, atlasName + ".spriteatlas");
880
+ path = AssetDatabase.GenerateUniqueAssetPath(path);
881
+ AssetDatabase.CreateAsset(atlas, path);
882
+ processed++;
883
+ }
884
+ }
885
+ }
886
+ catch (Exception e)
887
+ {
888
+ this.LogError($"An unexpected error occurred during atlas generation.", e);
889
+ }
890
+ finally
891
+ {
892
+ AssetDatabase.StopAssetEditing();
893
+ EditorUtility.ClearProgressBar();
894
+ }
895
+
896
+ if (processed > 0)
897
+ {
898
+ AssetDatabase.SaveAssets();
899
+ AssetDatabase.Refresh();
900
+ SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget, false);
901
+ this.Log(
902
+ $"[SpriteAtlasGenerator] Successfully created or updated {processed} atlases in '{_outputFolder}'."
903
+ );
904
+ }
905
+ else
906
+ {
907
+ this.Log(
908
+ $"[SpriteAtlasGenerator] No atlases were generated. Check source folders and regex pattern."
909
+ );
910
+ }
911
+ }
912
+ }
913
+ #endif
914
+ }