com.wallstop-studios.unity-helpers 2.0.0-rc72 → 2.0.0-rc73.1

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.
@@ -219,7 +219,7 @@
219
219
  );
220
220
 
221
221
  string currentName =
222
- nameProp != null ? (nameProp.stringValue ?? string.Empty) : string.Empty;
222
+ nameProp != null ? nameProp.stringValue ?? string.Empty : string.Empty;
223
223
 
224
224
  bool matchesSearch = true;
225
225
  if (searchTerms.Length > 0)
@@ -91,7 +91,8 @@
91
91
  {
92
92
  EditorGUILayout.LabelField("Validation Checks", EditorStyles.boldLabel);
93
93
 
94
- var drawRightAlignedToggle = SetupDrawRightAlignedToggle();
94
+ Func<GUIContent, bool, float?, bool, bool> drawRightAlignedToggle =
95
+ SetupDrawRightAlignedToggle();
95
96
 
96
97
  float targetAlignmentX = 0f;
97
98
  bool alignmentCalculated = false;
@@ -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 != 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
+ }
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(
@@ -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,64 @@
136
292
  return;
137
293
  }
138
294
 
139
- foreach (Object obj in _sourceFolders)
295
+ try
140
296
  {
141
- if (obj == null)
297
+ float total = _sourceFolders.Length;
298
+ foreach (Object obj in _sourceFolders)
142
299
  {
143
- continue;
144
- }
145
-
146
- string folderPath = AssetDatabase.GetAssetPath(obj);
147
- if (string.IsNullOrEmpty(folderPath) || !AssetDatabase.IsValidFolder(folderPath))
148
- {
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);
300
+ if (obj == null)
301
+ {
302
+ continue;
303
+ }
157
304
 
158
- Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path);
159
- if (assets == null)
305
+ string folderPath = AssetDatabase.GetAssetPath(obj);
306
+ if (
307
+ string.IsNullOrEmpty(folderPath) || !AssetDatabase.IsValidFolder(folderPath)
308
+ )
160
309
  {
310
+ this.LogWarn($"Skipping invalid or null source folder entry.");
161
311
  continue;
162
312
  }
163
313
 
164
- IEnumerable<Sprite> sprites = assets.OfType<Sprite>();
165
- foreach (Sprite sp in sprites)
314
+ string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { folderPath });
315
+ for (int i = 0; i < guids.Length; i++)
166
316
  {
167
- if (sp == null)
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)
168
328
  {
169
329
  continue;
170
330
  }
171
331
 
172
- _totalCount++;
173
- if (regex.IsMatch(sp.name))
332
+ IEnumerable<Sprite> sprites = assets.OfType<Sprite>();
333
+ foreach (Sprite sp in sprites)
174
334
  {
175
- _matchCount++;
335
+ if (sp == null)
336
+ {
337
+ continue;
338
+ }
339
+
340
+ _totalCount++;
341
+ if (regex.IsMatch(sp.name))
342
+ {
343
+ _matchCount++;
344
+ }
176
345
  }
177
346
  }
178
347
  }
179
348
  }
349
+ finally
350
+ {
351
+ EditorUtility.ClearProgressBar();
352
+ }
180
353
 
181
354
  Repaint();
182
355
  }
@@ -185,6 +358,7 @@
185
358
  {
186
359
  List<SpriteAtlas> atlases = new();
187
360
  int processed = 0;
361
+ AssetDatabase.StartAssetEditing();
188
362
  try
189
363
  {
190
364
  EditorUtility.DisplayProgressBar(Name, "Initializing...", 0f);
@@ -237,7 +411,7 @@
237
411
  }
238
412
  }
239
413
 
240
- EditorUtility.DisplayProgressBar(Name, "Deleting old atlases...", 0.1f);
414
+ EditorUtility.DisplayProgressBar(Name, "Deleting old atlases...", 0.05f);
241
415
  string[] existing = AssetDatabase
242
416
  .FindAssets("t:SpriteAtlas", new[] { _outputFolder })
243
417
  .Select(AssetDatabase.GUIDToAssetPath)
