com.wallstop-studios.unity-helpers 2.0.0-rc69 → 2.0.0-rc70

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 (45) hide show
  1. package/Editor/AnimationCopier.cs +875 -93
  2. package/Editor/AnimationCreator.cs +840 -137
  3. package/Editor/AnimationEventEditor.cs +4 -4
  4. package/Editor/AnimatorControllerCopier.cs +3 -3
  5. package/Editor/Extensions/UnityExtensions.cs +26 -0
  6. package/Editor/Extensions/UnityExtensions.cs.meta +3 -0
  7. package/Editor/Extensions.meta +3 -0
  8. package/Editor/FitTextureSizeWindow.cs +371 -0
  9. package/Editor/PrefabChecker.cs +716 -0
  10. package/Editor/SpriteAtlasGenerator.cs +598 -0
  11. package/Editor/SpriteAtlasGenerator.cs.meta +3 -0
  12. package/Editor/SpriteCropper.cs +407 -0
  13. package/Editor/SpriteCropper.cs.meta +3 -0
  14. package/Editor/SpriteSettingsApplier.cs +756 -92
  15. package/Editor/TextureResizerWizard.cs +3 -3
  16. package/Editor/TextureSettingsApplier.cs +9 -9
  17. package/Editor/WShowIfPropertyDrawer.cs +2 -2
  18. package/Runtime/Core/Attributes/EnumDisplayNameAttribute.cs +15 -0
  19. package/Runtime/Core/Attributes/EnumDisplayNameAttribute.cs.meta +3 -0
  20. package/Runtime/Core/Extension/EnumExtensions.cs +176 -1
  21. package/Runtime/Core/Extension/UnityExtensions.cs +1 -1
  22. package/Runtime/Tags/AttributeUtilities.cs +8 -7
  23. package/Runtime/Tags/EffectHandler.cs +2 -1
  24. package/Tests/Runtime/DataStructures/BalancedKDTreeTests.cs +1 -1
  25. package/Tests/Runtime/DataStructures/CyclicBufferTests.cs +1 -1
  26. package/Tests/Runtime/DataStructures/QuadTreeTests.cs +1 -1
  27. package/Tests/Runtime/DataStructures/UnbalancedKDTreeTests.cs +1 -1
  28. package/Tests/Runtime/Extensions/DictionaryExtensionTests.cs +1 -1
  29. package/Tests/Runtime/Extensions/EnumExtensionTests.cs +1 -1
  30. package/Tests/Runtime/Extensions/IListExtensionTests.cs +1 -1
  31. package/Tests/Runtime/Extensions/LoggingExtensionTests.cs +1 -1
  32. package/Tests/Runtime/Extensions/RandomExtensionTests.cs +1 -1
  33. package/Tests/Runtime/Extensions/StringExtensionTests.cs +1 -1
  34. package/Tests/Runtime/Helper/WallMathTests.cs +1 -1
  35. package/Tests/Runtime/Performance/KDTreePerformanceTests.cs +1 -1
  36. package/Tests/Runtime/Performance/QuadTreePerformanceTests.cs +1 -1
  37. package/Tests/Runtime/Performance/SpatialTreePerformanceTest.cs +1 -1
  38. package/Tests/Runtime/Performance/UnbalancedKDTreeTests.cs +1 -1
  39. package/Tests/Runtime/Random/RandomTestBase.cs +2 -2
  40. package/Tests/Runtime/Serialization/JsonSerializationTest.cs +1 -1
  41. package/package.json +1 -1
  42. package/Editor/FitTextureSizeWizard.cs +0 -147
  43. package/Editor/PrefabCheckWizard.cs +0 -167
  44. /package/Editor/{FitTextureSizeWizard.cs.meta → FitTextureSizeWindow.cs.meta} +0 -0
  45. /package/Editor/{PrefabCheckWizard.cs.meta → PrefabChecker.cs.meta} +0 -0
@@ -1,12 +1,14 @@
1
1
  namespace WallstopStudios.UnityHelpers.Editor
