com.wallstop-studios.unity-helpers 2.0.0-rc73.13 → 2.0.0-rc73.15

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 (36) hide show
  1. package/Editor/AnimationEventEditor.cs +4 -5
  2. package/Editor/CustomEditors/MatchColliderToSpriteEditor.cs +1 -1
  3. package/Editor/CustomEditors/PersistentDirectoryGUI.cs +590 -0
  4. package/Editor/CustomEditors/PersistentDirectoryGUI.cs.meta +3 -0
  5. package/Editor/CustomEditors/SourceFolderEntryDrawer.cs +298 -0
  6. package/Editor/CustomEditors/SourceFolderEntryDrawer.cs.meta +3 -0
  7. package/Editor/FitTextureSizeWindow.cs +5 -44
  8. package/Editor/PersistentDirectorySettings.cs +248 -0
  9. package/Editor/PersistentDirectorySettings.cs.meta +3 -0
  10. package/Editor/PrefabChecker.cs +1 -2
  11. package/Editor/{AnimationCopier.cs → Sprites/AnimationCopier.cs} +33 -166
  12. package/Editor/{AnimationCreator.cs → Sprites/AnimationCreator.cs} +9 -80
  13. package/Editor/Sprites/ScriptableSpriteAtlas.cs +95 -0
  14. package/Editor/Sprites/ScriptableSpriteAtlas.cs.meta +3 -0
  15. package/Editor/Sprites/ScriptableSpriteAtlasEditor.cs +938 -0
  16. package/Editor/Sprites/ScriptableSpriteAtlasEditor.cs.meta +3 -0
  17. package/Editor/{SpriteCropper.cs → Sprites/SpriteCropper.cs} +68 -66
  18. package/Editor/{SpriteSettingsApplier.cs → Sprites/SpriteSettingsApplier.cs} +9 -76
  19. package/Editor/{TextureResizerWizard.cs → Sprites/TextureResizerWizard.cs} +1 -1
  20. package/Editor/{TextureSettingsApplier.cs → Sprites/TextureSettingsApplier.cs} +1 -1
  21. package/Editor/Sprites.meta +3 -0
  22. package/Editor/Utils/DxReadOnlyPropertyDrawer.cs +1 -1
  23. package/Runtime/Core/Helper/DirectoryHelper.cs +64 -0
  24. package/package.json +3 -1
  25. package/Editor/SpriteAtlasGenerator.cs +0 -895
  26. package/Editor/SpriteAtlasGenerator.cs.meta +0 -3
  27. package/Editor/Utils/GUIHorizontalScope.cs +0 -20
  28. package/Editor/Utils/GUIHorizontalScope.cs.meta +0 -3
  29. package/Editor/Utils/GUIIndentScope.cs +0 -20
  30. package/Editor/Utils/GUIIndentScope.cs.meta +0 -3
  31. /package/Editor/{AnimationCopier.cs.meta → Sprites/AnimationCopier.cs.meta} +0 -0
  32. /package/Editor/{AnimationCreator.cs.meta → Sprites/AnimationCreator.cs.meta} +0 -0
  33. /package/Editor/{SpriteCropper.cs.meta → Sprites/SpriteCropper.cs.meta} +0 -0
  34. /package/Editor/{SpriteSettingsApplier.cs.meta → Sprites/SpriteSettingsApplier.cs.meta} +0 -0
  35. /package/Editor/{TextureResizerWizard.cs.meta → Sprites/TextureResizerWizard.cs.meta} +0 -0
  36. /package/Editor/{TextureSettingsApplier.cs.meta → Sprites/TextureSettingsApplier.cs.meta} +0 -0