@@ -257,7 +431,11 @@
257
431
  AssetDatabase.Refresh();
258
432
  }
259
433
 
260
- EditorUtility.DisplayProgressBar(Name, "Scanning sprites...", 0.25f);
434
+ EditorUtility.DisplayProgressBar(
435
+ Name,
436
+ "Scanning sprites & initial grouping...",
437
+ 0.1f
438
+ );
261
439
  Regex regex;
262
440
  try
263
441
  {
@@ -273,8 +451,9 @@
273
451
  }
274
452
  Dictionary<string, List<Sprite>> groups = new(StringComparer.Ordinal);
275
453
 
276
- float sourceFolderProgressIncrement = 0.15f / _sourceFolders.Length;
277
- float currentProgress = 0.25f;
454
+ float sourceFolderIncrement =
455
+ _sourceFolders.Length > 0 ? 0.2f / _sourceFolders.Length : 0f;
456
+ float sourceFolderProgress = 0.1f;
278
457
 
279
458
  foreach (Object sourceDirectory in _sourceFolders)
280
459
  {
@@ -295,9 +474,9 @@
295
474
  EditorUtility.DisplayProgressBar(
296
475
  Name,
297
476
  $"Scanning folder '{folderPath}'...",
298
- currentProgress
477
+ sourceFolderProgress
299
478
  );
300
-
479
+ sourceFolderProgress += sourceFolderIncrement;
301
480
  foreach (
302
481
  string assetGuid in AssetDatabase.FindAssets(
303
482
  "t:Sprite",
@@ -335,43 +514,38 @@
335
514
  groups.GetOrAdd(key).Add(sub);
336
515
  }
337
516
  }
338
- currentProgress += sourceFolderProgressIncrement;
339
517
  }
340
518
 
341
- const int atlasSize = 8192;
342
- const long budget = (long)atlasSize * atlasSize;
343
519
  int totalChunks = 0;
344
520
  Dictionary<string, List<List<Sprite>>> groupChunks = new();
345
521
 
346
- EditorUtility.DisplayProgressBar(Name, "Calculating chunks...", 0.4f);
522
+ EditorUtility.DisplayProgressBar(Name, "Calculating chunks...", 0.3f);
347
523
 
348
524
  foreach (KeyValuePair<string, List<Sprite>> kv in groups)
349
525
  {
350
- List<Sprite> sprites = kv
351
- .Value.OrderByDescending(s => s.rect.width * s.rect.height)
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)
352
529
  .ToList();
530
+ if (!spritesInGroup.Any())
531
+ {
532
+ continue;
533
+ }
534
+
353
535
  List<List<Sprite>> chunks = new();
354
536
  List<Sprite> current = new();
355
537
  long currentArea = 0;
356
- foreach (Sprite sprite in sprites)
538
+ foreach (Sprite sprite in spritesInGroup)
357
539
  {
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
540
  long area = (long)(sprite.rect.width * sprite.rect.height);
367
- if (area > budget)
541
+ if (area > AtlasAreaBudget)
368
542
  {
369
543
  this.LogWarn(
370
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."
371
545
  );
372
546
  continue;
373
547
  }
374
- if (currentArea + area <= budget && current.Count < 2000)
548
+ if (currentArea + area <= AtlasAreaBudget && current.Count < 2000)
375
549
  {
376
550
  current.Add(sprite);
377
551
  currentArea += area;
@@ -408,166 +582,304 @@
408
582
  return;
409
583
  }
410
584
 
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)
585
+ List<(string Name, List<Sprite> Sprites)> finalAtlasesData;
586
+ if (_optimizeGroupings)
416
587
  {
417
- for (int i = 0; i < chunks.Count; i++)
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
+ )
418
601
  {
419
- List<Sprite> chunk = chunks[i];
420
- if (chunk == null || chunk.Count == 0)
602
+ for (int i = 0; i < chunksForThisGroup.Count; i++)
421
603
  {
422
- continue;
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
+ );
423
620
  }
621
+ }
424
622
 
