com.wallstop-studios.unity-helpers 2.0.0-rc73 → 2.0.0-rc73.10

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