2
2
  {
3
3
  #if UNITY_EDITOR
4
+ using Core.Extension;
4
5
  using System;
5
6
  using System.Collections.Generic;
7
+ using System.IO;
6
8
  using System.Linq;
9
+ using System.Text.RegularExpressions;
7
10
  using UnityEditor;
8
11
  using UnityEngine;
9
- using WallstopStudios.UnityHelpers.Core.Extension;
10
12
  using Object = UnityEngine.Object;
11
13
 
12
14
  [Serializable]
@@ -14,236 +16,937 @@
14
16
  {
15
17
  public const int DefaultFramesPerSecond = 12;
16
18
 
17
- public List<Texture2D> frames = new();
19
+ public List<Sprite> frames = new();
18
20
  public int framesPerSecond = DefaultFramesPerSecond;
19
21
  public string animationName = string.Empty;
22
+ public bool isCreatedFromAutoParse;
23
+ public bool loop;
20
24
  }
21
25
 
22
- public sealed class AnimationCreator : ScriptableWizard
26
+ public sealed class AnimationCreatorWindow : EditorWindow
23
27
  {
24
- public List<AnimationData> animationData = new() { new AnimationData() };
28
+ private SerializedObject _serializedObject;
29
+ private SerializedProperty _animationDataProp;
30
+ private SerializedProperty _animationSourcesProp;
31
+ private SerializedProperty _spriteNameRegexProp;
32
+ private SerializedProperty _textProp;
33
+
34
+ public List<AnimationData> animationData = new();
25
35
  public List<Object> animationSources = new();
36
+ public string spriteNameRegex = ".*";
26
37
  public string text;
27
38
 
28
- [MenuItem("Tools/Unity Helpers/Animation Creator", priority = -3)]
29
- public static void CreateAnimation()
39
+ [HideInInspector]
40
+ [SerializeField]
41
+ private List<Sprite> _filteredSprites = new();
42
+ private int _matchedSpriteCount;
43
+ private int _unmatchedSpriteCount;
44
+ private Regex _compiledRegex;
45
+ private string _lastUsedRegex;
46
+ private string _searchString = string.Empty;
47
+ private Vector2 _scrollPosition;
48
+ private string _errorMessage = string.Empty;
49
+ private bool _animationDataIsExpanded = true;
50
+
51
+ [MenuItem("Tools/Wallstop Studios/Unity Helpers/Animation Creator", priority = -3)]
52
+ public static void ShowWindow()
53
+ {
54
+ GetWindow<AnimationCreatorWindow>("Animation Creator");
55
+ }
56
+
57
+ private void OnEnable()
30
58
  {
31
- _ = DisplayWizard<AnimationCreator>("Animation Creator", "Create");
59
+ _serializedObject = new SerializedObject(this);
60
+ _animationDataProp = _serializedObject.FindProperty(nameof(animationData));
61
+ _animationSourcesProp = _serializedObject.FindProperty(nameof(animationSources));
62
+ _spriteNameRegexProp = _serializedObject.FindProperty(nameof(spriteNameRegex));
63
+ _textProp = _serializedObject.FindProperty(nameof(text));
64
+
65
+ UpdateRegex();
66
+ FindAndFilterSprites();
67
+ Repaint();
32
68
  }
33
69
 
34
- protected override bool DrawWizardGUI()
70
+ private void OnGUI()
35
71
  {
36
- bool returnValue = base.DrawWizardGUI();
72
+ _serializedObject.Update();
37
73
 
38
- if (animationData is { Count: 1 })
74
+ _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
75
+
76
+ EditorGUILayout.LabelField("Configuration", EditorStyles.boldLabel);
77
+ EditorGUILayout.PropertyField(_animationSourcesProp, true);
78
+ EditorGUILayout.PropertyField(_spriteNameRegexProp);
79
+ EditorGUILayout.PropertyField(_textProp);
80
+
81
+ DrawAddSourceFolderButton();
82
+
83
+ if (!string.IsNullOrWhiteSpace(_errorMessage))
84
+ {
85
+ EditorGUILayout.HelpBox(_errorMessage, MessageType.Error);
86
+ }
87
+ else if (
88
+ _animationSourcesProp.arraySize == 0
89
+ || _animationSourcesProp.FindPropertyRelative("Array.size").intValue == 0
90
+ || animationSources.TrueForAll(s => s == null)
91
+ )
39
92
  {
40
- AnimationData data = animationData[0];
93
+ EditorGUILayout.HelpBox(
94
+ "Please specify at least one Animation Source (folder).",
95
+ MessageType.Error
96
+ );
97
+ }
41
98
 
42
- bool filled = false;
43
- if (
44
- data.frames is { Count: 0 }
45
- && GUILayout.Button("Fill Sprites From Animation Sources")
46
- )
99
+ EditorGUILayout.Space();
100
+ EditorGUILayout.LabelField("Animation Data", EditorStyles.boldLabel);
101
+ _searchString = EditorGUILayout.TextField("Search Animation Name", _searchString);
102
+
103
+ DrawFilteredAnimationData();
104
+
105
+ EditorGUILayout.Space();
106
+ EditorGUILayout.LabelField("Sprite Filter Status", EditorStyles.boldLabel);
107
+ EditorGUILayout.LabelField("Regex Pattern:", spriteNameRegex);
108
+ EditorGUILayout.LabelField("Matched Sprites:", _matchedSpriteCount.ToString());
109
+ EditorGUILayout.LabelField("Unmatched Sprites:", _unmatchedSpriteCount.ToString());
110
+
111
+ EditorGUILayout.Space();
112
+ EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel);
113
+
114
+ DrawActionButtons();
115
+
116
+ EditorGUILayout.EndScrollView();
117
+
118
+ _ = _serializedObject.ApplyModifiedProperties();
119
+ }
120
+
121
+ private void DrawAddSourceFolderButton()
122
+ {
123
+ if (!GUILayout.Button("Add Source Folder..."))
124
+ {
125
+ return;
126
+ }
127
+
128
+ string absolutePath = EditorUtility.OpenFolderPanel(
129
+ "Select Animation Source Folder",
130
+ "Assets",
131
+ ""
132
+ );
133
+
134
+ if (string.IsNullOrWhiteSpace(absolutePath))
135
+ {
136
+ return;
137
+ }
138
+
139
+ absolutePath = absolutePath.Replace("\\", "/");
140
+ if (absolutePath.StartsWith(Application.dataPath, StringComparison.OrdinalIgnoreCase))
141
+ {
142
+ string relativePath =
143
+ "Assets" + absolutePath.Substring(Application.dataPath.Length);
144
+
145
+ DefaultAsset folderAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(
146
+ relativePath
147
+ );
148
+
149
+ if (folderAsset != null && AssetDatabase.IsValidFolder(relativePath))
47
150
  {
48
- List<string> animationPaths = new();
49
- foreach (Object animationSource in animationSources)
151
+ bool alreadyExists = false;
152
+ for (int i = 0; i < _animationSourcesProp.arraySize; ++i)
50
153
  {
51
- string assetPath = AssetDatabase.GetAssetPath(animationSource);
52
- animationPaths.Add(assetPath);
53
- }
54
-
55
- foreach (
56
- string assetGuid in AssetDatabase.FindAssets(
57
- "t:sprite",
58
- animationPaths.ToArray()
154
+ if (
155
+ _animationSourcesProp.GetArrayElementAtIndex(i).objectReferenceValue
156
+ == folderAsset
59
157
  )
60
- )
61
- {
62
- string path = AssetDatabase.GUIDToAssetPath(assetGuid);
63
- Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
64
- if (sprite != null && sprite.texture != null)
65
158
  {
66
- data.frames.Add(sprite.texture);
159
+ alreadyExists = true;
160
+ this.LogWarn($"Folder '{relativePath}' is already in the list.");
161
+ break;
67
162
  }
68
163
  }
69
164
 
70
- filled = true;
165
+ if (!alreadyExists)
166
+ {
167
+ _animationSourcesProp.arraySize++;
168
+ _animationSourcesProp
169
+ .GetArrayElementAtIndex(_animationSourcesProp.arraySize - 1)
170
+ .objectReferenceValue = folderAsset;
171
+ this.Log($"Added source folder: {relativePath}");
172
+
173
+ _serializedObject.ApplyModifiedProperties();
174
+ FindAndFilterSprites();
175
+ Repaint();
176
+ }
177
+ }
178
+ else
179
+ {
180
+ this.LogError(
181
+ $"Could not load folder asset at path: {relativePath}. Is it a valid folder within the project?"
182
+ );
71
183
  }
184
+ }
185
+ else
186
+ {
187
+ this.LogError(
188
+ $"Selected folder must be inside the project's Assets folder. Path selected: {absolutePath}"
189
+ );
190
+ }
191
+ }
72
192
 
73
- if (
74
- data.frames is { Count: > 0 }
75
- && (filled || GUILayout.Button("Auto Parse Sprites"))
76
- )
193
+ private void DrawCheckSpritesButton()
194
+ {
195
+ if (GUILayout.Button("Check/Refresh Filtered Sprites"))
196
+ {
197
+ UpdateRegex();
198
+ FindAndFilterSprites();
199
+ Repaint();
200
+ }
201
+ }
202
+
203
+ private void DrawFilteredAnimationData()
204
+ {
205
+ int listSize = _animationDataProp.arraySize;
206
+ string[] searchTerms = string.IsNullOrWhiteSpace(_searchString)
207
+ ? Array.Empty<string>()
208
+ : _searchString.Split(
209
+ new[] { ' ', '\t', '\n', '\r' },
210
+ StringSplitOptions.RemoveEmptyEntries
211
+ );
212
+
213
+ List<int> matchingIndices = new();
214
+ for (int i = 0; i < listSize; ++i)
215
+ {
216
+ SerializedProperty elementProp = _animationDataProp.GetArrayElementAtIndex(i);
217
+ SerializedProperty nameProp = elementProp.FindPropertyRelative(
218
+ nameof(AnimationData.animationName)
219
+ );
220
+
221
+ string currentName =
222
+ nameProp != null ? (nameProp.stringValue ?? string.Empty) : string.Empty;
223
+
224
+ bool matchesSearch = true;
225
+ if (searchTerms.Length > 0)
77
226
  {
78
- Dictionary<
79
- string,
80
- Dictionary<string, List<Texture2D>>
81
- > texturesByPrefixAndAssetPath = new();
82
- foreach (Texture2D frame in data.frames)
83
- {
84
- string assetPathWithFrameName = AssetDatabase.GetAssetPath(frame);
85
- string frameName = frame.name;
86
- string assetPath = assetPathWithFrameName.Substring(
87
- 0,
88
- assetPathWithFrameName.LastIndexOf(frameName, StringComparison.Ordinal)
227
+ matchesSearch = searchTerms.All(term =>
228
+ currentName.Contains(term, StringComparison.OrdinalIgnoreCase)
229
+ );
230
+ }
231
+
232
+ if (matchesSearch)
233
+ {
234
+ matchingIndices.Add(i);
235
+ }
236
+ }
237
+ int matchCount = matchingIndices.Count;
238
+ string foldoutLabel =
239
+ $"{_animationDataProp.displayName} (Showing {matchCount} / {listSize})";
240
+ _animationDataIsExpanded = EditorGUILayout.Foldout(
241
+ _animationDataIsExpanded,
242
+ foldoutLabel,
243
+ true
244
+ );
245
+
246
+ if (_animationDataIsExpanded)
247
+ {
248
+ EditorGUI.indentLevel++;
249
+ if (matchCount > 0)
250
+ {
251
+ foreach (int index in matchingIndices)
252
+ {
253
+ SerializedProperty elementProp = _animationDataProp.GetArrayElementAtIndex(
254
+ index
89
255
  );
90
- int lastNumericIndex = frameName.Length - 1;
91
- for (int i = frameName.Length - 1; 0 <= i; --i)
92
- {
93
- if (char.IsNumber(frameName[i]))
94
- {
95
- continue;
96
- }
97
256
 
98
- lastNumericIndex = i + 1;
99
- break;
100
- }
257
+ SerializedProperty nameProp = elementProp.FindPropertyRelative(
258
+ nameof(AnimationData.animationName)
259
+ );
260
+ string currentName =
261
+ nameProp != null ? nameProp.stringValue ?? string.Empty : string.Empty;
262
+ string labelText = string.IsNullOrWhiteSpace(currentName)
263
+ ? $"Element {index} (No Name)"
264
+ : currentName;
101
265
 
102
- int lastUnderscoreIndex = frameName.LastIndexOf('_');
103
- int lastIndex =
104
- lastUnderscoreIndex == lastNumericIndex - 1
105
- ? lastUnderscoreIndex
106
- : Math.Max(lastUnderscoreIndex, lastNumericIndex);
107
- if (0 < lastIndex)
108
- {
109
- Dictionary<string, List<Texture2D>> texturesByPrefix =
110
- texturesByPrefixAndAssetPath.GetOrAdd(assetPath);
111
- string key = frameName.Substring(0, lastIndex);
112
- texturesByPrefix.GetOrAdd(key).Add(frame);
113
- }
114
- else
266
+ EditorGUILayout.PropertyField(elementProp, new GUIContent(labelText), true);
267
+ }
268
+ }
269
+ else if (listSize > 0)
270
+ {
271
+ EditorGUILayout.HelpBox(
272
+ $"No animation data matched the search term '{_searchString}'.",
273
+ MessageType.Info
274
+ );
275
+ }
276
+
277
+ EditorGUI.indentLevel--;
278
+ }
279
+ }
280
+
281
+ private void DrawActionButtons()
282
+ {
283
+ DrawCheckSpritesButton();
284
+ EditorGUILayout.Space();
285
+ EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel);
286
+ using (new EditorGUI.DisabledScope(_filteredSprites.Count == 0))
287
+ {
288
+ if (
289
+ GUILayout.Button(
290
+ $"Populate First Slot with {_filteredSprites.Count} Matched Sprites"
291
+ )
292
+ )
293
+ {
294
+ if (animationData.Count == 0)
295
+ {
296
+ this.LogWarn($"Add at least one Animation Data entry first.");
297
+ }
298
+ else if (animationData[0].frames.Count > 0)
299
+ {
300
+ if (
301
+ !EditorUtility.DisplayDialog(
302
+ "Confirm Overwrite",
303
+ "This will replace the frames currently in the first animation slot. Are you sure?",
304
+ "Replace",
305
+ "Cancel"
306
+ )
307
+ )
115
308
  {
116
- this.LogWarn($"Failed to process frame {frameName}.");
309
+ return;
117
310
  }
118
311
  }
312
+ if (animationData.Count > 0)
313
+ {
314
+ animationData[0].frames = new List<Sprite>(_filteredSprites);
315
+ animationData[0].animationName = "All_Matched_Sprites";
316
+ animationData[0].isCreatedFromAutoParse = false;
317
+ _serializedObject.Update();
318
+ Repaint();
319
+ this.Log($"Populated first slot with {_filteredSprites.Count} sprites.");
320
+ }
321
+ }
119
322
 
120
- if (0 < texturesByPrefixAndAssetPath.Count)
323
+ if (GUILayout.Button("Auto-Parse Matched Sprites into Animations"))
324
+ {
325
+ if (
326
+ EditorUtility.DisplayDialog(
327
+ "Confirm Auto-Parse",
328
+ "This will replace the current animation list with animations generated from matched sprites based on their names (e.g., 'Player_Run_0', 'Player_Run_1'). Are you sure?",
329
+ "Parse",
330
+ "Cancel"
331
+ )
332
+ )
121
333
  {
122
- animationData.Clear();
123
- animationData.AddRange(
124
- texturesByPrefixAndAssetPath.SelectMany(assetPathAndTextures =>
125
- assetPathAndTextures.Value.Select(
126
- textureAndPrefix => new AnimationData
127
- {
128
- frames = textureAndPrefix.Value,
129
- framesPerSecond = data.framesPerSecond,
130
- animationName = $"Anim_{textureAndPrefix.Key}",
131
- }
132
- )
133
- )
134
- );
334
+ AutoParseSprites();
335
+ _serializedObject.Update();
336
+ Repaint();
135
337
  }
136
338
  }
137
339
  }
138
340
 
139
- if (
341
+ if (_filteredSprites.Count == 0)
342
+ {
343
+ EditorGUILayout.HelpBox(
344
+ "Cannot perform sprite actions: No sprites matched the filter criteria or sources are empty.",
345
+ MessageType.Info
346
+ );
347
+ }
348
+
349
+ bool canBulkName =
140
350
  animationData is { Count: > 0 }
141
- && animationData.Any(data => data.frames is { Count: > 0 })
142
- && !string.IsNullOrWhiteSpace(text)
143
- )
351
+ && animationData.Any(data => data.frames?.Count > 0)
352
+ && !string.IsNullOrWhiteSpace(text);
353
+
354
+ using (new EditorGUI.DisabledScope(!canBulkName))
144
355
  {
145
- if (GUILayout.Button("Append Text To All Animations"))
356
+ EditorGUILayout.Space();
357
+ EditorGUILayout.LabelField("Bulk Naming Operations", EditorStyles.boldLabel);
358
+
359
+ if (GUILayout.Button($"Append '{text}' To All Animation Names"))
146
360
  {
361
+ bool changed = false;
147
362
  foreach (AnimationData data in animationData)
148
363
  {
149
- data.animationName += "_" + text;
364
+ if (
365
+ !string.IsNullOrWhiteSpace(data.animationName)
366
+ && !data.animationName.EndsWith($"_{text}")
367
+ )
368
+ {
369
+ data.animationName += $"_{text}";
370
+ changed = true;
371
+ }
372
+ }
373
+ if (changed)
374
+ {
375
+ this.Log($"Appended '{text}' to animation names.");
376
+ _serializedObject.Update();
377
+ Repaint();
378
+ }
379
+ else
380
+ {
381
+ this.LogWarn(
382
+ $"No animation names modified. Either none exist or they already end with '_{text}'."
383
+ );
150
384
  }
151
385
  }
152
386
 
153
- if (GUILayout.Button("Remove Text From All Animations"))
387
+ if (GUILayout.Button($"Remove '{text}' From End of Names"))
154
388
  {
389
+ bool changed = false;
390
+ string suffix = $"_{text}";
155
391
  foreach (AnimationData data in animationData)
156
392
  {
157
- if (data.animationName.EndsWith(text))
393
+ if (
394
+ !string.IsNullOrWhiteSpace(data.animationName)
395
+ && data.animationName.EndsWith(suffix)
396
+ )
397
+ {
398
+ data.animationName = data.animationName.Remove(
399
+ data.animationName.Length - suffix.Length
400
+ );
401
+ changed = true;
402
+ }
403
+ else if (
404
+ !string.IsNullOrWhiteSpace(data.animationName)
405
+ && data.animationName.EndsWith(text)
406
+ )
158
407
  {
159
408
  data.animationName = data.animationName.Remove(
160
409
  data.animationName.Length - text.Length
161
410
  );
411
+ changed = true;
162
412
  }
163
413
  }
414
+ if (changed)
415
+ {
416
+ this.Log($"Removed '{text}' suffix from animation names.");
417
+ _serializedObject.Update();
418
+ Repaint();
419
+ }
420
+ else
421
+ {
422
+ this.LogWarn(
423
+ $"No animation names modified. Either none exist or they do not end with '{text}' or '_{text}'."
424
+ );
425
+ }
164
426
  }
165
427
  }
166
428
 
167
- return returnValue;
429
+ if (
430
+ !canBulkName
431
+ && animationData is { Count: > 0 }
432
+ && animationData.Any(data => data.frames?.Count > 0)
433
+ )
434
+ {
435
+ EditorGUILayout.HelpBox(
436
+ "Enter text in the 'Text' field above to enable bulk naming operations.",
437
+ MessageType.Info
438
+ );
439
+ }
440
+
441
+ EditorGUILayout.Space();
442
+ using (new EditorGUI.DisabledScope(animationData == null || animationData.Count == 0))
443
+ {
444
+ if (GUILayout.Button("Create Animations"))
445
+ {
446
+ CreateAnimations();
447
+ }
448
+ }
449
+ if (animationData == null || animationData.Count == 0)
450
+ {
451
+ EditorGUILayout.HelpBox(
452
+ "Add Animation Data entries before creating.",
453
+ MessageType.Warning
454
+ );
455
+ }
168
456
  }
169
457
 
170
- private void OnWizardCreate()
458
+ private void CreateAnimations()
171
459
  {
172
- if (animationData is not { Count: > 0 })
460
+ if (animationData is not { Count: not 0 })
173
461
  {
462
+ this.LogError($"No animation data to create.");
174
463
  return;
175
464
  }
176
465
 
177
- foreach (AnimationData data in animationData)
466
+ string[] searchTerms = string.IsNullOrWhiteSpace(_searchString)
467
+ ? Array.Empty<string>()
468
+ : _searchString
469
+ .ToLowerInvariant()
470
+ .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
471
+
472
+ List<AnimationData> dataToCreate = new();
473
+ if (searchTerms.Length == 0)
178
474
  {
179
- string animationName = data.animationName;
180
- if (string.IsNullOrWhiteSpace(animationName))
475
+ dataToCreate.AddRange(animationData);
476
+ }
477
+ else
478
+ {
479
+ foreach (AnimationData data in animationData)
181
480
  {
182
- this.LogWarn($"Ignoring animationData without an animation name.");
183
- continue;
481
+ string lowerName = (data.animationName ?? string.Empty).ToLowerInvariant();
482
+ if (searchTerms.All(term => lowerName.Contains(term)))
483
+ {
484
+ dataToCreate.Add(data);
485
+ }
184
486
  }
487
+ this.Log(
488
+ $"Creating animations based on current search filter '{_searchString}'. Only {dataToCreate.Count} out of {animationData.Count} items will be processed."
489
+ );
490
+ }
491
+
492
+ if (dataToCreate.Count == 0)
493
+ {
494
+ this.LogError(
495
+ $"No animation data matches the current search filter '{_searchString}'. Nothing to create."
496
+ );
497
+ return;
498
+ }
185
499
 
186
- int framesPerSecond = data.framesPerSecond;
187
- if (framesPerSecond <= 0)
500
+ int totalAnimations = dataToCreate.Count;
501
+ int currentAnimationIndex = 0;
502
+ bool errorOccurred = false;
503
+
504
+ AssetDatabase.StartAssetEditing();
505
+ try
506
+ {
507
+ foreach (AnimationData data in dataToCreate)
188
508
  {
189
- this.LogWarn(
190
- $"Ignoring animationData with FPS of {framesPerSecond} with name {animationName}."
509
+ currentAnimationIndex++;
510
+ string animationName = data.animationName;
511
+ if (string.IsNullOrWhiteSpace(animationName))
512
+ {
513
+ this.LogWarn(
514
+ $"Ignoring animation data entry (original index unknown due to filtering) without an animation name."
515
+ );
516
+ continue;
517
+ }
518
+
519
+ EditorUtility.DisplayProgressBar(
520
+ "Creating Animations",
521
+ $"Processing '{animationName}' ({currentAnimationIndex}/{totalAnimations})",
522
+ (float)currentAnimationIndex / totalAnimations
191
523
  );
192
- continue;
524
+
525
+ int framesPerSecond = data.framesPerSecond;
526
+ if (framesPerSecond <= 0)
527
+ {
528
+ this.LogWarn(
529
+ $"Ignoring animation '{animationName}' with invalid FPS ({framesPerSecond})."
530
+ );
531
+ continue;
532
+ }
533
+
534
+ List<Sprite> frames = data.frames;
535
+ if (frames is not { Count: not 0 })
536
+ {
537
+ this.LogWarn(
538
+ $"Ignoring animation '{animationName}' because it has no frames."
539
+ );
540
+ continue;
541
+ }
542
+
543
+ List<Sprite> validFrames = frames.Where(f => f != null).ToList();
544
+ if (validFrames.Count == 0)
545
+ {
546
+ this.LogWarn(
547
+ $"Ignoring animation '{animationName}' because it only contains null frames."
548
+ );
549
+ continue;
550
+ }
551
+
552
+ validFrames.Sort((s1, s2) => EditorUtility.NaturalCompare(s1.name, s2.name));
553
+
554
+ List<ObjectReferenceKeyframe> keyFrames = new(validFrames.Count);
555
+ float timeStep = 1f / framesPerSecond;
556
+ float currentTime = 0f;
557
+
558
+ foreach (
559
+ ObjectReferenceKeyframe keyFrame in validFrames.Select(
560
+ sprite => new ObjectReferenceKeyframe
561
+ {
562
+ time = currentTime,
563
+ value = sprite,
564
+ }
565
+ )
566
+ )
567
+ {
568
+ keyFrames.Add(keyFrame);
569
+ currentTime += timeStep;
570
+ }
571
+
572
+ if (keyFrames.Count <= 0)
573
+ {
574
+ this.LogWarn(
575
+ $"No valid keyframes could be generated for animation '{animationName}'."
576
+ );
577
+ continue;
578
+ }
579
+
580
+ AnimationClip animationClip = new() { frameRate = framesPerSecond };
581
+
582
+ if (data.loop)
583
+ {
584
+ AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(
585
+ animationClip
586
+ );
587
+ settings.loopTime = true;
588
+ AnimationUtility.SetAnimationClipSettings(animationClip, settings);
589
+ }
590
+
591
+ AnimationUtility.SetObjectReferenceCurve(
592
+ animationClip,
593
+ EditorCurveBinding.PPtrCurve("", typeof(SpriteRenderer), "m_Sprite"),
594
+ keyFrames.ToArray()
595
+ );
596
+
597
+ string firstFramePath = AssetDatabase.GetAssetPath(validFrames[0]);
598
+ string assetPath =
599
+ Path.GetDirectoryName(firstFramePath)?.Replace("\\", "/") ?? "Assets";
600
+ if (!assetPath.EndsWith("/"))
601
+ {
602
+ assetPath += "/";
603
+ }
604
+
605
+ string finalPath = AssetDatabase.GenerateUniqueAssetPath(
606
+ $"{assetPath}{animationName}.anim"
607
+ );
608
+ AssetDatabase.CreateAsset(animationClip, finalPath);
609
+ this.Log($"Created animation at '{finalPath}'.");
610
+ }
611
+ }
612
+ catch (Exception e)
613
+ {
614
+ errorOccurred = true;
615
+ this.LogError($"An error occurred during animation creation: {e}");
616
+ }
617
+ finally
618
+ {
619
+ EditorUtility.ClearProgressBar();
620
+ if (!errorOccurred)
621
+ {
622
+ this.Log($"Finished creating {totalAnimations} animations.");
193
623
  }
624
+ else
625
+ {
626
+ this.LogError($"Animation creation finished with errors. Check console.");
627
+ }
628
+
629
+ AssetDatabase.StopAssetEditing();
630
+ AssetDatabase.SaveAssets();
631
+ AssetDatabase.Refresh();
632
+ }
633
+ }
194
634
 
195
- List<Texture2D> frames = data.frames;
196
- if (frames is not { Count: > 0 })
635
+ private void UpdateRegex()
636
+ {
637
+ if (_compiledRegex == null || _lastUsedRegex != spriteNameRegex)
638
+ {
639
+ try
640
+ {
641
+ _compiledRegex = new Regex(spriteNameRegex, RegexOptions.Compiled);
642
+ _lastUsedRegex = spriteNameRegex;
643
+ _errorMessage = "";
644
+ this.Log($"Regex updated to: {spriteNameRegex}");
645
+ }
646
+ catch (ArgumentException ex)
647
+ {
648
+ _compiledRegex = null;
649
+ _lastUsedRegex = spriteNameRegex;
650
+ _errorMessage = $"Invalid Regex: {ex.Message}";
651
+ this.LogError($"Invalid Regex '{spriteNameRegex}': {ex.Message}");
652
+ }
653
+ }
654
+ }
655
+
656
+ private void FindAndFilterSprites()
657
+ {
658
+ _filteredSprites.Clear();
659
+ _matchedSpriteCount = 0;
660
+ _unmatchedSpriteCount = 0;
661
+
662
+ if (animationSources is not { Count: not 0 } || _compiledRegex == null)
663
+ {
664
+ if (_compiledRegex == null && !string.IsNullOrWhiteSpace(spriteNameRegex))
197
665
  {
198
666
  this.LogWarn(
199
- $"Ignoring animationData without frames with name {animationName}."
667
+ $"Cannot find sprites, regex pattern '{spriteNameRegex}' is invalid."
200
668
  );
669
+ }
670
+ else if (animationSources is not { Count: not 0 })
671
+ {
672
+ this.LogWarn($"Cannot find sprites, no animation sources specified.");
673
+ }
674
+ return;
675
+ }
676
+
677
+ List<string> searchPaths = new();
678
+ foreach (Object source in animationSources)
679
+ {
680
+ if (source == null)
681
+ {
201
682
  continue;
202
683
  }
203
684
 
204
- List<ObjectReferenceKeyframe> keyFrames = new(frames.Count);
685
+ string path = AssetDatabase.GetAssetPath(source);
686
+ if (!string.IsNullOrWhiteSpace(path) && AssetDatabase.IsValidFolder(path))
687
+ {
688
+ searchPaths.Add(path);
689
+ }
690
+ else if (source != null)
691
+ {
692
+ this.LogWarn($"Source '{source.name}' is not a valid folder. Skipping.");
693
+ }
694
+ }
695
+
696
+ if (searchPaths.Count == 0)
697
+ {
698
+ this.LogWarn($"No valid folders found in Animation Sources.");
699
+ return;
700
+ }
701
+
702
+ string[] assetGuids = AssetDatabase.FindAssets("t:sprite", searchPaths.ToArray());
703
+ float totalAssets = assetGuids.Length;
704
+ this.Log($"Found {totalAssets} total sprite assets in specified paths.");
705
+
706
+ try
707
+ {
708
+ EditorUtility.DisplayProgressBar(
709
+ "Finding and Filtering Sprites",
710
+ $"Scanning {assetGuids.Length} assets...",
711
+ 0f
712
+ );
205
713
 
206
- float currentTime = 0f;
207
- float timeStep = 1f / framesPerSecond;
208
- foreach (Texture2D frame in frames)
714
+ for (int i = 0; i < totalAssets; i++)
209
715
  {
210
- Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(
211
- AssetDatabase.GetAssetPath(frame)
212
- );
213
- if (sprite == null)
716
+ string guid = assetGuids[i];
717
+ string path = AssetDatabase.GUIDToAssetPath(guid);
718
+
719
+ if (i % 20 == 0 || Mathf.Approximately(i, totalAssets - 1))
214
720
  {
215
- continue;
721
+ float progress = (i + 1) / totalAssets;
722
+ EditorUtility.DisplayProgressBar(
723
+ "Finding and Filtering Sprites",
724
+ $"Checking: {Path.GetFileName(path)} ({i + 1}/{assetGuids.Length})",
725
+ progress
726
+ );
216
727
  }
217
728
 
218
- ObjectReferenceKeyframe keyFrame = new() { time = currentTime, value = sprite };
219
- keyFrames.Add(keyFrame);
729
+ Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
220
730
 
221
- currentTime += timeStep;
731
+ if (sprite != null)
732
+ {
733
+ if (_compiledRegex.IsMatch(sprite.name))
734
+ {
735
+ _filteredSprites.Add(sprite);
736
+ _matchedSpriteCount++;
737
+ }
738
+ else
739
+ {
740
+ _unmatchedSpriteCount++;
741
+ }
742
+ }
743
+ }
744
+ this.Log(
745
+ $"Sprite filtering complete. Matched: {_matchedSpriteCount}, Unmatched: {_unmatchedSpriteCount}."
746
+ );
747
+ }
748
+ finally
749
+ {
750
+ EditorUtility.ClearProgressBar();
751
+ }
752
+ }
753
+
754
+ private void AutoParseSprites()
755
+ {
756
+ if (_filteredSprites.Count == 0)
757
+ {
758
+ this.LogWarn($"Cannot Auto-Parse, no matched sprites available.");
759
+ return;
760
+ }
761
+
762
+ Dictionary<string, Dictionary<string, List<Sprite>>> spritesByPrefixAndAssetPath = new(
763
+ StringComparer.Ordinal
764
+ );
765
+ int totalSprites = _filteredSprites.Count;
766
+ int processedCount = 0;
767
+ this.Log($"Starting auto-parse for {_filteredSprites.Count} matched sprites.");
768
+
769
+ try
770
+ {
771
+ foreach (Sprite sprite in _filteredSprites)
772
+ {
773
+ processedCount++;
774
+ if (processedCount % 10 == 0 || processedCount == totalSprites)
775
+ {
776
+ EditorUtility.DisplayProgressBar(
777
+ "Auto-Parsing Sprites",
778
+ $"Processing: {sprite.name} ({processedCount}/{totalSprites})",
779
+ (float)processedCount / totalSprites
780
+ );
781
+ }
782
+
783
+ string assetPath = AssetDatabase.GetAssetPath(sprite);
784
+ string directoryPath =
785
+ Path.GetDirectoryName(assetPath)?.Replace("\\", "/") ?? "";
786
+ string frameName = sprite.name;
787
+
788
+ int splitIndex = frameName.LastIndexOf('_');
789
+ string prefix = frameName;
790
+
791
+ if (splitIndex > 0 && splitIndex < frameName.Length - 1)
792
+ {
793
+ bool allDigitsAfter = true;
794
+ for (int j = splitIndex + 1; j < frameName.Length; j++)
795
+ {
796
+ if (!char.IsDigit(frameName[j]))
797
+ {
798
+ allDigitsAfter = false;
799
+ break;
800
+ }
801
+ }
802
+
803
+ if (allDigitsAfter)
804
+ {
805
+ prefix = frameName.Substring(0, splitIndex);
806
+ }
807
+ else
808
+ {
809
+ prefix = frameName;
810
+ this.LogWarn(
811
+ $"Sprite name '{frameName}' has an underscore but not only digits after the last one. Treating as single frame or check naming."
812
+ );
813
+ }
814
+ }
815
+ else if (splitIndex == -1)
816
+ {
817
+ prefix = frameName;
818
+ this.LogWarn(
819
+ $"Sprite name '{frameName}' has no underscore. Treating as single frame or check naming."
820
+ );
821
+ }
822
+
823
+ if (!string.IsNullOrWhiteSpace(prefix))
824
+ {
825
+ if (
826
+ !spritesByPrefixAndAssetPath.TryGetValue(
827
+ directoryPath,
828
+ out Dictionary<string, List<Sprite>> spritesByPrefix
829
+ )
830
+ )
831
+ {
832
+ spritesByPrefix = new Dictionary<string, List<Sprite>>(
833
+ StringComparer.Ordinal
834
+ );
835
+ spritesByPrefixAndAssetPath.Add(directoryPath, spritesByPrefix);
836
+ }
837
+
838
+ spritesByPrefix.GetOrAdd(prefix).Add(sprite);
839
+ }
840
+ else
841
+ {
842
+ this.LogWarn(
843
+ $"Could not extract valid prefix for frame '{frameName}' at path '{assetPath}'. Skipping."
844
+ );
845
+ }
222
846
  }
223
847
 
224
- if (keyFrames.Count <= 0)
848
+ if (spritesByPrefixAndAssetPath.Count > 0)
849
+ {
850
+ int removedCount = animationData.RemoveAll(data => data.isCreatedFromAutoParse);
851
+ this.Log($"Removed {removedCount} previously auto-parsed animation entries.");
852
+
853
+ int addedCount = 0;
854
+ foreach (
855
+ KeyValuePair<
856
+ string,
857
+ Dictionary<string, List<Sprite>>
858
+ > kvpAssetPath in spritesByPrefixAndAssetPath
859
+ )
860
+ {
861
+ string dirName = new DirectoryInfo(kvpAssetPath.Key).Name;
862
+
863
+ foreach ((string key, List<Sprite> spritesInGroup) in kvpAssetPath.Value)
864
+ {
865
+ if (spritesInGroup.Count == 0)
866
+ {
867
+ continue;
868
+ }
869
+
870
+ spritesInGroup.Sort(
871
+ (s1, s2) => EditorUtility.NaturalCompare(s1.name, s2.name)
872
+ );
873
+
874
+ string finalAnimName;
875
+
876
+ bool keyIsLikelyFullName =
877
+ spritesInGroup.Count > 0 && key == spritesInGroup[0].name;
878
+
879
+ if (keyIsLikelyFullName)
880
+ {
881
+ int lastUnderscore = key.LastIndexOf('_');
882
+
883
+ if (lastUnderscore > 0 && lastUnderscore < key.Length - 1)
884
+ {
885
+ string suffix = key.Substring(lastUnderscore + 1);
886
+ finalAnimName = SanitizeName($"Anim_{suffix}");
887
+ this.Log(
888
+ $"Naming non-standard sprite group '{key}' as '{finalAnimName}' using suffix '{suffix}'."
889
+ );
890
+ }
891
+ else
892
+ {
893
+ finalAnimName = SanitizeName($"Anim_{key}");
894
+ this.LogWarn(
895
+ $"Naming non-standard sprite group '{key}' as '{finalAnimName}'. Could not extract suffix."
896
+ );
897
+ }
898
+ }
899
+ else
900
+ {
901
+ finalAnimName = SanitizeName($"Anim_{key}");
902
+ this.Log(
903
+ $"Naming standard sprite group '{key}' as '{finalAnimName}'."
904
+ );
905
+ }
906
+
907
+ animationData.Add(
908
+ new AnimationData
909
+ {
910
+ frames = spritesInGroup,
911
+ framesPerSecond = AnimationData.DefaultFramesPerSecond,
912
+ animationName = finalAnimName,
913
+ isCreatedFromAutoParse = true,
914
+ loop = false,
915
+ }
916
+ );
917
+ addedCount++;
918
+ }
919
+ }
920
+
921
+ this.Log($"Auto-parsed into {addedCount} new animation groups.");
922
+ }
923
+ else
225
924
  {
226
925
  this.LogWarn(
227
- $"Ignoring animationData with empty frames with name {animationName}."
926
+ $"Auto-parsing did not result in any animation groups. Check sprite naming conventions (e.g., 'Prefix_0', 'Prefix_1')."
228
927
  );
229
- continue;
230
928
  }
929
+ }
930
+ finally
931
+ {
932
+ EditorUtility.ClearProgressBar();
933
+ _serializedObject.Update();
934
+ }
935
+ }
231
936
 
232
- AnimationClip animationClip = new();
233
- AnimationUtility.SetObjectReferenceCurve(
234
- animationClip,
235
- EditorCurveBinding.PPtrCurve("", typeof(SpriteRenderer), "m_Sprite"),
236
- keyFrames.ToArray()
237
- );
238
- string assetPathWithFileNameAndExtension = AssetDatabase.GetAssetPath(frames[0]);
239
- string assetPath = assetPathWithFileNameAndExtension.Substring(
240
- 0,
241
- assetPathWithFileNameAndExtension.LastIndexOf("/", StringComparison.Ordinal) + 1
242
- );
937
+ private static string SanitizeName(string inputName)
938
+ {
939
+ inputName = inputName.Replace(" ", "_");
940
+ inputName = Regex.Replace(inputName, @"[^a-zA-Z0-9_]", "");
243
941
 
244
- ProjectWindowUtil.CreateAsset(animationClip, assetPath + animationName + ".anim");
942
+ if (string.IsNullOrWhiteSpace(inputName))
943
+ {
944
+ return "Default_Animation";
245
945
  }
946
+
947
+ return inputName.Trim('_');
246
948
  }
247
949
  }
950
+
248
951
  #endif
249
952
  }