425
- float progress =
426
- atlasCreationProgressStart
427
- + atlasCreationProgressRange * (chunkIndex / (float)totalChunks);
623
+ allInitialCandidates = allInitialCandidates
624
+ .OrderByDescending(c => c.TotalArea)
625
+ .ThenBy(c => c.CandidateName, StringComparer.Ordinal)
626
+ .ToList();
428
627
 
429
- string atlasName = chunks.Count > 1 ? $"{prefix}_{i}" : prefix;
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);
430
647
  EditorUtility.DisplayProgressBar(
431
648
  Name,
432
- $"Creating atlas '{atlasName}' ({i + 1}/{chunks.Count})... Sprites: {chunk.Count}",
433
- progress
649
+ $"Optimizing atlas count (Pass {passNumber}, {workingAtlases.Count} atlases)...",
650
+ Mathf.Min(
651
+ currentPassProgress,
652
+ mergeOptimizationProgressStart + mergeOptimizationProgressRange
653
+ )
434
654
  );
435
655
 
436
- SpriteAtlas atlas = new();
437
- atlases.Add(atlas);
656
+ workingAtlases = workingAtlases
657
+ .OrderByDescending(a => a.TotalArea)
658
+ .ThenBy(a => a.RepresentativeInitialName, StringComparer.Ordinal)
659
+ .ToList();
438
660
 
439
- SpriteAtlasPackingSettings packingSettings = atlas.GetPackingSettings();
440
- packingSettings.enableTightPacking = true;
441
- packingSettings.padding = 4;
442
- packingSettings.enableRotation = false;
443
- atlas.SetPackingSettings(packingSettings);
661
+ bool[] isSubsumed = new bool[workingAtlases.Count];
444
662
 
445
- SpriteAtlasTextureSettings textureSettings = atlas.GetTextureSettings();
446
- textureSettings.generateMipMaps = false;
447
- textureSettings.filterMode = FilterMode.Bilinear;
448
- textureSettings.readable = false;
449
- atlas.SetTextureSettings(textureSettings);
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;
450
678
 
451
- TextureImporterPlatformSettings platformSettings =
452
- atlas.GetPlatformSettings(DefaultPlatformName);
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
+ }
453
745
 
454
- if (platformSettings == null)
746
+ workingAtlases = workingAtlases.Where((_, k) => !isSubsumed[k]).ToList();
747
+ if (passNumber > 100)
455
748
  {
456
- platformSettings = new TextureImporterPlatformSettings();
457
- platformSettings.name = DefaultPlatformName;
458
749
  this.LogWarn(
459
- $"Could not get default platform settings for {atlasName}. Creating new default."
750
+ $"Merge optimization exceeded 100 passes, aborting merge loop."
460
751
  );
752
+ break;
461
753
  }
754
+ }
462
755
 
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)
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 =>
469
765
  {
470
- platformSettings.crunchedCompression = true;
471
- platformSettings.compressionQuality = _crunchCompression;
472
- }
473
- else
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++)
474
784
  {
475
- if (100 < _crunchCompression)
785
+ List<Sprite> chunk = chunks[i];
786
+ if (chunk == null || chunk.Count == 0)
476
787
  {
477
- this.LogWarn(
478
- $"Invalid crunch compression: {_crunchCompression}. Using default (off)."
479
- );
788
+ continue;
480
789
  }
481
- platformSettings.crunchedCompression = false;
482
- platformSettings.compressionQuality = 50;
483
- }
484
790
 
485
- atlas.SetPlatformSettings(platformSettings);
791
+ float progress =
792
+ atlasCreationProgressStart
793
+ + atlasCreationProgressRange * (chunkIndex / (float)totalChunks);
486
794
 
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."
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
495
800
  );
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++;
801
+
802
+ chunkIndex++;
506
803
  }
507
- chunkIndex++;
508
804
  }
509
805
  }
510
806
 
511
- if (processed > 0)
807
+ foreach ((string atlasName, List<Sprite> sprites) in finalAtlasesData)
512
808
  {
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
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
521
826
  );
