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
@@ -2,172 +2,954 @@
2
2
  {
3
3
  #if UNITY_EDITOR
4
4
  using System;
5
+ using System.Collections.Generic;
5
6
  using System.IO;
7
+ using System.Linq;
8
+ using System.Security.Cryptography;
6
9
  using UnityEditor;
7
10
  using UnityEngine;
8
- using Utils;
9
- using WallstopStudios.UnityHelpers.Core.Attributes;
10
- using WallstopStudios.UnityHelpers.Core.Extension;
11
+ using Core.Extension;
11
12
 
12
- public sealed class AnimationCopier : ScriptableWizard
13
+ public sealed class AnimationCopierWindow : EditorWindow
13
14
  {
14
- private string _fullSourcePath;
15
- private string _fullDestinationPath;
15
+ private const string SourcePathPrefKey = "AnimationCopier_SourcePathRelative";
16
+ private const string DestPathPrefKey = "AnimationCopier_DestPathRelative";
17
+ private const string DefaultSourcePath = "Assets/Sprites";
18
+ private const string DefaultDestPath = "Assets/Animations";
16
19
 
17
- [DxReadOnly]
18
- public string animationSourcePath;
20
+ private string _animationSourcePathRelative = "";
21
+ private string _animationDestinationPathRelative = "";
22
+ private string _fullSourcePath = "";
23
+ private string _fullDestinationPath = "";
19
24
 
20
- [DxReadOnly]
21
- public string animationDestinationPath;
25
+ private bool _analysisNeeded = true;
26
+ private bool _isAnalyzing;
27
+ private bool _isCopying;
28
+ private bool _isDeleting;
29
+
30
+ private readonly List<AnimationFileInfo> _sourceAnimations = new();
31
+ private readonly List<AnimationFileInfo> _newAnimations = new();
32
+ private readonly List<AnimationFileInfo> _changedAnimations = new();
33
+ private readonly List<AnimationFileInfo> _unchangedAnimations = new();
34
+
35
+ private enum AnimationStatus
36
+ {
37
+ Unknown,
38
+ New,
39
+ Changed,
40
+ Unchanged,
41
+ }
42
+
43
+ private enum CopyMode
44
+ {
45
+ All,
46
+ Changed,
47
+ New,
48
+ }
49
+
50
+ private class AnimationFileInfo
51
+ {
52
+ public string RelativePath { get; set; }
53
+ public string FullPath { get; set; }
54
+ public string FileName { get; set; }
55
+ public string RelativeDirectory { get; set; }
56
+ public string Hash { get; set; }
57
+ public AnimationStatus Status { get; set; } = AnimationStatus.Unknown;
58
+ public string DestinationRelativePath { get; set; }
59
+ }
60
+
61
+ [MenuItem("Tools/Wallstop Studios/Unity Helpers/Animation Copier Window", priority = -2)]
62
+ public static void ShowWindow()
63
+ {
64
+ GetWindow<AnimationCopierWindow>("Animation Copier");
65
+ }
22
66
 
23
67
  private void OnEnable()
24
68
  {
25
- if (string.IsNullOrWhiteSpace(_fullSourcePath))
69
+ LoadPaths();
70
+ ValidatePaths();
71
+ _analysisNeeded = true;
72
+ this.Log($"Animation Copier Window opened.");
73
+ }
74
+
75
+ private void OnGUI()
76
+ {
77
+ bool operationInProgress = _isAnalyzing || _isCopying || _isDeleting;
78
+
79
+ if (operationInProgress)
80
+ {
81
+ string status =
82
+ _isAnalyzing ? "Analyzing..."
83
+ : _isCopying ? "Copying..."
84
+ : "Deleting...";
85
+ EditorGUILayout.LabelField(status, EditorStyles.centeredGreyMiniLabel);
86
+ }
87
+
88
+ EditorGUI.BeginDisabledGroup(operationInProgress);
89
+
90
+ DrawPathSection(
91
+ "Source Path",
92
+ ref _animationSourcePathRelative,
93
+ ref _fullSourcePath,
94
+ SourcePathPrefKey
95
+ );
96
+ EditorGUILayout.Separator();
97
+ DrawPathSection(
98
+ "Destination Path",
99
+ ref _animationDestinationPathRelative,
100
+ ref _fullDestinationPath,
101
+ DestPathPrefKey
102
+ );
103
+ EditorGUILayout.Separator();
104
+
105
+ DrawAnalysisSection();
106
+ EditorGUILayout.Separator();
107
+ DrawCopySection();
108
+ EditorGUILayout.Separator();
109
+ DrawCleanupSection();
110
+
111
+ EditorGUI.EndDisabledGroup();
112
+
113
+ if (!operationInProgress && _analysisNeeded && Event.current.type == EventType.Layout)
26
114
  {
27
- _fullSourcePath = $"{Application.dataPath}/Sprites";
28
- int assetIndex = _fullSourcePath.IndexOf("Assets", StringComparison.Ordinal);
29
- if (0 <= assetIndex)
115
+ if (ArePathsValid())
116
+ {
117
+ AnalyzeAnimations();
118
+ }
119
+ else
30
120
  {
31
- animationSourcePath = _fullSourcePath.Substring(assetIndex);
121
+ ClearAnalysisResults();
32
122
  }
123
+ _analysisNeeded = false;
124
+ Repaint();
125
+ }
126
+ }
127
+
128
+ private void DrawPathSection(
129
+ string label,
130
+ ref string relativePath,
131
+ ref string fullPath,
132
+ string prefKey
133
+ )
134
+ {
135
+ if (!prefKey.StartsWith("WallstopStudios.UnityHelpers.Editor"))
136
+ {
137
+ prefKey = "WallstopStudios.UnityHelpers.Editor" + prefKey;
33
138
  }
139
+ EditorGUILayout.LabelField(label + ":", EditorStyles.boldLabel);
34
140
 
35
- if (string.IsNullOrWhiteSpace(_fullDestinationPath))
141
+ EditorGUI.BeginChangeCheck();
142
+ string newRelativePath = EditorGUILayout.TextField(relativePath ?? "");
143
+ if (EditorGUI.EndChangeCheck() && newRelativePath != relativePath)
36
144
  {
37
- _fullDestinationPath = $"{Application.dataPath}/Animations";
38
- int assetIndex = _fullDestinationPath.IndexOf("Assets", StringComparison.Ordinal);
39
- if (0 <= assetIndex)
145
+ if (string.IsNullOrWhiteSpace(newRelativePath))
146
+ {
147
+ relativePath = "";
148
+ fullPath = "";
149
+ EditorPrefs.SetString(prefKey, "");
150
+ ValidatePaths();
151
+ _analysisNeeded = true;
152
+ }
153
+ else
40
154
  {
41
- animationDestinationPath = _fullDestinationPath.Substring(assetIndex);
155
+ string tempFullPath = GetFullPathFromRelative(newRelativePath);
156
+ if (tempFullPath != null && Directory.Exists(tempFullPath))
157
+ {
158
+ relativePath = newRelativePath;
159
+ fullPath = tempFullPath;
160
+ EditorPrefs.SetString(prefKey, relativePath);
161
+ ValidatePaths();
162
+ _analysisNeeded = true;
163
+ }
164
+ else
165
+ {
166
+ this.LogWarn(
167
+ $"Manual path entry '{newRelativePath}' is invalid or not inside Assets. Please use the button."
168
+ );
169
+ }
170
+ }
171
+ }
172
+
173
+ if (GUILayout.Button("Browse..."))
174
+ {
175
+ string initialPath = Directory.Exists(fullPath) ? fullPath : Application.dataPath;
176
+ string selectedPath = EditorUtility.OpenFolderPanel(
177
+ $"Select {label}",
178
+ initialPath,
179
+ string.Empty
180
+ );
181
+
182
+ if (!string.IsNullOrWhiteSpace(selectedPath))
183
+ {
184
+ string newRelPath = GetRelativeAssetPath(selectedPath);
185
+ if (newRelPath != null)
186
+ {
187
+ relativePath = newRelPath;
188
+ fullPath = selectedPath.Replace(Path.DirectorySeparatorChar, '/');
189
+ EditorPrefs.SetString(prefKey, relativePath);
190
+ this.Log($"{label} set to: {relativePath}");
191
+ ValidatePaths();
192
+ _analysisNeeded = true;
193
+ Repaint();
194
+ }
195
+ else
196
+ {
197
+ EditorUtility.DisplayDialog(
198
+ "Invalid Path",
199
+ "The selected path must be inside the project's 'Assets' folder.",
200
+ "OK"
201
+ );
202
+ }
42
203
  }
43
204
  }
44
205
  }
45
206
 
46
- [MenuItem("Tools/Unity Helpers/Animation Copier", priority = -2)]
47
- public static void CopyAnimations()
207
+ private void DrawAnalysisSection()
48
208
  {
49
- _ = DisplayWizard<AnimationCopier>("Animation Copier", "Copy");
209
+ EditorGUILayout.LabelField("Analysis:", EditorStyles.boldLabel);
210
+
211
+ if (GUILayout.Button("Analyze Source & Destination"))
212
+ {
213
+ if (ArePathsValid())
214
+ {
215
+ AnalyzeAnimations();
216
+ }
217
+ else
218
+ {
219
+ EditorUtility.DisplayDialog(
220
+ "Error",
221
+ "Source or Destination path is not set or invalid.",
222
+ "OK"
223
+ );
224
+ }
225
+ }
226
+
227
+ EditorGUILayout.Space();
228
+
229
+ EditorGUILayout.LabelField(
230
+ "Source Animations Found:",
231
+ _sourceAnimations.Count.ToString()
232
+ );
233
+ EditorGUILayout.LabelField("- New:", _newAnimations.Count.ToString());
234
+ EditorGUILayout.LabelField("- Changed:", _changedAnimations.Count.ToString());
235
+ EditorGUILayout.LabelField(
236
+ "- Unchanged (Duplicates):",
237
+ _unchangedAnimations.Count.ToString()
238
+ );
50
239
  }
51
240
 
52
- protected override bool DrawWizardGUI()
241
+ private void DrawCopySection()
53
242
  {
54
- bool returnValue = base.DrawWizardGUI();
243
+ EditorGUILayout.LabelField("Copy Actions:", EditorStyles.boldLabel);
244
+
245
+ bool canAnalyze = ArePathsValid();
246
+ bool analysisDone = !_analysisNeeded;
55
247
 
56
- if (GUILayout.Button("Set Animation Source Path"))
248
+ bool canCopyNew = canAnalyze && analysisDone && _newAnimations.Any();
249
+ bool canCopyChanged = canAnalyze && analysisDone && _changedAnimations.Any();
250
+ bool canCopyAll = canAnalyze && analysisDone && _sourceAnimations.Any();
251
+
252
+ EditorGUI.BeginDisabledGroup(!canCopyNew);
253
+ if (GUILayout.Button($"Copy New ({_newAnimations.Count})"))
57
254
  {
58
- string sourcePath = EditorUtility.OpenFolderPanel(
59
- "Select Animation Source Path",
60
- EditorUtilities.GetCurrentPathOfProjectWindow(),
61
- string.Empty
62
- );
63
- int assetIndex = sourcePath?.IndexOf("Assets", StringComparison.Ordinal) ?? -1;
64
- if (assetIndex < 0)
255
+ if (
256
+ EditorUtility.DisplayDialog(
257
+ "Confirm Copy New",
258
+ $"Copy {_newAnimations.Count} new animations from '{_animationSourcePathRelative}' to '{_animationDestinationPathRelative}'?",
259
+ "Yes, Copy New",
260
+ "Cancel"
261
+ )
262
+ )
65
263
  {
66
- return false;
264
+ CopyAnimationsInternal(CopyMode.New);
67
265
  }
266
+ }
267
+ EditorGUI.EndDisabledGroup();
68
268
 
69
- _fullSourcePath = animationSourcePath = sourcePath ?? string.Empty;
70
- animationSourcePath = animationSourcePath.Substring(assetIndex);
71
- return true;
269
+ EditorGUI.BeginDisabledGroup(!canCopyChanged);
270
+ if (GUILayout.Button($"Copy Changed ({_changedAnimations.Count})"))
271
+ {
272
+ if (
273
+ EditorUtility.DisplayDialog(
274
+ "Confirm Copy Changed",
275
+ $"Copy {_changedAnimations.Count} changed animations from '{_animationSourcePathRelative}' to '{_animationDestinationPathRelative}', overwriting existing files?",
276
+ "Yes, Copy Changed",
277
+ "Cancel"
278
+ )
279
+ )
280
+ {
281
+ CopyAnimationsInternal(CopyMode.Changed);
282
+ }
72
283
  }
284
+ EditorGUI.EndDisabledGroup();
73
285
 
74
- if (GUILayout.Button("Set Animation Destination Path"))
286
+ int totalToCopyAll =
287
+ _newAnimations.Count + _changedAnimations.Count + _unchangedAnimations.Count;
288
+ EditorGUI.BeginDisabledGroup(!canCopyAll);
289
+ if (GUILayout.Button($"Copy All ({totalToCopyAll})"))
75
290
  {
76
- string sourcePath = EditorUtility.OpenFolderPanel(
77
- "Select Animation Destination Path",
78
- EditorUtilities.GetCurrentPathOfProjectWindow(),
79
- string.Empty
80
- );
81
- int assetIndex = sourcePath?.IndexOf("Assets", StringComparison.Ordinal) ?? -1;
82
- if (assetIndex < 0)
291
+ string overwriteWarning =
292
+ _changedAnimations.Count + _unchangedAnimations.Count > 0
293
+ ? $" This will overwrite {_changedAnimations.Count + _unchangedAnimations.Count} existing files."
294
+ : "";
295
+ if (
296
+ EditorUtility.DisplayDialog(
297
+ "Confirm Copy All",
298
+ $"Copy {totalToCopyAll} animations from '{_animationSourcePathRelative}' to '{_animationDestinationPathRelative}'?{overwriteWarning}",
299
+ "Yes, Copy All",
300
+ "Cancel"
301
+ )
302
+ )
83
303
  {
84
- return false;
304
+ CopyAnimationsInternal(CopyMode.All);
85
305
  }
306
+ }
307
+ EditorGUI.EndDisabledGroup();
308
+ }
309
+
310
+ private void DrawCleanupSection()
311
+ {
312
+ EditorGUILayout.LabelField("Cleanup Actions:", EditorStyles.boldLabel);
86
313
 
87
- _fullDestinationPath = animationDestinationPath = sourcePath ?? string.Empty;
88
- animationDestinationPath = animationDestinationPath.Substring(assetIndex);
89
- return true;
314
+ bool canAnalyze = ArePathsValid();
315
+ bool analysisDone = !_analysisNeeded;
316
+ bool hasUnchanged = _unchangedAnimations.Any();
317
+
318
+ if (canAnalyze && analysisDone && hasUnchanged)
319
+ {
320
+ Color originalColor = GUI.color;
321
+ GUI.color = Color.red;
322
+
323
+ string buttonText =
324
+ $"Delete {_unchangedAnimations.Count} Unchanged Source Duplicates";
325
+
326
+ if (GUILayout.Button(buttonText))
327
+ {
328
+ DeleteUnchangedSourceAnimations();
329
+ }
330
+
331
+ GUI.color = originalColor;
90
332
  }
333
+ else
334
+ {
335
+ EditorGUI.BeginDisabledGroup(true);
336
+ GUILayout.Button("Delete Unchanged Source Duplicates (None found)");
337
+ EditorGUI.EndDisabledGroup();
338
+ }
339
+ }
91
340
 
92
- return returnValue;
341
+ private void LoadPaths()
342
+ {
343
+ _animationSourcePathRelative = EditorPrefs.GetString(
344
+ SourcePathPrefKey,
345
+ DefaultSourcePath
346
+ );
347
+ _animationDestinationPathRelative = EditorPrefs.GetString(
348
+ DestPathPrefKey,
349
+ DefaultDestPath
350
+ );
93
351
  }
94
352
 
95
- private void OnWizardCreate()
353
+ private void ValidatePaths()
96
354
  {
97
- if (string.IsNullOrEmpty(_fullSourcePath) || string.IsNullOrEmpty(_fullDestinationPath))
355
+ _fullSourcePath = GetFullPathFromRelative(_animationSourcePathRelative);
356
+ _fullDestinationPath = GetFullPathFromRelative(_animationDestinationPathRelative);
357
+
358
+ if (_fullSourcePath == null || !Directory.Exists(_fullSourcePath))
359
+ {
360
+ this.LogWarn(
361
+ $"Source path '{_animationSourcePathRelative}' is invalid or outside the project. Please set a valid path within Assets."
362
+ );
363
+ _fullSourcePath = null;
364
+ _analysisNeeded = true;
365
+ ClearAnalysisResults();
366
+ }
367
+ if (_fullDestinationPath == null)
98
368
  {
369
+ this.LogWarn(
370
+ $"Destination path '{_animationDestinationPathRelative}' is invalid or outside the project. Please set a valid path within Assets."
371
+ );
372
+ _analysisNeeded = true;
373
+ ClearAnalysisResults();
374
+ }
375
+ else
376
+ {
377
+ string parentDir = Path.GetDirectoryName(_fullDestinationPath);
378
+ if (!Directory.Exists(parentDir))
379
+ {
380
+ this.LogWarn(
381
+ $"The parent directory for the destination path '{_animationDestinationPathRelative}' does not exist ('{parentDir}'). Copy operations may fail to create folders."
382
+ );
383
+ }
384
+ }
385
+ }
386
+
387
+ private bool ArePathsValid()
388
+ {
389
+ return !string.IsNullOrWhiteSpace(_animationSourcePathRelative)
390
+ && !string.IsNullOrWhiteSpace(_animationDestinationPathRelative)
391
+ && _fullSourcePath != null
392
+ && _fullDestinationPath != null;
393
+ }
394
+
395
+ private void AnalyzeAnimations()
396
+ {
397
+ if (!ArePathsValid())
398
+ {
399
+ this.LogError($"Cannot analyze: Paths are invalid.");
400
+ ClearAnalysisResults();
401
+ _analysisNeeded = false;
402
+ Repaint();
99
403
  return;
100
404
  }
101
405
 
102
- if (
103
- string.IsNullOrEmpty(animationSourcePath)
104
- || string.IsNullOrEmpty(animationDestinationPath)
105
- )
406
+ if (_isAnalyzing || _isCopying || _isDeleting)
106
407
  {
107
408
  return;
108
409
  }
109
410
 
110
- int processed = 0;
111
- foreach (
112
- string assetGuid in AssetDatabase.FindAssets(
113
- "t:AnimationClip",
114
- new[] { animationSourcePath }
115
- )
116
- )
411
+ this.Log($"Starting animation analysis...");
412
+ _isAnalyzing = true;
413
+ ClearAnalysisResults();
414
+ Repaint();
415
+
416
+ try
117
417
  {
118
- string path = AssetDatabase.GUIDToAssetPath(assetGuid);
418
+ string[] sourceGuids = AssetDatabase.FindAssets(
419
+ "t:AnimationClip",
420
+ new[] { _animationSourcePathRelative }
421
+ );
422
+ _sourceAnimations.Clear();
423
+
424
+ float total = sourceGuids.Length * 2;
425
+ int current = 0;
119
426
 
120
- AnimationClip animationClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
121
- if (animationClip == null)
427
+ EditorUtility.DisplayProgressBar(
428
+ "Analyzing Animations",
429
+ "Gathering source files...",
430
+ 0f
431
+ );
432
+
433
+ foreach (string guid in sourceGuids)
122
434
  {
123
- this.LogError(
124
- $"Invalid AnimationClip (null) found at path '{path}', skipping."
435
+ current++;
436
+ string sourceRelPath = AssetDatabase.GUIDToAssetPath(guid);
437
+ if (
438
+ string.IsNullOrWhiteSpace(sourceRelPath)
439
+ || !sourceRelPath.StartsWith(_animationSourcePathRelative)
440
+ )
441
+ {
442
+ continue;
443
+ }
444
+
445
+ string sourceFullPath = GetFullPathFromRelative(sourceRelPath);
446
+ if (sourceFullPath == null || !File.Exists(sourceFullPath))
447
+ {
448
+ continue;
449
+ }
450
+
451
+ string directoryName = Path.GetDirectoryName(sourceRelPath);
452
+ if (string.IsNullOrWhiteSpace(directoryName))
453
+ {
454
+ continue;
455
+ }
456
+ AnimationFileInfo fileInfo = new()
457
+ {
458
+ RelativePath = sourceRelPath,
459
+ FullPath = sourceFullPath,
460
+ FileName = Path.GetFileName(sourceRelPath),
461
+ RelativeDirectory = GetRelativeSubPath(
462
+ _animationSourcePathRelative,
463
+ directoryName.Replace(Path.DirectorySeparatorChar, '/')
464
+ ),
465
+ Hash = CalculateFileHash(sourceFullPath),
466
+ };
467
+ fileInfo.DestinationRelativePath = Path.Combine(
468
+ _animationDestinationPathRelative,
469
+ fileInfo.RelativeDirectory,
470
+ fileInfo.FileName
471
+ )
472
+ .Replace(Path.DirectorySeparatorChar, '/');
473
+ _sourceAnimations.Add(fileInfo);
474
+
475
+ EditorUtility.DisplayProgressBar(
476
+ "Analyzing Animations",
477
+ $"Hashing: {fileInfo.FileName}",
478
+ current / total
125
479
  );
126
- continue;
127
480
  }
128
481
 
129
- string prefix = animationClip.name;
130
- string relativePath = path.Substring(animationSourcePath.Length);
131
- int prefixIndex = relativePath.LastIndexOf(
132
- prefix,
133
- StringComparison.OrdinalIgnoreCase
482
+ this.Log(
483
+ $"Found {_sourceAnimations.Count} animations in source. Comparing with destination..."
134
484
  );
135
- if (prefixIndex < 0)
485
+
486
+ for (int i = 0; i < _sourceAnimations.Count; i++)
136
487
  {
137
- this.LogWarn(
138
- $"Unsupported animation at '{path}', expected to be prefixed by '{prefix}'."
488
+ AnimationFileInfo sourceInfo = _sourceAnimations[i];
489
+ current++;
490
+ EditorUtility.DisplayProgressBar(
491
+ "Analyzing Animations",
492
+ $"Comparing: {sourceInfo.FileName}",
493
+ current / total
139
494
  );
140
- continue;
495
+
496
+ string destRelPath = sourceInfo.DestinationRelativePath;
497
+ string destFullPath = GetFullPathFromRelative(destRelPath);
498
+ bool destExists = destFullPath != null && File.Exists(destFullPath);
499
+
500
+ if (!destExists)
501
+ {
502
+ sourceInfo.Status = AnimationStatus.New;
503
+ _newAnimations.Add(sourceInfo);
504
+ }
505
+ else
506
+ {
507
+ string destHash = CalculateFileHash(destFullPath);
508
+ if (string.IsNullOrEmpty(sourceInfo.Hash) || string.IsNullOrEmpty(destHash))
509
+ {
510
+ this.LogWarn(
511
+ $"Could not compare '{sourceInfo.FileName}' due to hashing error. Treating as 'Changed'."
512
+ );
513
+ sourceInfo.Status = AnimationStatus.Changed;
514
+ _changedAnimations.Add(sourceInfo);
515
+ }
516
+ else if (
517
+ sourceInfo.Hash.Equals(destHash, StringComparison.OrdinalIgnoreCase)
518
+ )
519
+ {
520
+ sourceInfo.Status = AnimationStatus.Unchanged;
521
+ _unchangedAnimations.Add(sourceInfo);
522
+ }
523
+ else
524
+ {
525
+ sourceInfo.Status = AnimationStatus.Changed;
526
+ _changedAnimations.Add(sourceInfo);
527
+ }
528
+ }
141
529
  }
142
530
 
143
- string partialPath = relativePath.Substring(0, prefixIndex);
144
- string outputPath = _fullDestinationPath + partialPath;
531
+ this.Log(
532
+ $"Analysis complete: {_newAnimations.Count} New, {_changedAnimations.Count} Changed, {_unchangedAnimations.Count} Unchanged."
533
+ );
534
+ }
535
+ catch (Exception ex)
536
+ {
537
+ this.LogError($"Error during analysis: {ex.Message}\n{ex.StackTrace}");
538
+ EditorUtility.DisplayDialog(
539
+ "Analysis Error",
540
+ $"An error occurred during analysis: {ex.Message}",
541
+ "OK"
542
+ );
543
+ ClearAnalysisResults();
544
+ }
545
+ finally
546
+ {
547
+ _isAnalyzing = false;
548
+ _analysisNeeded = false;
549
+ EditorUtility.ClearProgressBar();
550
+ Repaint();
551
+ }
552
+ }
145
553
 
146
- if (!Directory.Exists(outputPath))
554
+ private void CopyAnimationsInternal(CopyMode mode)
555
+ {
556
+ if (!ArePathsValid() || _isAnalyzing || _isCopying || _isDeleting)
557
+ {
558
+ return;
559
+ }
560
+
561
+ List<AnimationFileInfo> animationsToCopy = new();
562
+ switch (mode)
563
+ {
564
+ case CopyMode.All:
565
+ animationsToCopy.AddRange(_newAnimations);
566
+ animationsToCopy.AddRange(_changedAnimations);
567
+ animationsToCopy.AddRange(_unchangedAnimations);
568
+ break;
569
+ case CopyMode.Changed:
570
+ animationsToCopy.AddRange(_changedAnimations);
571
+ break;
572
+ case CopyMode.New:
573
+ animationsToCopy.AddRange(_newAnimations);
574
+ break;
575
+ }
576
+
577
+ if (animationsToCopy.Count == 0)
578
+ {
579
+ this.Log($"No animations to copy for the selected mode.");
580
+ EditorUtility.DisplayDialog(
581
+ "Nothing to Copy",
582
+ "There are no animations matching the selected criteria.",
583
+ "OK"
584
+ );
585
+ return;
586
+ }
587
+
588
+ this.Log(
589
+ $"Starting copy operation (Mode: {mode}) for {animationsToCopy.Count} animations..."
590
+ );
591
+ _isCopying = true;
592
+ Repaint();
593
+
594
+ int successCount = 0;
595
+ int errorCount = 0;
596
+ AssetDatabase.StartAssetEditing();
597
+
598
+ try
599
+ {
600
+ for (int i = 0; i < animationsToCopy.Count; i++)
147
601
  {
148
- _ = Directory.CreateDirectory(outputPath);
602
+ AnimationFileInfo animInfo = animationsToCopy[i];
603
+ float progress = (float)(i + 1) / animationsToCopy.Count;
604
+ bool userCancelled = EditorUtility.DisplayCancelableProgressBar(
605
+ $"Copying Animations ({mode})",
606
+ $"Copying: {animInfo.FileName} ({i + 1}/{animationsToCopy.Count})",
607
+ progress
608
+ );
609
+
610
+ if (userCancelled)
611
+ {
612
+ this.LogWarn($"Copy operation cancelled by user.");
613
+ break;
614
+ }
615
+
616
+ string sourceAssetPath = animInfo.RelativePath;
617
+ string destinationAssetPath = animInfo.DestinationRelativePath;
618
+ string destDirectory = Path.GetDirectoryName(destinationAssetPath);
619
+
620
+ if (
621
+ !string.IsNullOrEmpty(destDirectory)
622
+ && !AssetDatabase.IsValidFolder(destDirectory)
623
+ )
624
+ {
625
+ try
626
+ {
627
+ EnsureDirectoryExists(destDirectory);
628
+ }
629
+ catch (Exception ex)
630
+ {
631
+ this.LogError(
632
+ $"Failed to create destination directory '{destDirectory}' for animation '{animInfo.FileName}'. Error: {ex.Message}. Skipping."
633
+ );
634
+ errorCount++;
635
+ continue;
636
+ }
637
+ }
638
+
639
+ bool copySuccessful = AssetDatabase.CopyAsset(
640
+ sourceAssetPath,
641
+ destinationAssetPath
642
+ );
643
+
644
+ if (copySuccessful)
645
+ {
646
+ successCount++;
647
+ }
648
+ else
649
+ {
650
+ this.LogError(
651
+ $"Failed to copy animation from '{sourceAssetPath}' to '{destinationAssetPath}'."
652
+ );
653
+ errorCount++;
654
+ }
149
655
  }
656
+ }
657
+ catch (Exception ex)
658
+ {
659
+ this.LogError(
660
+ $"An unexpected error occurred during the copy process: {ex.Message}\n{ex.StackTrace}"
661
+ );
662
+ errorCount = animationsToCopy.Count - successCount;
663
+ }
664
+ finally
665
+ {
666
+ AssetDatabase.StopAssetEditing();
667
+ AssetDatabase.SaveAssets();
668
+ AssetDatabase.Refresh();
669
+ EditorUtility.ClearProgressBar();
670
+ _isCopying = false;
671
+ this.Log(
672
+ $"Copy operation finished. Mode: {mode}. Success: {successCount}, Errors: {errorCount}."
673
+ );
674
+
675
+ EditorUtility.DisplayDialog(
676
+ "Copy Complete",
677
+ $"Copy operation finished.\nMode: {mode}\nSuccessfully copied: {successCount}\nErrors: {errorCount}\n\nSee console log for details.",
678
+ "OK"
679
+ );
680
+
681
+ _analysisNeeded = true;
682
+ Repaint();
683
+ }
684
+ }
685
+
686
+ private void DeleteUnchangedSourceAnimations()
687
+ {
688
+ if (!ArePathsValid() || _isAnalyzing || _isCopying || _isDeleting)
689
+ {
690
+ return;
691
+ }
692
+
693
+ List<AnimationFileInfo> animationsToDelete = _unchangedAnimations.ToList();
694
+
695
+ if (animationsToDelete.Count == 0)
696
+ {
697
+ this.Log($"No unchanged source animations to delete.");
698
+
699
+ return;
700
+ }
701
+
702
+ this.Log(
703
+ $"Starting delete operation for {animationsToDelete.Count} unchanged source animations..."
704
+ );
705
+ _isDeleting = true;
706
+ Repaint();
150
707
 
151
- string destination =
152
- animationDestinationPath + partialPath + relativePath.Substring(prefixIndex);
153
- bool copySuccessful = AssetDatabase.CopyAsset(path, destination);
154
- if (copySuccessful)
708
+ int successCount = 0;
709
+ int errorCount = 0;
710
+ AssetDatabase.StartAssetEditing();
711
+
712
+ try
713
+ {
714
+ for (int i = 0; i < animationsToDelete.Count; i++)
155
715
  {
156
- bool deleteSuccessful = AssetDatabase.DeleteAsset(path);
157
- if (!deleteSuccessful)
716
+ AnimationFileInfo animInfo = animationsToDelete[i];
717
+ float progress = (float)(i + 1) / animationsToDelete.Count;
718
+ bool userCancelled = EditorUtility.DisplayCancelableProgressBar(
719
+ "Deleting Source Duplicates",
720
+ $"Deleting: {animInfo.FileName} ({i + 1}/{animationsToDelete.Count})",
721
+ progress
722
+ );
723
+
724
+ if (userCancelled)
158
725
  {
159
- this.LogError($"Failed to delete asset at path '{path}'.");
726
+ this.LogWarn($"Delete operation cancelled by user.");
727
+ break;
160
728
  }
161
729
 
162
- ++processed;
730
+ string sourceAssetPath = animInfo.RelativePath;
731
+
732
+ bool deleteSuccessful = AssetDatabase.DeleteAsset(sourceAssetPath);
733
+
734
+ if (deleteSuccessful)
735
+ {
736
+ successCount++;
737
+ }
738
+ else
739
+ {
740
+ this.LogError(
741
+ $"Failed to delete source duplicate: '{sourceAssetPath}'. It might have been moved or deleted already."
742
+ );
743
+ errorCount++;
744
+ }
163
745
  }
164
- else
746
+ }
747
+ catch (Exception ex)
748
+ {
749
+ this.LogError(
750
+ $"An unexpected error occurred during the delete process: {ex.Message}\n{ex.StackTrace}"
751
+ );
752
+ errorCount = animationsToDelete.Count - successCount;
753
+ }
754
+ finally
755
+ {
756
+ AssetDatabase.StopAssetEditing();
757
+
758
+ AssetDatabase.Refresh();
759
+ EditorUtility.ClearProgressBar();
760
+ _isDeleting = false;
761
+ this.Log(
762
+ $"Delete operation finished. Successfully deleted: {successCount}, Errors: {errorCount}."
763
+ );
764
+
765
+ _analysisNeeded = true;
766
+ Repaint();
767
+ }
768
+ }
769
+
770
+ private void ClearAnalysisResults()
771
+ {
772
+ _sourceAnimations.Clear();
773
+ _newAnimations.Clear();
774
+ _changedAnimations.Clear();
775
+ _unchangedAnimations.Clear();
776
+
777
+ Repaint();
778
+ }
779
+
780
+ private static string GetRelativeAssetPath(string fullPath)
781
+ {
782
+ if (string.IsNullOrWhiteSpace(fullPath))
783
+ {
784
+ return null;
785
+ }
786
+
787
+ fullPath = fullPath.Replace(Path.DirectorySeparatorChar, '/');
788
+ if (
789
+ fullPath.EndsWith("/Assets", StringComparison.OrdinalIgnoreCase)
790
+ && Path.GetFileName(fullPath).Equals("Assets", StringComparison.OrdinalIgnoreCase)
791
+ )
792
+ {
793
+ return "Assets";
794
+ }
795
+
796
+ string assetsPath = Application.dataPath.Replace(Path.DirectorySeparatorChar, '/');
797
+ if (fullPath.StartsWith(assetsPath, StringComparison.OrdinalIgnoreCase))
798
+ {
799
+ if (fullPath.Length == assetsPath.Length)
800
+ {
801
+ return "Assets";
802
+ }
803
+
804
+ int startIndex = assetsPath.Length;
805
+ if (fullPath.Length > startIndex && fullPath[startIndex] == '/')
806
+ {
807
+ startIndex++;
808
+ }
809
+
810
+ return "Assets/" + fullPath.Substring(startIndex);
811
+ }
812
+
813
+ int assetIndex = fullPath.IndexOf("/Assets/", StringComparison.OrdinalIgnoreCase);
814
+ if (assetIndex >= 0)
815
+ {
816
+ return fullPath.Substring(assetIndex + 1);
817
+ }
818
+
819
+ return null;
820
+ }
821
+
822
+ private static string GetFullPathFromRelative(string relativePath)
823
+ {
824
+ if (string.IsNullOrWhiteSpace(relativePath))
825
+ {
826
+ return null;
827
+ }
828
+
829
+ if (relativePath.Equals("Assets", StringComparison.OrdinalIgnoreCase))
830
+ {
831
+ return Application.dataPath.Replace(Path.DirectorySeparatorChar, '/');
832
+ }
833
+
834
+ if (relativePath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
835
+ {
836
+ string projectRoot = Application.dataPath.Substring(
837
+ 0,
838
+ Application.dataPath.Length - "Assets".Length
839
+ );
840
+ return (projectRoot + relativePath).Replace(Path.DirectorySeparatorChar, '/');
841
+ }
842
+ return null;
843
+ }
844
+
845
+ private string GetRelativeSubPath(string basePath, string fullPath)
846
+ {
847
+ string normalizedBasePath = basePath.TrimEnd('/') + "/";
848
+ string normalizedFullPath = fullPath.TrimEnd('/') + "/";
849
+
850
+ if (
851
+ normalizedFullPath.StartsWith(
852
+ normalizedBasePath,
853
+ StringComparison.OrdinalIgnoreCase
854
+ )
855
+ )
856
+ {
857
+ string subPath = normalizedFullPath
858
+ .Substring(normalizedBasePath.Length)
859
+ .TrimEnd('/');
860
+ return subPath;
861
+ }
862
+
863
+ this.LogWarn(
864
+ $"Path '{fullPath}' did not start with expected base '{basePath}'. Could not determine relative sub-path."
865
+ );
866
+ return string.Empty;
867
+ }
868
+
869
+ private static string CalculateFileHash(string filePath)
870
+ {
871
+ try
872
+ {
873
+ using (MD5 md5 = MD5.Create())
874
+ using (FileStream stream = File.OpenRead(filePath))
875
+ {
876
+ byte[] hashBytes = md5.ComputeHash(stream);
877
+ return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
878
+ }
879
+ }
880
+ catch (IOException ioEx)
881
+ {
882
+ Debug.LogError(
883
+ $"[AnimationCopierWindow] IO Error calculating hash for {filePath}: {ioEx.Message}"
884
+ );
885
+ return string.Empty;
886
+ }
887
+ catch (Exception ex)
888
+ {
889
+ Debug.LogError(
890
+ $"[AnimationCopierWindow] Error calculating hash for {filePath}: {ex.Message}"
891
+ );
892
+ return string.Empty;
893
+ }
894
+ }
895
+
896
+ private void EnsureDirectoryExists(string relativeDirectoryPath)
897
+ {
898
+ if (string.IsNullOrWhiteSpace(relativeDirectoryPath))
899
+ {
900
+ return;
901
+ }
902
+ if (!relativeDirectoryPath.StartsWith("Assets/"))
903
+ {
904
+ if (relativeDirectoryPath.Equals("Assets", StringComparison.OrdinalIgnoreCase))
165
905
  {
166
- this.LogError($"Failed to copy animation from '{path}' to '{destination}'.");
906
+ return;
167
907
  }
908
+
909
+ this.LogError(
910
+ $"Attempted to create directory outside of Assets: '{relativeDirectoryPath}'"
911
+ );
912
+ throw new ArgumentException(
913
+ "Cannot create directories outside the Assets folder using AssetDatabase.",
914
+ nameof(relativeDirectoryPath)
915
+ );
168
916
  }
169
917
 
170
- this.Log($"Processed {processed} AnimationClips.");
918
+ if (AssetDatabase.IsValidFolder(relativeDirectoryPath))
919
+ {
920
+ return;
921
+ }
922
+
923
+ string parentPath = Path.GetDirectoryName(relativeDirectoryPath)
924
+ ?.Replace(Path.DirectorySeparatorChar, '/');
925
+
926
+ if (
927
+ string.IsNullOrEmpty(parentPath)
928
+ || parentPath.Equals("Assets", StringComparison.OrdinalIgnoreCase)
929
+ )
930
+ {
931
+ string folderNameToCreate = Path.GetFileName(relativeDirectoryPath);
932
+ if (
933
+ !string.IsNullOrEmpty(folderNameToCreate)
934
+ && !AssetDatabase.IsValidFolder(relativeDirectoryPath)
935
+ )
936
+ {
937
+ AssetDatabase.CreateFolder("Assets", folderNameToCreate);
938
+ }
939
+ return;
940
+ }
941
+
942
+ EnsureDirectoryExists(parentPath);
943
+
944
+ string currentFolderName = Path.GetFileName(relativeDirectoryPath);
945
+ if (
946
+ !string.IsNullOrEmpty(currentFolderName)
947
+ && !AssetDatabase.IsValidFolder(relativeDirectoryPath)
948
+ )
949
+ {
950
+ AssetDatabase.CreateFolder(parentPath, currentFolderName);
951
+ this.Log($"Created folder: {relativeDirectoryPath}");
952
+ }
171
953
  }
172
954
  }
173
955
  #endif