com.wallstop-studios.unity-helpers 2.0.0-rc73 → 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.
- package/Editor/AnimationCreator.cs +1 -1
- package/Editor/PrefabChecker.cs +2 -1
- package/Editor/SpriteAtlasGenerator.cs +497 -181
- package/Editor/Utils/GUIHorizontalScope.cs +20 -0
- package/Editor/Utils/GUIHorizontalScope.cs.meta +3 -0
- package/Runtime/Core/Extension/StringExtensions.cs +49 -0
- package/package.json +2 -1
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
);
|
|
220
220
|
|
|
221
221
|
string currentName =
|
|
222
|
-
nameProp != null ?
|
|
222
|
+
nameProp != null ? nameProp.stringValue ?? string.Empty : string.Empty;
|
|
223
223
|
|
|
224
224
|
bool matchesSearch = true;
|
|
225
225
|
if (searchTerms.Length > 0)
|
package/Editor/PrefabChecker.cs
CHANGED
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
{
|
|
92
92
|
EditorGUILayout.LabelField("Validation Checks", EditorStyles.boldLabel);
|
|
93
93
|
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
208
|
+
using (new GUIHorizontalScope())
|
|
72
209
|
{
|
|
73
|
-
|
|
210
|
+
EditorGUILayout.LabelField($"Matches: {_matchCount}");
|
|
211
|
+
GUILayout.FlexibleSpace();
|
|
212
|
+
EditorGUILayout.LabelField($"Non-matches: {_totalCount - _matchCount}");
|
|
74
213
|
}
|
|
75
214
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
EditorGUILayout.
|
|
232
|
+
using (new GUIHorizontalScope())
|
|
233
|
+
{
|
|
234
|
+
EditorGUILayout.LabelField("Optimize Groupings");
|
|
235
|
+
_optimizeGroupings = EditorGUILayout.Toggle(_optimizeGroupings);
|
|
236
|
+
}
|
|
88
237
|
|
|
89
238
|
GUILayout.Space(12);
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
270
|
+
using (new GUIHorizontalScope())
|
|
118
271
|
{
|
|
119
|
-
|
|
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
|
-
|
|
295
|
+
try
|
|
140
296
|
{
|
|
141
|
-
|
|
297
|
+
float total = _sourceFolders.Length;
|
|
298
|
+
foreach (Object obj in _sourceFolders)
|
|
142
299
|
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
159
|
-
if (
|
|
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
|
-
|
|
165
|
-
|
|
314
|
+
string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { folderPath });
|
|
315
|
+
for (int i = 0; i < guids.Length; i++)
|
|
166
316
|
{
|
|
167
|
-
|
|
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
|
-
|
|
173
|
-
|
|
332
|
+
IEnumerable<Sprite> sprites = assets.OfType<Sprite>();
|
|
333
|
+
foreach (Sprite sp in sprites)
|
|
174
334
|
{
|
|
175
|
-
|
|
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.
|
|
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(
|
|
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
|
|
277
|
-
|
|
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
|
-
|
|
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.
|
|
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>
|
|
351
|
-
.Value.
|
|
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
|
|
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 >
|
|
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 <=
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
-
if (chunk == null || chunk.Count == 0)
|
|
602
|
+
for (int i = 0; i < chunksForThisGroup.Count; i++)
|
|
421
603
|
{
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
623
|
+
allInitialCandidates = allInitialCandidates
|
|
624
|
+
.OrderByDescending(c => c.TotalArea)
|
|
625
|
+
.ThenBy(c => c.CandidateName, StringComparer.Ordinal)
|
|
626
|
+
.ToList();
|
|
428
627
|
|
|
429
|
-
|
|
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
|
-
$"
|
|
433
|
-
|
|
649
|
+
$"Optimizing atlas count (Pass {passNumber}, {workingAtlases.Count} atlases)...",
|
|
650
|
+
Mathf.Min(
|
|
651
|
+
currentPassProgress,
|
|
652
|
+
mergeOptimizationProgressStart + mergeOptimizationProgressRange
|
|
653
|
+
)
|
|
434
654
|
);
|
|
435
655
|
|
|
436
|
-
|
|
437
|
-
|
|
656
|
+
workingAtlases = workingAtlases
|
|
657
|
+
.OrderByDescending(a => a.TotalArea)
|
|
658
|
+
.ThenBy(a => a.RepresentativeInitialName, StringComparer.Ordinal)
|
|
659
|
+
.ToList();
|
|
438
660
|
|
|
439
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
$"
|
|
750
|
+
$"Merge optimization exceeded 100 passes, aborting merge loop."
|
|
460
751
|
);
|
|
752
|
+
break;
|
|
461
753
|
}
|
|
754
|
+
}
|
|
462
755
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
785
|
+
List<Sprite> chunk = chunks[i];
|
|
786
|
+
if (chunk == null || chunk.Count == 0)
|
|
476
787
|
{
|
|
477
|
-
|
|
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
|
-
|
|
791
|
+
float progress =
|
|
792
|
+
atlasCreationProgressStart
|
|
793
|
+
+ atlasCreationProgressRange * (chunkIndex / (float)totalChunks);
|
|
486
794
|
|
|
487
|
-
|
|
488
|
-
.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
807
|
+
foreach ((string atlasName, List<Sprite> sprites) in finalAtlasesData)
|
|
512
808
|
{
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
platformSettings.overridden = true;
|
|
840
|
+
platformSettings.maxTextureSize = MaxAtlasDimension;
|
|
841
|
+
platformSettings.textureCompression = _compressionLevel;
|
|
842
|
+
platformSettings.format = TextureImporterFormat.Automatic;
|
|
553
843
|
|
|
554
|
-
|
|
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.
|
|
557
|
-
$"
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
}
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.wallstop-studios.unity-helpers",
|
|
3
|
-
"version": "2.0.0-rc73",
|
|
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
|
+
|