522
827
 
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
- )
828
+ if (platformSettings == null)
532
829
  {
533
- Texture2D preview = atlas.GetPreviewTexture();
534
- if (preview == null)
535
- {
536
- continue;
537
- }
538
-
539
- TextureImporterPlatformSettings platformSettings =
540
- atlas.GetPlatformSettings(DefaultPlatformName);
541
- if (platformSettings is not { overridden: true })
830
+ platformSettings = new TextureImporterPlatformSettings
542
831
  {
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)
832
+ name = DefaultPlatformName,
833
+ };
834
+ this.LogWarn(
835
+ $"Could not get default platform settings for {atlasName}. Creating new default."
551
836
  );
552
- newMaxSize = Mathf.Clamp(newMaxSize, 32, atlasSize);
837
+ }
838
+
839
+ platformSettings.overridden = true;
840
+ platformSettings.maxTextureSize = MaxAtlasDimension;
841
+ platformSettings.textureCompression = _compressionLevel;
842
+ platformSettings.format = TextureImporterFormat.Automatic;
553
843
 
554
- if (newMaxSize < platformSettings.maxTextureSize)
844
+ if (_crunchCompression is >= 0 and <= 100)
845
+ {
846
+ platformSettings.crunchedCompression = true;
847
+ platformSettings.compressionQuality = _crunchCompression;
848
+ }
849
+ else
850
+ {
851
+ if (100 < _crunchCompression)
555
852
  {
556
- this.Log(
557
- $"Optimizing atlas '{atlas.name}' max size from {platformSettings.maxTextureSize} to {newMaxSize}"
853
+ this.LogWarn(
854
+ $"Invalid crunch compression: {_crunchCompression}. Using default (off)."
558
855
  );
559
- platformSettings.maxTextureSize = newMaxSize;
560
- atlas.SetPlatformSettings(platformSettings);
561
- EditorUtility.SetDirty(atlas);
562
- anyChanged = true;
563
856
  }
857
+
858
+ platformSettings.crunchedCompression = false;
859
+ platformSettings.compressionQuality = 50;
564
860
  }
565
861
 
566
- if (anyChanged)
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)
567
869
  {
568
- EditorUtility.DisplayProgressBar(Name, "Saving optimizations...", 0.99f);
569
- AssetDatabase.SaveAssets();
570
- AssetDatabase.Refresh();
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++;
571
883
  }
572
884
  }
573
885
  }
@@ -577,11 +889,15 @@
577
889
  }
578
890
  finally
579
891
  {
892
+ AssetDatabase.StopAssetEditing();
580
893
  EditorUtility.ClearProgressBar();
581
894
  }
582
895
 
583
896
  if (processed > 0)
584
897
  {
898
+ AssetDatabase.SaveAssets();
899
+ AssetDatabase.Refresh();
900
+ SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget, false);
585
901
  this.Log(
586
902
  $"[SpriteAtlasGenerator] Successfully created or updated {processed} atlases in '{_outputFolder}'."
587
903
  );