@@ -1,895 +0,0 @@
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 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
- }
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.IsNullOrWhiteSpace(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.Where(Objects.NotNull))
299
- {
300
- string folderPath = AssetDatabase.GetAssetPath(obj);
301
- if (
302
- string.IsNullOrWhiteSpace(folderPath)
303
- || !AssetDatabase.IsValidFolder(folderPath)
304
- )
305
- {
306
- this.LogWarn($"Skipping invalid or null source folder entry.");
307
- continue;
308
- }
309
-
310
- string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { folderPath });
311
- for (int i = 0; i < guids.Length; ++i)
312
- {
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)
324
- {
325
- continue;
326
- }
327
-
328
- foreach (Sprite sp in assets.OfType<Sprite>().Where(Objects.NotNull))
329
- {
330
- _totalCount++;
331
- if (regex.IsMatch(sp.name))
332
- {
333
- _matchCount++;
334
- }
335
- }
336
- }
337
- }
338
- }
339
- finally
340
- {
341
- EditorUtility.ClearProgressBar();
342
- }
343
-
344
- Repaint();
345
- }
346
-
347
- private void GenerateAtlases()
348
- {
349
- List<SpriteAtlas> atlases = new();
350
- int processed = 0;
351
- AssetDatabase.StartAssetEditing();
352
- try
353
- {
354
- EditorUtility.DisplayProgressBar(Name, "Initializing...", 0f);
355
- if (string.IsNullOrWhiteSpace(_outputFolder))
356
- {
357
- this.LogError($"Invalid output folder.");
358
- EditorUtility.ClearProgressBar();
359
- return;
360
- }
361
-
362
- if (_sourceFolders == null || Array.TrueForAll(_sourceFolders, Objects.Null))
363
- {
364
- this.LogError($"No valid source folders specified.");
365
- EditorUtility.ClearProgressBar();
366
- return;
367
- }
368
-
369
- if (!AssetDatabase.IsValidFolder(_outputFolder))
370
- {
371
- try
372
- {
373
- string parent = Path.GetDirectoryName(_outputFolder);
374
- string newFolderName = Path.GetFileName(_outputFolder);
375
- if (
376
- string.IsNullOrWhiteSpace(parent)
377
- || string.IsNullOrWhiteSpace(newFolderName)
378
- )
379
- {
380
- this.LogError($"Output folder path '{_outputFolder}' is invalid.");
381
- EditorUtility.ClearProgressBar();
382
- return;
383
- }
384
- AssetDatabase.CreateFolder(parent, newFolderName);
385
- AssetDatabase.Refresh();
386
- if (!AssetDatabase.IsValidFolder(_outputFolder))
387
- {
388
- this.LogError($"Failed to create output folder: '{_outputFolder}'");
389
- EditorUtility.ClearProgressBar();
390
- return;
391
- }
392
- }
393
- catch (Exception ex)
394
- {
395
- this.LogError(
396
- $"Error creating output folder '{_outputFolder}': {ex.Message}"
397
- );
398
- EditorUtility.ClearProgressBar();
399
- return;
400
- }
401
- }
402
-
403
- EditorUtility.DisplayProgressBar(Name, "Deleting old atlases...", 0.05f);
404
- string[] existing = AssetDatabase
405
- .FindAssets("t:SpriteAtlas", new[] { _outputFolder })
406
- .Select(AssetDatabase.GUIDToAssetPath)
407
- .Where(path => !string.IsNullOrWhiteSpace(path))
408
- .ToArray();
409
-
410
- if (existing.Length > 0)
411
- {
412
- List<string> failedPaths = new();
413
- AssetDatabase.DeleteAssets(existing, failedPaths);
414
- if (failedPaths.Any())
415
- {
416
- this.LogWarn(
417
- $"Failed to delete {failedPaths.Count} atlases:\n{string.Join("\n", failedPaths)}"
418
- );
419
- }
420
- AssetDatabase.Refresh();
421
- }
422
-
423
- EditorUtility.DisplayProgressBar(
424
- Name,
425
- "Scanning sprites & initial grouping...",
426
- 0.1f
427
- );
428
- Regex regex;
429
- try
430
- {
431
- regex = new Regex(_nameRegex);
432
- }
433
- catch (ArgumentException ex)
434
- {
435
- this.LogError(
436
- $"Invalid Regex pattern for generation: '{_nameRegex}'. Error: {ex.Message}"
437
- );
438
- EditorUtility.ClearProgressBar();
439
- return;
440
- }
441
- Dictionary<string, List<Sprite>> groups = new(StringComparer.Ordinal);
442
-
443
- float sourceFolderIncrement =
444
- _sourceFolders.Length > 0 ? 0.2f / _sourceFolders.Length : 0f;
445
- float sourceFolderProgress = 0.1f;
446
-
447
- foreach (Object sourceDirectory in _sourceFolders.Where(Objects.NotNull))
448
- {
449
- string folderPath = AssetDatabase.GetAssetPath(sourceDirectory);
450
- if (!AssetDatabase.IsValidFolder(folderPath))
451
- {
452
- this.LogWarn(
453
- $"Skipping invalid source folder during generation: '{folderPath}'"
454
- );
455
- continue;
456
- }
457
-
458
- EditorUtility.DisplayProgressBar(
459
- Name,
460
- $"Scanning folder '{folderPath}'...",
461
- sourceFolderProgress
462
- );
463
- sourceFolderProgress += sourceFolderIncrement;
464
- foreach (
465
- string assetGuid in AssetDatabase.FindAssets(
466
- "t:Sprite",
467
- new[] { folderPath }
468
- )
469
- )
470
- {
471
- string assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
472
- if (string.IsNullOrWhiteSpace(assetPath))
473
- {
474
- continue;
475
- }
476
-
477
- Object[] allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
478
- if (allAssets == null)
479
- {
480
- continue;
481
- }
482
-
483
- foreach (Sprite sub in allAssets.OfType<Sprite>().Where(Objects.NotNull))
484
- {
485
- string assetName = sub.name;
486
- if (!regex.IsMatch(assetName))
487
- {
488
- continue;
489
- }
490
-
491
- Match match = Regex.Match(assetName, @"^(.+?)(?:_\d+)?$");
492
- string key = match.Success ? match.Groups[1].Value : assetName;
493
- groups.GetOrAdd(key).Add(sub);
494
- }
495
- }
496
- }
497
-
498
- int totalChunks = 0;
499
- Dictionary<string, List<List<Sprite>>> groupChunks = new();
500
- EditorUtility.DisplayProgressBar(Name, "Calculating chunks...", 0.3f);
501
- foreach (KeyValuePair<string, List<Sprite>> kv in groups)
502
- {
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)
507
- .ToList();
508
- if (!spritesInGroup.Any())
509
- {
510
- continue;
511
- }
512
-
513
- List<List<Sprite>> chunks = new();
514
- List<Sprite> current = new();
515
- long currentArea = 0;
516
- foreach (Sprite sprite in spritesInGroup)
517
- {
518
- long area = (long)(sprite.rect.width * sprite.rect.height);
519
- if (area > AtlasAreaBudget)
520
- {
521
- this.LogWarn(
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."
523
- );
524
- continue;
525
- }
526
- if (currentArea + area <= AtlasAreaBudget && current.Count < 2000)
527
- {
528
- current.Add(sprite);
529
- currentArea += area;
530
- }
531
- else
532
- {
533
- if (current.Count > 1)
534
- {
535
- chunks.Add(current);
536
- }
537
- current = new List<Sprite> { sprite };
538
- currentArea = area;
539
- }
540
- }
541
-
542
- if (current.Count > 1)
543
- {
544
- chunks.Add(current);
545
- }
546
-
547
- if (chunks.Count > 0)
548
- {
549
- groupChunks[kv.Key] = chunks;
550
- totalChunks += chunks.Count;
551
- }
552
- }
553
-
554
- if (totalChunks == 0)
555
- {
556
- this.Log(
557
- $"No sprites matched the regex '{_nameRegex}' or formed valid chunks."
558
- );
559
- EditorUtility.ClearProgressBar();
560
- return;
561
- }
562
-
563
- List<(string Name, List<Sprite> Sprites)> finalAtlasesData;
564
- if (_optimizeGroupings)
565
- {
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
- )
579
- {
580
- for (int i = 0; i < chunksForThisGroup.Count; ++i)
581
- {
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
- );
598
- }
599
- }
600
-
601
- allInitialCandidates = allInitialCandidates
602
- .OrderByDescending(candidate => candidate.TotalArea)
603
- .ThenBy(candidate => candidate.CandidateName, StringComparer.Ordinal)
604
- .ToList();
605
-
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;
617
-
618
- while (true)
619
- {
620
- passNumber++;
621
- bool mergedInThisPass = false;
622
- float currentPassProgress =
623
- mergeOptimizationProgressStart
624
- + passNumber * (mergeOptimizationProgressRange / 15f);
625
- EditorUtility.DisplayProgressBar(
626
- Name,
627
- $"Optimizing atlas count (Pass {passNumber}, {workingAtlases.Count} atlases)...",
628
- Mathf.Min(
629
- currentPassProgress,
630
- mergeOptimizationProgressStart + mergeOptimizationProgressRange
631
- )
632
- );
633
-
634
- workingAtlases = workingAtlases
635
- .OrderByDescending(atlas => atlas.TotalArea)
636
- .ThenBy(
637
- atlas => atlas.RepresentativeInitialName,
638
- StringComparer.Ordinal
639
- )
640
- .ToList();
641
-
642
- bool[] isSubsumed = new bool[workingAtlases.Count];
643
-
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();
655
-
656
- int bestPartnerIndex = -1;
657
- MergeableAtlas bestPartnerObject = null;
658
- int currentMinLevenshtein = int.MaxValue;
659
-
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)
729
- {
730
- this.LogWarn(
731
- $"Merge optimization exceeded 100 passes, aborting merge loop."
732
- );
733
- break;
734
- }
735
- }
736
-
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 =>
746
- {
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)
765
- {
766
- List<Sprite> chunk = chunks[i];
767
- if (chunk is not { Count: > 0 })
768
- {
769
- continue;
770
- }
771
-
772
- float progress =
773
- atlasCreationProgressStart
774
- + atlasCreationProgressRange * (chunkIndex / (float)totalChunks);
775
-
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
781
- );
782
-
783
- chunkIndex++;
784
- }
785
- }
786
- }
787
-
788
- foreach ((string atlasName, List<Sprite> sprites) in finalAtlasesData)
789
- {
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
807
- );
808
-
809
- if (platformSettings == null)
810
- {
811
- platformSettings = new TextureImporterPlatformSettings
812
- {
813
- name = DefaultPlatformName,
814
- };
815
- this.LogWarn(
816
- $"Could not get default platform settings for {atlasName}. Creating new default."
817
- );
818
- }
819
-
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)
833
- {
834
- this.LogWarn(
835
- $"Invalid crunch compression: {_crunchCompression}. Using default (off)."
836
- );
837
- }
838
-
839
- platformSettings.crunchedCompression = false;
840
- platformSettings.compressionQuality = 50;
841
- }
842
-
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)
850
- {
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++;
864
- }
865
- }
866
- }
867
- catch (Exception e)
868
- {
869
- this.LogError($"An unexpected error occurred during atlas generation.", e);
870
- }
871
- finally
872
- {
873
- AssetDatabase.StopAssetEditing();
874
- EditorUtility.ClearProgressBar();
875
- }
876
-
877
- if (processed > 0)
878
- {
879
- AssetDatabase.SaveAssets();
880
- AssetDatabase.Refresh();
881
- SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget, false);
882
- this.Log(
883
- $"[SpriteAtlasGenerator] Successfully created or updated {processed} atlases in '{_outputFolder}'."
884
- );
885
- }
886
- else
887
- {
888
- this.Log(
889
- $"[SpriteAtlasGenerator] No atlases were generated. Check source folders and regex pattern."
890
- );
891
- }
892
- }
893
- }
894
- #endif
895
- }