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.
- package/Editor/AnimationEventEditor.cs +4 -5
- package/Editor/CustomEditors/MatchColliderToSpriteEditor.cs +1 -1
- package/Editor/CustomEditors/PersistentDirectoryGUI.cs +590 -0
- package/Editor/CustomEditors/PersistentDirectoryGUI.cs.meta +3 -0
- package/Editor/CustomEditors/SourceFolderEntryDrawer.cs +298 -0
- package/Editor/CustomEditors/SourceFolderEntryDrawer.cs.meta +3 -0
- package/Editor/FitTextureSizeWindow.cs +5 -44
- package/Editor/PersistentDirectorySettings.cs +248 -0
- package/Editor/PersistentDirectorySettings.cs.meta +3 -0
- package/Editor/PrefabChecker.cs +1 -2
- package/Editor/{AnimationCopier.cs → Sprites/AnimationCopier.cs} +33 -166
- package/Editor/{AnimationCreator.cs → Sprites/AnimationCreator.cs} +9 -80
- package/Editor/Sprites/ScriptableSpriteAtlas.cs +95 -0
- package/Editor/Sprites/ScriptableSpriteAtlas.cs.meta +3 -0
- package/Editor/Sprites/ScriptableSpriteAtlasEditor.cs +938 -0
- package/Editor/Sprites/ScriptableSpriteAtlasEditor.cs.meta +3 -0
- package/Editor/{SpriteCropper.cs → Sprites/SpriteCropper.cs} +68 -66
- package/Editor/{SpriteSettingsApplier.cs → Sprites/SpriteSettingsApplier.cs} +9 -76
- package/Editor/{TextureResizerWizard.cs → Sprites/TextureResizerWizard.cs} +1 -1
- package/Editor/{TextureSettingsApplier.cs → Sprites/TextureSettingsApplier.cs} +1 -1
- package/Editor/Sprites.meta +3 -0
- package/Editor/Utils/DxReadOnlyPropertyDrawer.cs +1 -1
- package/Runtime/Core/Helper/DirectoryHelper.cs +64 -0
- package/package.json +3 -1
- package/Editor/SpriteAtlasGenerator.cs +0 -895
- package/Editor/SpriteAtlasGenerator.cs.meta +0 -3
- package/Editor/Utils/GUIHorizontalScope.cs +0 -20
- package/Editor/Utils/GUIHorizontalScope.cs.meta +0 -3
- package/Editor/Utils/GUIIndentScope.cs +0 -20
- package/Editor/Utils/GUIIndentScope.cs.meta +0 -3
- /package/Editor/{AnimationCopier.cs.meta → Sprites/AnimationCopier.cs.meta} +0 -0
- /package/Editor/{AnimationCreator.cs.meta → Sprites/AnimationCreator.cs.meta} +0 -0
- /package/Editor/{SpriteCropper.cs.meta → Sprites/SpriteCropper.cs.meta} +0 -0
- /package/Editor/{SpriteSettingsApplier.cs.meta → Sprites/SpriteSettingsApplier.cs.meta} +0 -0
- /package/Editor/{TextureResizerWizard.cs.meta → Sprites/TextureResizerWizard.cs.meta} +0 -0
- /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
|
-
}
|