@@ -0,0 +1,20 @@
1
+ namespace WallstopStudios.UnityHelpers.Editor.Utils
2
+ {
3
+ #if UNITY_EDITOR
4
+ using System;
5
+ using UnityEditor;
6
+
7
+ public sealed class GUIHorizontalScope : IDisposable
8
+ {
9
+ public GUIHorizontalScope()
10
+ {
11
+ EditorGUILayout.BeginHorizontal();
12
+ }
13
+
14
+ public void Dispose()
15
+ {
16
+ EditorGUILayout.EndHorizontal();
17
+ }
18
+ }
19
+ #endif
20
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 91a6cb913d1847ae9e2a82b1f59660a7
3
+ timeCreated: 1746644886
@@ -4,6 +4,7 @@
4
4
  using System.Text;
5
5
  using System.Threading;
6
6
  using Serialization;
7
+ using UnityEngine;
7
8
 
8
9
  public static class StringExtensions
9
10
  {
@@ -48,6 +49,54 @@
48
49
  return Serializer.JsonStringify(value);
49
50
  }
50
51
 
52
+ public static int LevenshteinDistance(this string source1, string source2)
53
+ {
54
+ source1 ??= string.Empty;
55
+ source2 ??= string.Empty;
56
+
57
+ int source1Length = source1.Length;
58
+ int source2Length = source2.Length;
59
+
60
+ int[][] matrix = new int[source1Length + 1][];
61
+ for (int index = 0; index < source1Length + 1; index++)
62
+ {
63
+ matrix[index] = new int[source2Length + 1];
64
+ }
65
+
66
+ if (source1Length == 0)
67
+ {
68
+ return source2Length;
69
+ }
70
+
71
+ if (source2Length == 0)
72
+ {
73
+ return source1Length;
74
+ }
75
+
76
+ for (int i = 0; i <= source1Length; matrix[i][0] = ++i)
77
+ {
78
+ // Spin to force array population
79
+ }
80
+
81
+ for (int j = 0; j <= source2Length; matrix[0][j] = ++j)
82
+ {
83
+ // Spin to force array population
84
+ }
85
+
86
+ for (int i = 1; i <= source1Length; ++i)
87
+ {
88
+ for (int j = 1; j <= source2Length; ++j)
89
+ {
90
+ int cost = source2[j - 1] == source1[i - 1] ? 0 : 1;
91
+ matrix[i][j] = Mathf.Min(
92
+ Mathf.Min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1),
93
+ matrix[i - 1][j - 1] + cost
94
+ );
95
+ }
96
+ }
97
+ return matrix[source1Length][source2Length];
98
+ }
99
+
51
100
  public static string ToPascalCase(this string value, string separator = "")
52
101
  {
53
102
  int startIndex = 0;
@@ -5,10 +5,12 @@
5
5
  using System.ComponentModel;
6
6
  using System.Linq;
7
7
  using System.Text;
8
+ using System.Text.Json.Serialization;
8
9
  using Core.Extension;
9
10
  using Core.Helper;
10
- using Newtonsoft.Json;
11
+ #if ODIN_INSPECTOR
11
12
  using Sirenix.OdinInspector;
13
+ #endif
12
14
 
13
15
  [Serializable]
14
16
  public sealed class AttributeEffect :
@@ -219,7 +221,9 @@
219
221
 
220
222
  for (int i = 0; i < effectTags.Count; ++i)
221
223
  {
222
- if (effectTags[i] != other.effectTags[i])
224
+ if (
225
+ !string.Equals(effectTags[i], other.effectTags[i], StringComparison.Ordinal)
226
+ )
223
227
  {
224
228
  return false;
225
229
  }
@@ -25,10 +25,7 @@
25
25
 
26
26
  public static bool operator ==(AttributeModification lhs, AttributeModification rhs)
27
27
  {
28
- // ReSharper disable once CompareOfFloatsByEqualityOperator
29
- return string.Equals(lhs.attribute, rhs.attribute)
30
- && lhs.action == rhs.action
31
- && lhs.value == rhs.value;
28
+ return lhs.Equals(rhs);
32
29
  }
33
30
 
34
31
  public override bool Equals(object obj)
@@ -36,16 +33,16 @@
36
33
  return obj is AttributeModification other && Equals(other);
37
34
  }
38
35
 
39
- public override int GetHashCode()
40
- {
41
- return Objects.HashCode(attribute, action, value);
42
- }
43
-
44
36
  public bool Equals(AttributeModification other)
45
37
  {
46
38
  return string.Equals(attribute, other.attribute, StringComparison.Ordinal)
47
39
  && action == other.action
48
40
  && value.Equals(other.value);
49
41
  }
42
+
43
+ public override int GetHashCode()
44
+ {
45
+ return Objects.HashCode(attribute, action, value);
46
+ }
50
47
  }
51
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.wallstop-studios.unity-helpers",
3
- "version": "2.0.0-rc72",
3
+ "version": "2.0.0-rc73.1",
4
4
  "displayName": "Unity Helpers",
5
5
  "description": "Various Unity Helper Library",
6
6
  "dependencies": {},
@@ -36,3 +36,4 @@
36
36
  "test": "echo \"Error: no test specified\" && exit 1"
37
37
  }
38
38
  }
39
+