com.wallstop-studios.unity-helpers 2.0.0-rc75.8 → 2.0.0-rc76

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 (27) hide show
  1. package/Editor/CustomDrawers/IntDropdownDrawer.cs +46 -0
  2. package/Editor/CustomDrawers/IntDropdownDrawer.cs.meta +3 -0
  3. package/Editor/{StringInListeDrawer.cs → CustomDrawers/StringInListeDrawer.cs} +2 -2
  4. package/Editor/{WShowIfPropertyDrawer.cs → CustomDrawers/WShowIfPropertyDrawer.cs} +3 -3
  5. package/Editor/CustomDrawers.meta +3 -0
  6. package/Editor/Sprites/AnimationViewerWindow.cs +1521 -0
  7. package/Editor/Sprites/AnimationViewerWindow.cs.meta +3 -0
  8. package/Editor/Sprites/ProjectAnimationSettings.cs +50 -0
  9. package/Editor/Sprites/ProjectAnimationSettings.cs.meta +3 -0
  10. package/Editor/Sprites/ScriptableSpriteAtlas.cs +4 -2
  11. package/Editor/Sprites/SpriteSettingsApplier.cs +6 -0
  12. package/Editor/Styles/AnimationViewer.uss +116 -0
  13. package/Editor/Styles/AnimationViewer.uss.meta +3 -0
  14. package/Editor/Styles/AnimationViewer.uxml +57 -0
  15. package/Editor/Styles/AnimationViewer.uxml.meta +3 -0
  16. package/Editor/Styles.meta +3 -0
  17. package/Editor/UI.meta +3 -0
  18. package/Runtime/Core/Attributes/IntDropdownAttribute.cs +14 -0
  19. package/Runtime/Core/Attributes/IntDropdownAttribute.cs.meta +3 -0
  20. package/Runtime/UI/LayeredImage.cs +193 -98
  21. package/Runtime/UI/MultiFileSelectorElement.cs +322 -0
  22. package/Runtime/UI/MultiFileSelectorElement.cs.meta +3 -0
  23. package/package.json +3 -1
  24. package/Editor/AnimatorControllerCopier.cs +0 -156
  25. package/Editor/AnimatorControllerCopier.cs.meta +0 -3
  26. /package/Editor/{StringInListeDrawer.cs.meta → CustomDrawers/StringInListeDrawer.cs.meta} +0 -0
  27. /package/Editor/{WShowIfPropertyDrawer.cs.meta → CustomDrawers/WShowIfPropertyDrawer.cs.meta} +0 -0
@@ -0,0 +1,1521 @@
1
+ // ReSharper disable HeapView.CanAvoidClosure
2
+ namespace WallstopStudios.UnityHelpers.Editor.Sprites
3
+ {
4
+ #if UNITY_EDITOR
5
+ using System;
6
+ using System.Collections.Generic;
7
+ using System.IO;
8
+ using System.Linq;
9
+ using Core.Extension;
10
+ using Core.Helper;
11
+ using UnityEditor;
12
+ using UnityEditor.UIElements;
13
+ using UnityEngine;
14
+ using UnityEngine.UIElements;
15
+ using UI;
16
+ using Object = UnityEngine.Object;
17
+
18
+ public sealed class AnimationViewerWindow : EditorWindow
19
+ {
20
+ private const string PackageId = "com.wallstop-studios.unity-helpers";
21
+ private const float DragThresholdSqrMagnitude = 10f * 10f;
22
+ private const int InvalidPointerId = -1;
23
+
24
+ private sealed class EditorLayerData
25
+ {
26
+ public AnimationClip SourceClip { get; }
27
+ public List<Sprite> Sprites { get; }
28
+ public string ClipName => SourceClip != null ? SourceClip.name : "Unnamed Layer";
29
+ public float OriginalClipFps { get; }
30
+ public string BindingPath { get; }
31
+
32
+ public EditorLayerData(AnimationClip clip)
33
+ {
34
+ SourceClip = clip;
35
+ Sprites = clip.GetSpritesFromClip()?.ToList();
36
+ OriginalClipFps =
37
+ clip.frameRate > 0 ? clip.frameRate : AnimatedSpriteLayer.FrameRate;
38
+
39
+ BindingPath = string.Empty;
40
+ if (SourceClip != null)
41
+ {
42
+ foreach (
43
+ EditorCurveBinding binding in AnimationUtility.GetObjectReferenceCurveBindings(
44
+ SourceClip
45
+ )
46
+ )
47
+ {
48
+ if (
49
+ binding.type == typeof(SpriteRenderer)
50
+ && binding.propertyName == "m_Sprite"
51
+ )
52
+ {
53
+ BindingPath = binding.path;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ [MenuItem("Tools/Wallstop Studios/Unity Helpers/Sprite Animation Editor")]
62
+ public static void ShowWindow()
63
+ {
64
+ AnimationViewerWindow wnd = GetWindow<AnimationViewerWindow>();
65
+ wnd.titleContent = new GUIContent("2D Animation Viewer");
66
+ wnd.minSize = new Vector2(750, 500);
67
+ }
68
+
69
+ private VisualTreeAsset _visualTree;
70
+ private StyleSheet _styleSheet;
71
+
72
+ private ObjectField _addAnimationClipField;
73
+ private Button _browseAndAddButton;
74
+ private FloatField _fpsField;
75
+ private Button _applyFpsButton;
76
+ private Button _saveClipButton;
77
+ private VisualElement _loadedClipsContainer;
78
+ private VisualElement _previewPanelHost;
79
+ private LayeredImage _animationPreview;
80
+ private VisualElement _framesContainer;
81
+ private Label _fpsDebugLabel;
82
+ private Label _framesPanelTitle;
83
+ private MultiFileSelectorElement _fileSelector;
84
+
85
+ private readonly List<EditorLayerData> _loadedEditorLayers = new();
86
+
87
+ private EditorLayerData _activeEditorLayer;
88
+ private float _currentPreviewFps = AnimatedSpriteLayer.FrameRate;
89
+
90
+ private VisualElement _draggedFrameElement;
91
+ private int _draggedFrameOriginalDataIndex;
92
+ private VisualElement _frameDropPlaceholder;
93
+
94
+ private VisualElement _draggedLoadedClipElement;
95
+ private int _draggedLoadedClipOriginalIndex;
96
+ private VisualElement _loadedClipDropPlaceholder;
97
+
98
+ private bool _isClipDragPending;
99
+ private Vector3 _clipDragStartPosition;
100
+ private VisualElement _clipDragPendingElement;
101
+ private int _clipDragPendingOriginalIndex;
102
+
103
+ public void CreateGUI()
104
+ {
105
+ VisualElement root = rootVisualElement;
106
+
107
+ TryLoadStyleSheets();
108
+
109
+ if (_visualTree == null)
110
+ {
111
+ root.Add(new Label("Error: AnimationViewer.uxml not found."));
112
+ return;
113
+ }
114
+ if (_styleSheet != null)
115
+ {
116
+ root.styleSheets.Add(_styleSheet);
117
+ }
118
+
119
+ _visualTree.CloneTree(root);
120
+
121
+ _addAnimationClipField = root.Q<ObjectField>("addAnimationClipField");
122
+ _browseAndAddButton = root.Q<Button>("browseAndAddButton");
123
+ _fpsField = root.Q<FloatField>("fpsField");
124
+ _applyFpsButton = root.Q<Button>("applyFpsButton");
125
+ _saveClipButton = root.Q<Button>("saveClipButton");
126
+ _loadedClipsContainer = root.Q<VisualElement>("loadedClipsContainer");
127
+ _previewPanelHost = root.Q<VisualElement>("preview-panel");
128
+ _framesContainer = root.Q<VisualElement>("framesContainer");
129
+ _fpsDebugLabel = root.Q<Label>("fpsDebugLabel");
130
+ _framesPanelTitle = root.Q<Label>("framesPanelTitle");
131
+
132
+ _previewPanelHost.AddToClassList("animation-preview-container");
133
+
134
+ _fpsField.value = _currentPreviewFps;
135
+ _saveClipButton.SetEnabled(false);
136
+
137
+ _addAnimationClipField.RegisterValueChangedCallback(OnAddAnimationClipFieldChanged);
138
+ _browseAndAddButton.text = "Add Selected Clips from Project";
139
+ _browseAndAddButton.clicked -= OnBrowseAndAddClicked;
140
+ if (_browseAndAddButton != null)
141
+ {
142
+ _browseAndAddButton.text = "Browse Clips (Multi)...";
143
+ _browseAndAddButton.clicked -= OnAddSelectedClipsFromProjectClicked;
144
+ _browseAndAddButton.clicked += ToggleMultiFileSelector;
145
+ }
146
+ else
147
+ {
148
+ this.LogError(
149
+ $"'browseAndAddButton' not found in UXML. Multi-file browser cannot be triggered."
150
+ );
151
+ }
152
+ _applyFpsButton.clicked += OnApplyFpsToPreviewClicked;
153
+ _saveClipButton.clicked += OnSaveClipClicked;
154
+
155
+ _frameDropPlaceholder = new VisualElement();
156
+ _frameDropPlaceholder.AddToClassList("drop-placeholder");
157
+ _frameDropPlaceholder.style.height = 5;
158
+ _frameDropPlaceholder.style.visibility = Visibility.Hidden;
159
+
160
+ _framesContainer.RegisterCallback<DragUpdatedEvent>(OnFramesContainerDragUpdated);
161
+ _framesContainer.RegisterCallback<DragPerformEvent>(OnFramesContainerDragPerform);
162
+ _framesContainer.RegisterCallback<DragLeaveEvent>(OnFramesContainerDragLeave);
163
+
164
+ _loadedClipDropPlaceholder = new VisualElement();
165
+ _loadedClipDropPlaceholder.AddToClassList("drop-placeholder");
166
+ _loadedClipDropPlaceholder.style.height = 5;
167
+ _loadedClipDropPlaceholder.style.visibility = Visibility.Hidden;
168
+
169
+ _loadedClipsContainer.RegisterCallback<DragUpdatedEvent>(
170
+ OnLoadedClipsContainerDragUpdated
171
+ );
172
+ _loadedClipsContainer.RegisterCallback<DragPerformEvent>(
173
+ OnLoadedClipsContainerDragPerform
174
+ );
175
+ _loadedClipsContainer.RegisterCallback<DragLeaveEvent>(OnLoadedClipsContainerDragLeave);
176
+
177
+ UpdateFramesPanelTitle();
178
+ RebuildLoadedClipsUI();
179
+ RecreatePreviewImage();
180
+ }
181
+
182
+ private void Update()
183
+ {
184
+ _animationPreview?.Update();
185
+ }
186
+
187
+ private void TryLoadStyleSheets()
188
+ {
189
+ string packageRoot = DirectoryHelper.FindPackageRootPath(
190
+ DirectoryHelper.GetCallerScriptDirectory()
191
+ );
192
+ if (!string.IsNullOrWhiteSpace(packageRoot))
193
+ {
194
+ if (
195
+ packageRoot.StartsWith("Packages", StringComparison.OrdinalIgnoreCase)
196
+ && !packageRoot.Contains(PackageId, StringComparison.OrdinalIgnoreCase)
197
+ )
198
+ {
199
+ int helpersIndex = packageRoot.LastIndexOf(
200
+ "UnityHelpers",
201
+ StringComparison.Ordinal
202
+ );
203
+ if (0 <= helpersIndex)
204
+ {
205
+ packageRoot = packageRoot[..helpersIndex];
206
+ packageRoot += PackageId;
207
+ }
208
+ }
209
+
210
+ char pathSeparator = Path.DirectorySeparatorChar;
211
+ string styleSheetPath =
212
+ $"{packageRoot}{pathSeparator}Editor{pathSeparator}Styles{pathSeparator}AnimationViewer.uss";
213
+ string unityRelativeStyleSheetPath = DirectoryHelper.AbsoluteToUnityRelativePath(
214
+ styleSheetPath
215
+ );
216
+ unityRelativeStyleSheetPath = unityRelativeStyleSheetPath.SanitizePath();
217
+
218
+ const string packageCache = "PackageCache/";
219
+ int packageCacheIndex;
220
+ if (!string.IsNullOrWhiteSpace(unityRelativeStyleSheetPath))
221
+ {
222
+ _styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
223
+ unityRelativeStyleSheetPath
224
+ );
225
+ }
226
+
227
+ if (_styleSheet == null && !string.IsNullOrWhiteSpace(unityRelativeStyleSheetPath))
228
+ {
229
+ packageCacheIndex = unityRelativeStyleSheetPath.IndexOf(
230
+ packageCache,
231
+ StringComparison.OrdinalIgnoreCase
232
+ );
233
+ if (0 <= packageCacheIndex)
234
+ {
235
+ unityRelativeStyleSheetPath = unityRelativeStyleSheetPath[
236
+ (packageCacheIndex + packageCache.Length)..
237
+ ];
238
+ int forwardIndex = unityRelativeStyleSheetPath.IndexOf(
239
+ "/",
240
+ StringComparison.Ordinal
241
+ );
242
+ if (0 <= forwardIndex)
243
+ {
244
+ unityRelativeStyleSheetPath = unityRelativeStyleSheetPath.Substring(
245
+ forwardIndex
246
+ );
247
+ unityRelativeStyleSheetPath =
248
+ "Packages/" + PackageId + "/" + unityRelativeStyleSheetPath;
249
+ }
250
+ else
251
+ {
252
+ unityRelativeStyleSheetPath = "Packages/" + unityRelativeStyleSheetPath;
253
+ }
254
+ }
255
+
256
+ if (!string.IsNullOrWhiteSpace(unityRelativeStyleSheetPath))
257
+ {
258
+ _styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
259
+ unityRelativeStyleSheetPath
260
+ );
261
+ if (_styleSheet == null)
262
+ {
263
+ this.LogError(
264
+ $"Failed to load Animation Viewer style sheet (package root: '{packageRoot}'), relative path '{unityRelativeStyleSheetPath}'."
265
+ );
266
+ }
267
+ }
268
+ else
269
+ {
270
+ this.LogError(
271
+ $"Failed to convert absolute path '{styleSheetPath}' to Unity relative path."
272
+ );
273
+ }
274
+ }
275
+
276
+ string visualTreePath =
277
+ $"{packageRoot}{pathSeparator}Editor{pathSeparator}Styles{pathSeparator}AnimationViewer.uxml";
278
+ string unityRelativeVisualTreePath = DirectoryHelper.AbsoluteToUnityRelativePath(
279
+ visualTreePath
280
+ );
281
+
282
+ _visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
283
+ unityRelativeVisualTreePath
284
+ );
285
+ if (_visualTree == null)
286
+ {
287
+ packageCacheIndex = unityRelativeVisualTreePath.IndexOf(
288
+ packageCache,
289
+ StringComparison.OrdinalIgnoreCase
290
+ );
291
+ if (0 <= packageCacheIndex)
292
+ {
293
+ unityRelativeVisualTreePath = unityRelativeVisualTreePath[
294
+ (packageCacheIndex + packageCache.Length)..
295
+ ];
296
+ int forwardIndex = unityRelativeVisualTreePath.IndexOf(
297
+ "/",
298
+ StringComparison.Ordinal
299
+ );
300
+ if (0 <= forwardIndex)
301
+ {
302
+ unityRelativeVisualTreePath = unityRelativeVisualTreePath.Substring(
303
+ forwardIndex
304
+ );
305
+ unityRelativeVisualTreePath =
306
+ "Packages/" + PackageId + "/" + unityRelativeVisualTreePath;
307
+ }
308
+ else
309
+ {
310
+ unityRelativeVisualTreePath = "Packages/" + unityRelativeVisualTreePath;
311
+ }
312
+ }
313
+
314
+ if (!string.IsNullOrWhiteSpace(unityRelativeVisualTreePath))
315
+ {
316
+ _visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
317
+ unityRelativeVisualTreePath
318
+ );
319
+ }
320
+ }
321
+ }
322
+ else
323
+ {
324
+ this.LogError(
325
+ $"Failed to find Animation Viewer style sheet (package root: '{packageRoot}')."
326
+ );
327
+ }
328
+ }
329
+
330
+ private void ToggleMultiFileSelector()
331
+ {
332
+ VisualElement root = rootVisualElement;
333
+ if (_fileSelector == null)
334
+ {
335
+ _fileSelector = new MultiFileSelectorElement(
336
+ ProjectAnimationSettings.Instance.lastAnimationPath,
337
+ new[] { ".anim" }
338
+ );
339
+ _fileSelector.OnFilesSelected += HandleFilesSelectedFromCustomBrowser;
340
+ _fileSelector.OnCancelled += HideMultiFileSelector;
341
+ root.Add(_fileSelector);
342
+ if (root.childCount > 1)
343
+ {
344
+ _fileSelector.PlaceInFront(root.Children().FirstOrDefault());
345
+ }
346
+ }
347
+ else if (_fileSelector.parent == null)
348
+ {
349
+ _fileSelector.ResetAndShow(ProjectAnimationSettings.Instance.lastAnimationPath);
350
+ root.Add(_fileSelector);
351
+ if (root.childCount > 1)
352
+ {
353
+ _fileSelector.PlaceInFront(root.Children().FirstOrDefault());
354
+ }
355
+ }
356
+ else
357
+ {
358
+ HideMultiFileSelector();
359
+ }
360
+ }
361
+
362
+ private void HideMultiFileSelector()
363
+ {
364
+ if (_fileSelector is { parent: not null })
365
+ {
366
+ _fileSelector.parent.Remove(_fileSelector);
367
+ }
368
+ }
369
+
370
+ private void HandleFilesSelectedFromCustomBrowser(List<string> selectedFullPaths)
371
+ {
372
+ HideMultiFileSelector();
373
+
374
+ if (selectedFullPaths == null || selectedFullPaths.Count == 0)
375
+ {
376
+ return;
377
+ }
378
+
379
+ int clipsAddedCount = 0;
380
+ string lastValidDirectory = null;
381
+
382
+ foreach (string fullPath in selectedFullPaths)
383
+ {
384
+ string assetPath = fullPath.SanitizePath();
385
+ if (!assetPath.StartsWith("Assets", StringComparison.OrdinalIgnoreCase))
386
+ {
387
+ if (
388
+ assetPath.StartsWith(
389
+ Application.dataPath,
390
+ StringComparison.OrdinalIgnoreCase
391
+ )
392
+ )
393
+ {
394
+ assetPath = "Assets" + assetPath.Substring(Application.dataPath.Length);
395
+ }
396
+ else
397
+ {
398
+ this.LogWarn(
399
+ $"Selected file '{fullPath}' is outside the project's Assets folder or path is not project-relative. Skipping."
400
+ );
401
+ continue;
402
+ }
403
+ }
404
+
405
+ AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(assetPath);
406
+ if (clip != null)
407
+ {
408
+ if (_loadedEditorLayers.All(layer => layer.SourceClip != clip))
409
+ {
410
+ AddEditorLayer(clip);
411
+ clipsAddedCount++;
412
+ lastValidDirectory = Path.GetDirectoryName(assetPath);
413
+ }
414
+ else
415
+ {
416
+ this.LogWarn($"Clip '{clip.name}' already loaded. Skipping.");
417
+ }
418
+ }
419
+ else
420
+ {
421
+ this.LogWarn($"Could not load AnimationClip from: {assetPath}");
422
+ }
423
+ }
424
+
425
+ if (clipsAddedCount > 0)
426
+ {
427
+ this.Log($"Added {clipsAddedCount} clip(s).");
428
+ if (!string.IsNullOrWhiteSpace(lastValidDirectory))
429
+ {
430
+ ProjectAnimationSettings.Instance.lastAnimationPath = lastValidDirectory;
431
+ ProjectAnimationSettings.Instance.Save();
432
+ }
433
+ }
434
+ }
435
+
436
+ private void OnAddAnimationClipFieldChanged(ChangeEvent<Object> evt)
437
+ {
438
+ AnimationClip clip = evt.newValue as AnimationClip;
439
+ if (clip != null)
440
+ {
441
+ AddEditorLayer(clip);
442
+ _addAnimationClipField.SetValueWithoutNotify(null);
443
+ }
444
+ }
445
+
446
+ private void OnAddSelectedClipsFromProjectClicked()
447
+ {
448
+ Object[] selectedObjects = Selection.GetFiltered(
449
+ typeof(AnimationClip),
450
+ SelectionMode.Assets
451
+ );
452
+
453
+ if (selectedObjects == null || selectedObjects.Length == 0)
454
+ {
455
+ EditorUtility.DisplayDialog(
456
+ "No Clips Selected",
457
+ "Please select one or more AnimationClip assets in the Project window first.",
458
+ "OK"
459
+ );
460
+ return;
461
+ }
462
+
463
+ int clipsAddedCount = 0;
464
+ foreach (Object obj in selectedObjects)
465
+ {
466
+ if (obj is AnimationClip clip)
467
+ {
468
+ bool alreadyExists = _loadedEditorLayers.Any(layer => layer.SourceClip == clip);
469
+ if (!alreadyExists)
470
+ {
471
+ AddEditorLayer(clip);
472
+ clipsAddedCount++;
473
+ }
474
+ else
475
+ {
476
+ this.LogWarn($"Clip '{clip.name}' is already loaded. Skipping.");
477
+ }
478
+ }
479
+ }
480
+
481
+ if (clipsAddedCount > 0)
482
+ {
483
+ this.Log($"Added {clipsAddedCount} new AnimationClip(s) to the viewer.");
484
+ }
485
+ else if (selectedObjects.Length > 0)
486
+ {
487
+ this.Log($"All selected AnimationClips were already loaded.");
488
+ }
489
+ }
490
+
491
+ private void OnBrowseAndAddClicked()
492
+ {
493
+ string path = EditorUtility.OpenFilePanelWithFilters(
494
+ "Select Animation Clip to Add",
495
+ ProjectAnimationSettings.Instance.lastAnimationPath,
496
+ new[] { "Animation Clip", "anim" }
497
+ );
498
+
499
+ if (!string.IsNullOrWhiteSpace(path))
500
+ {
501
+ if (path.StartsWith(Application.dataPath))
502
+ {
503
+ path = "Assets" + path.Substring(Application.dataPath.Length);
504
+ }
505
+
506
+ ProjectAnimationSettings.Instance.lastAnimationPath = Path.GetDirectoryName(path);
507
+ ProjectAnimationSettings.Instance.Save();
508
+
509
+ AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
510
+ if (clip != null)
511
+ {
512
+ AddEditorLayer(clip);
513
+ }
514
+ }
515
+ }
516
+
517
+ private void AddEditorLayer(AnimationClip clip)
518
+ {
519
+ if (clip == null || _loadedEditorLayers.Any(layer => layer.SourceClip == clip))
520
+ {
521
+ this.LogWarn($"Clip '{clip?.name}' is null or already loaded.");
522
+ return;
523
+ }
524
+
525
+ EditorLayerData newEditorLayer = new(clip);
526
+ _loadedEditorLayers.Add(newEditorLayer);
527
+
528
+ if (_activeEditorLayer == null && newEditorLayer.Sprites.Count > 0)
529
+ {
530
+ SetActiveEditorLayer(newEditorLayer);
531
+ }
532
+ else if (_activeEditorLayer == null)
533
+ {
534
+ SetActiveEditorLayer(newEditorLayer);
535
+ }
536
+
537
+ RebuildLoadedClipsUI();
538
+ RecreatePreviewImage();
539
+ }
540
+
541
+ private void RemoveEditorLayer(EditorLayerData layerToRemove)
542
+ {
543
+ if (layerToRemove == null)
544
+ {
545
+ return;
546
+ }
547
+
548
+ _loadedEditorLayers.Remove(layerToRemove);
549
+
550
+ if (_activeEditorLayer == layerToRemove)
551
+ {
552
+ SetActiveEditorLayer(_loadedEditorLayers.FirstOrDefault());
553
+ }
554
+
555
+ RebuildLoadedClipsUI();
556
+ RecreatePreviewImage();
557
+ }
558
+
559
+ private void SetActiveEditorLayer(EditorLayerData layer)
560
+ {
561
+ _activeEditorLayer = layer;
562
+ _framesContainer.Clear();
563
+ _fpsDebugLabel.text = "Detected FPS Info (Active Clip):";
564
+
565
+ if (_activeEditorLayer != null)
566
+ {
567
+ UpdateFpsDebugLabelForActiveLayer();
568
+ _saveClipButton.SetEnabled(_activeEditorLayer.SourceClip != null);
569
+ }
570
+ else
571
+ {
572
+ _saveClipButton.SetEnabled(false);
573
+ }
574
+
575
+ RebuildFramesListUI();
576
+ RebuildLoadedClipsUI();
577
+ UpdateFramesPanelTitle();
578
+ }
579
+
580
+ private void UpdateFramesPanelTitle()
581
+ {
582
+ _framesPanelTitle.text =
583
+ _activeEditorLayer != null
584
+ ? $"Frames (Editing: {_activeEditorLayer.ClipName})"
585
+ : "Frames (No Active Clip Selected)";
586
+ }
587
+
588
+ private void RebuildLoadedClipsUI()
589
+ {
590
+ _loadedClipsContainer.Clear();
591
+ if (_loadedClipDropPlaceholder.parent == _loadedClipsContainer)
592
+ {
593
+ _loadedClipsContainer.Remove(_loadedClipDropPlaceholder);
594
+ }
595
+
596
+ for (int i = 0; i < _loadedEditorLayers.Count; i++)
597
+ {
598
+ EditorLayerData editorLayer = _loadedEditorLayers[i];
599
+
600
+ VisualElement itemElement = new();
601
+ itemElement.AddToClassList("loaded-clip-item");
602
+ if (editorLayer == _activeEditorLayer)
603
+ {
604
+ itemElement.AddToClassList("loaded-clip-item--active");
605
+ }
606
+ itemElement.userData = i;
607
+
608
+ Label label = new(editorLayer.ClipName);
609
+ itemElement.Add(label);
610
+
611
+ Button removeButton = new(() => RemoveEditorLayer(editorLayer)) { text = "X" };
612
+ itemElement.Add(removeButton);
613
+
614
+ itemElement.RegisterCallback<PointerDownEvent>(evt =>
615
+ {
616
+ if (evt.button == 0 && _draggedLoadedClipElement == null)
617
+ {
618
+ SetActiveEditorLayer(editorLayer);
619
+ }
620
+ });
621
+
622
+ int currentIndex = i;
623
+ itemElement.RegisterCallback<PointerDownEvent>(evt =>
624
+ OnLoadedClipItemPointerDownSetup(evt, itemElement, currentIndex)
625
+ );
626
+ itemElement.RegisterCallback<PointerMoveEvent>(OnLoadedClipItemPointerMove);
627
+ itemElement.RegisterCallback<PointerUpEvent>(evt =>
628
+ OnLoadedClipItemPointerUpForClick(evt, itemElement, currentIndex)
629
+ );
630
+
631
+ _loadedClipsContainer.Add(itemElement);
632
+ }
633
+ }
634
+
635
+ private void OnLoadedClipItemPointerDownSetup(
636
+ PointerDownEvent evt,
637
+ VisualElement clipElement,
638
+ int originalListIndex
639
+ )
640
+ {
641
+ if (evt.button != 0 || _draggedLoadedClipElement != null || _isClipDragPending)
642
+ {
643
+ return;
644
+ }
645
+
646
+ _isClipDragPending = true;
647
+ _clipDragPendingElement = clipElement;
648
+ _clipDragPendingOriginalIndex = originalListIndex;
649
+ _clipDragStartPosition = evt.position;
650
+
651
+ clipElement.CapturePointer(evt.pointerId);
652
+
653
+ evt.StopPropagation();
654
+ }
655
+
656
+ private void OnLoadedClipItemPointerMove(PointerMoveEvent evt)
657
+ {
658
+ if (!_isClipDragPending)
659
+ {
660
+ return;
661
+ }
662
+
663
+ float diffSqrMagnitude = (evt.position - _clipDragStartPosition).sqrMagnitude;
664
+
665
+ if (diffSqrMagnitude >= DragThresholdSqrMagnitude)
666
+ {
667
+ _draggedLoadedClipElement = _clipDragPendingElement;
668
+ _draggedLoadedClipOriginalIndex = _clipDragPendingOriginalIndex;
669
+
670
+ _draggedLoadedClipElement.AddToClassList("frame-item-dragged");
671
+
672
+ DragAndDrop.PrepareStartDrag();
673
+ DragAndDrop.SetGenericData(
674
+ "DraggedLoadedClipIndex",
675
+ _draggedLoadedClipOriginalIndex
676
+ );
677
+ Object dragContextObject =
678
+ _loadedEditorLayers[_draggedLoadedClipOriginalIndex]?.SourceClip
679
+ ?? (Object)CreateInstance<ScriptableObject>();
680
+ DragAndDrop.objectReferences = new[] { dragContextObject };
681
+ DragAndDrop.StartDrag(
682
+ _loadedEditorLayers[_draggedLoadedClipOriginalIndex].ClipName
683
+ ?? "Dragging Layer"
684
+ );
685
+
686
+ _isClipDragPending = false;
687
+ }
688
+ }
689
+
690
+ private void OnLoadedClipItemPointerUpForClick(
691
+ PointerUpEvent evt,
692
+ VisualElement clipElement,
693
+ int listIndex
694
+ )
695
+ {
696
+ if (evt.button != 0)
697
+ {
698
+ return;
699
+ }
700
+
701
+ if (clipElement.HasPointerCapture(evt.pointerId))
702
+ {
703
+ clipElement.ReleasePointer(evt.pointerId);
704
+ }
705
+ if (_isClipDragPending)
706
+ {
707
+ if (listIndex >= 0 && listIndex < _loadedEditorLayers.Count)
708
+ {
709
+ SetActiveEditorLayer(_loadedEditorLayers[listIndex]);
710
+ }
711
+ _isClipDragPending = false;
712
+ _clipDragPendingElement = null;
713
+ evt.StopPropagation();
714
+ }
715
+ else if (_draggedLoadedClipElement == _clipDragPendingElement)
716
+ {
717
+ if (DragAndDrop.GetGenericData("DraggedLoadedClipIndex") != null)
718
+ {
719
+ CleanupLoadedClipDragState(evt.pointerId);
720
+ }
721
+ evt.StopPropagation();
722
+ }
723
+
724
+ _isClipDragPending = false;
725
+ _clipDragPendingElement = null;
726
+ }
727
+
728
+ private void OnDraggedLoadedClipItemPointerUp(PointerUpEvent evt)
729
+ {
730
+ if (_draggedLoadedClipElement == null || evt.currentTarget != _draggedLoadedClipElement)
731
+ {
732
+ return;
733
+ }
734
+
735
+ if (
736
+ DragAndDrop.GetGenericData("DraggedLoadedClipIndex") != null
737
+ || _draggedLoadedClipElement != null
738
+ && _draggedLoadedClipElement.HasPointerCapture(evt.pointerId)
739
+ )
740
+ {
741
+ CleanupLoadedClipDragState(evt.pointerId);
742
+ }
743
+
744
+ evt.StopPropagation();
745
+ }
746
+
747
+ private void OnLoadedClipsContainerDragUpdated(DragUpdatedEvent evt)
748
+ {
749
+ object draggedIndexData = DragAndDrop.GetGenericData("DraggedLoadedClipIndex");
750
+ if (draggedIndexData != null && _draggedLoadedClipElement != null)
751
+ {
752
+ DragAndDrop.visualMode = DragAndDropVisualMode.Move;
753
+ float mouseY = evt.localMousePosition.y;
754
+ int newVisualIndex = -1;
755
+
756
+ if (_loadedClipDropPlaceholder.parent == _loadedClipsContainer)
757
+ {
758
+ _loadedClipsContainer.Remove(_loadedClipDropPlaceholder);
759
+ }
760
+
761
+ for (int i = 0; i < _loadedClipsContainer.childCount; i++)
762
+ {
763
+ VisualElement child = _loadedClipsContainer[i];
764
+ if (child == _draggedLoadedClipElement)
765
+ {
766
+ continue;
767
+ }
768
+
769
+ float childMidY = child.layout.yMin + child.layout.height / 2f;
770
+ if (mouseY < childMidY)
771
+ {
772
+ newVisualIndex = i;
773
+ break;
774
+ }
775
+ }
776
+ if (
777
+ newVisualIndex < 0
778
+ && _loadedClipsContainer.childCount > 0
779
+ && _draggedLoadedClipElement
780
+ != _loadedClipsContainer.ElementAt(_loadedClipsContainer.childCount - 1)
781
+ )
782
+ {
783
+ newVisualIndex = _loadedClipsContainer.childCount;
784
+ }
785
+
786
+ if (0 <= newVisualIndex)
787
+ {
788
+ _loadedClipsContainer.Insert(newVisualIndex, _loadedClipDropPlaceholder);
789
+ _loadedClipDropPlaceholder.style.visibility = Visibility.Visible;
790
+ }
791
+ else if (_loadedClipsContainer.childCount == 0 && _draggedLoadedClipElement != null)
792
+ {
793
+ _loadedClipsContainer.Add(_loadedClipDropPlaceholder);
794
+ _loadedClipDropPlaceholder.style.visibility = Visibility.Visible;
795
+ }
796
+ else
797
+ {
798
+ if (_loadedClipDropPlaceholder.parent == _loadedClipsContainer)
799
+ {
800
+ _loadedClipsContainer.Remove(_loadedClipDropPlaceholder);
801
+ }
802
+
803
+ _loadedClipDropPlaceholder.style.visibility = Visibility.Hidden;
804
+ }
805
+ }
806
+ else
807
+ {
808
+ DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
809
+ }
810
+ evt.StopPropagation();
811
+ }
812
+
813
+ private void OnLoadedClipsContainerDragPerform(DragPerformEvent evt)
814
+ {
815
+ object draggedIndexData = DragAndDrop.GetGenericData("DraggedLoadedClipIndex");
816
+ if (draggedIndexData != null && _draggedLoadedClipElement != null)
817
+ {
818
+ int originalListIndex = (int)draggedIndexData;
819
+
820
+ if (originalListIndex < 0 || originalListIndex >= _loadedEditorLayers.Count)
821
+ {
822
+ this.LogError(
823
+ $"DragPerform (LoadedClips): Stale or invalid dragged index. Aborting drop."
824
+ );
825
+ CleanupLoadedClipDragState(InvalidPointerId);
826
+ evt.StopPropagation();
827
+ return;
828
+ }
829
+
830
+ EditorLayerData movedLayer = _loadedEditorLayers[originalListIndex];
831
+ _loadedEditorLayers.RemoveAt(originalListIndex);
832
+
833
+ int placeholderVisualIndex = _loadedClipsContainer.IndexOf(
834
+ _loadedClipDropPlaceholder
835
+ );
836
+ int targetListIndex;
837
+
838
+ if (0 <= placeholderVisualIndex)
839
+ {
840
+ int itemsBeforePlaceholder = -1;
841
+ for (int i = 0; i < placeholderVisualIndex; i++)
842
+ {
843
+ if (_loadedClipsContainer[i] != _loadedClipDropPlaceholder)
844
+ {
845
+ itemsBeforePlaceholder++;
846
+ }
847
+ else
848
+ {
849
+ break;
850
+ }
851
+ }
852
+ targetListIndex = itemsBeforePlaceholder;
853
+ }
854
+ else
855
+ {
856
+ targetListIndex = _loadedEditorLayers.Count;
857
+ }
858
+ targetListIndex = Mathf.Clamp(targetListIndex, 0, _loadedEditorLayers.Count);
859
+ _loadedEditorLayers.Insert(targetListIndex, movedLayer);
860
+
861
+ DragAndDrop.AcceptDrag();
862
+ CleanupLoadedClipDragState(InvalidPointerId);
863
+
864
+ RebuildLoadedClipsUI();
865
+ RecreatePreviewImage();
866
+ }
867
+ else
868
+ {
869
+ if (DragAndDrop.GetGenericData("DraggedLoadedClipIndex") != null)
870
+ {
871
+ CleanupLoadedClipDragState(InvalidPointerId);
872
+ }
873
+ }
874
+ evt.StopPropagation();
875
+ }
876
+
877
+ private void OnLoadedClipsContainerDragLeave(DragLeaveEvent evt)
878
+ {
879
+ if (evt.target == _loadedClipsContainer)
880
+ {
881
+ if (_loadedClipDropPlaceholder.parent == _loadedClipsContainer)
882
+ {
883
+ _loadedClipsContainer.Remove(_loadedClipDropPlaceholder);
884
+ }
885
+
886
+ if (_loadedClipDropPlaceholder != null)
887
+ {
888
+ _loadedClipDropPlaceholder.style.visibility = Visibility.Hidden;
889
+ }
890
+ }
891
+ }
892
+
893
+ private void CleanupLoadedClipDragState(int pointerIdToRelease)
894
+ {
895
+ if (_draggedLoadedClipElement != null)
896
+ {
897
+ if (
898
+ pointerIdToRelease != InvalidPointerId
899
+ && _draggedLoadedClipElement.HasPointerCapture(pointerIdToRelease)
900
+ )
901
+ {
902
+ _draggedLoadedClipElement.ReleasePointer(pointerIdToRelease);
903
+ }
904
+
905
+ _draggedLoadedClipElement.UnregisterCallback<PointerUpEvent>(
906
+ OnDraggedLoadedClipItemPointerUp
907
+ );
908
+
909
+ _draggedLoadedClipElement.RemoveFromClassList("frame-item-dragged");
910
+ _draggedLoadedClipElement = null;
911
+ }
912
+ _draggedLoadedClipOriginalIndex = -1;
913
+
914
+ _isClipDragPending = false;
915
+ if (_clipDragPendingElement != null)
916
+ {
917
+ if (
918
+ pointerIdToRelease != InvalidPointerId
919
+ && _clipDragPendingElement.HasPointerCapture(pointerIdToRelease)
920
+ )
921
+ {
922
+ _clipDragPendingElement.ReleasePointer(pointerIdToRelease);
923
+ }
924
+
925
+ _clipDragPendingElement = null;
926
+ }
927
+
928
+ if (_loadedClipDropPlaceholder != null)
929
+ {
930
+ if (_loadedClipDropPlaceholder.parent == _loadedClipsContainer)
931
+ {
932
+ _loadedClipsContainer.Remove(_loadedClipDropPlaceholder);
933
+ }
934
+
935
+ _loadedClipDropPlaceholder.style.visibility = Visibility.Hidden;
936
+ }
937
+ DragAndDrop.SetGenericData("DraggedLoadedClipIndex", null);
938
+ }
939
+
940
+ private void RecreatePreviewImage()
941
+ {
942
+ if (_animationPreview != null)
943
+ {
944
+ if (_animationPreview.parent == _previewPanelHost)
945
+ {
946
+ _previewPanelHost.Remove(_animationPreview);
947
+ }
948
+
949
+ _animationPreview = null;
950
+ }
951
+
952
+ if (_previewPanelHost == null)
953
+ {
954
+ return;
955
+ }
956
+
957
+ List<AnimatedSpriteLayer> animatedSpriteLayers = new();
958
+ if (_loadedEditorLayers.Count > 0)
959
+ {
960
+ foreach (EditorLayerData editorLayer in _loadedEditorLayers)
961
+ {
962
+ animatedSpriteLayers.Add(new AnimatedSpriteLayer(editorLayer.Sprites));
963
+ }
964
+ }
965
+
966
+ _animationPreview = new LayeredImage(
967
+ animatedSpriteLayers,
968
+ Color.clear,
969
+ _currentPreviewFps,
970
+ updatesSelf: false
971
+ )
972
+ {
973
+ name = "animationPreviewElement",
974
+ };
975
+
976
+ _previewPanelHost.Add(_animationPreview);
977
+ }
978
+
979
+ private void OnFramesContainerDragUpdated(DragUpdatedEvent evt)
980
+ {
981
+ object draggedIndexData = DragAndDrop.GetGenericData("DraggedFrameDataIndex");
982
+ if (draggedIndexData != null && _draggedFrameElement != null)
983
+ {
984
+ DragAndDrop.visualMode = DragAndDropVisualMode.Move;
985
+ float mouseY = evt.localMousePosition.y;
986
+ int newVisualIndex = -1;
987
+
988
+ if (_frameDropPlaceholder.parent == _framesContainer)
989
+ {
990
+ _framesContainer.Remove(_frameDropPlaceholder);
991
+ }
992
+
993
+ for (int i = 0; i < _framesContainer.childCount; i++)
994
+ {
995
+ VisualElement child = _framesContainer[i];
996
+ if (child == _draggedFrameElement)
997
+ {
998
+ continue;
999
+ }
1000
+
1001
+ float childMidY = child.layout.yMin + child.layout.height / 2f;
1002
+ if (mouseY < childMidY)
1003
+ {
1004
+ newVisualIndex = i;
1005
+ break;
1006
+ }
1007
+ }
1008
+ if (
1009
+ newVisualIndex < 0
1010
+ && _framesContainer.childCount > 0
1011
+ && _draggedFrameElement
1012
+ != _framesContainer.ElementAt(_framesContainer.childCount - 1)
1013
+ )
1014
+ {
1015
+ newVisualIndex = _framesContainer.childCount;
1016
+ }
1017
+
1018
+ if (0 <= newVisualIndex)
1019
+ {
1020
+ _framesContainer.Insert(newVisualIndex, _frameDropPlaceholder);
1021
+ _frameDropPlaceholder.style.visibility = Visibility.Visible;
1022
+ }
1023
+ else if (_framesContainer.childCount == 0 && _draggedFrameElement != null)
1024
+ {
1025
+ _framesContainer.Add(_frameDropPlaceholder);
1026
+ _frameDropPlaceholder.style.visibility = Visibility.Visible;
1027
+ }
1028
+ else
1029
+ {
1030
+ if (_frameDropPlaceholder.parent == _framesContainer)
1031
+ {
1032
+ _framesContainer.Remove(_frameDropPlaceholder);
1033
+ }
1034
+
1035
+ _frameDropPlaceholder.style.visibility = Visibility.Hidden;
1036
+ }
1037
+ }
1038
+ else
1039
+ {
1040
+ DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
1041
+ }
1042
+ evt.StopPropagation();
1043
+ }
1044
+
1045
+ private void OnFramesContainerDragPerform(DragPerformEvent evt)
1046
+ {
1047
+ object draggedIndexData = DragAndDrop.GetGenericData("DraggedFrameDataIndex");
1048
+ if (
1049
+ draggedIndexData != null
1050
+ && _draggedFrameElement != null
1051
+ && _activeEditorLayer != null
1052
+ )
1053
+ {
1054
+ int originalDataIndex = (int)draggedIndexData;
1055
+
1056
+ if (originalDataIndex < 0 || originalDataIndex >= _activeEditorLayer.Sprites.Count)
1057
+ {
1058
+ this.LogError(
1059
+ $"DragPerform (Frames): Stale or invalid dragged index. Aborting drop."
1060
+ );
1061
+ CleanupFrameDragState(InvalidPointerId);
1062
+ evt.StopPropagation();
1063
+ return;
1064
+ }
1065
+
1066
+ Sprite movedSprite = _activeEditorLayer.Sprites[originalDataIndex];
1067
+ _activeEditorLayer.Sprites.RemoveAt(originalDataIndex);
1068
+
1069
+ int placeholderVisualIndex = _framesContainer.IndexOf(_frameDropPlaceholder);
1070
+ int targetDataIndex;
1071
+
1072
+ if (0 <= placeholderVisualIndex)
1073
+ {
1074
+ int itemsBeforePlaceholder = 0;
1075
+ for (int i = 0; i < placeholderVisualIndex; i++)
1076
+ {
1077
+ if (
1078
+ _framesContainer[i] != _draggedFrameElement
1079
+ && _framesContainer[i] != _frameDropPlaceholder
1080
+ )
1081
+ {
1082
+ itemsBeforePlaceholder++;
1083
+ }
1084
+ }
1085
+ targetDataIndex = itemsBeforePlaceholder;
1086
+ }
1087
+ else
1088
+ {
1089
+ targetDataIndex = _activeEditorLayer.Sprites.Count;
1090
+ }
1091
+ targetDataIndex = Mathf.Clamp(targetDataIndex, 0, _activeEditorLayer.Sprites.Count);
1092
+ _activeEditorLayer.Sprites.Insert(targetDataIndex, movedSprite);
1093
+
1094
+ DragAndDrop.AcceptDrag();
1095
+ CleanupFrameDragState(InvalidPointerId);
1096
+
1097
+ RebuildFramesListUI();
1098
+ RecreatePreviewImage();
1099
+ }
1100
+ else
1101
+ {
1102
+ if (DragAndDrop.GetGenericData("DraggedFrameDataIndex") != null)
1103
+ {
1104
+ CleanupFrameDragState(InvalidPointerId);
1105
+ }
1106
+ }
1107
+ evt.StopPropagation();
1108
+ }
1109
+
1110
+ private void OnFramesContainerDragLeave(DragLeaveEvent evt)
1111
+ {
1112
+ if (evt.target == _framesContainer)
1113
+ {
1114
+ if (_frameDropPlaceholder.parent == _framesContainer)
1115
+ {
1116
+ _framesContainer.Remove(_frameDropPlaceholder);
1117
+ }
1118
+
1119
+ if (_frameDropPlaceholder != null)
1120
+ {
1121
+ _frameDropPlaceholder.style.visibility = Visibility.Hidden;
1122
+ }
1123
+ }
1124
+
1125
+ CleanupFrameDragState(InvalidPointerId);
1126
+ }
1127
+
1128
+ private void CleanupFrameDragState(int pointerIdToRelease)
1129
+ {
1130
+ this.Log($"Cleaning up frame drag state with pointer {pointerIdToRelease}");
1131
+ if (_draggedFrameElement != null)
1132
+ {
1133
+ if (
1134
+ pointerIdToRelease != InvalidPointerId
1135
+ && _draggedFrameElement.HasPointerCapture(pointerIdToRelease)
1136
+ )
1137
+ {
1138
+ _draggedFrameElement.ReleasePointer(pointerIdToRelease);
1139
+ }
1140
+
1141
+ _draggedFrameElement.UnregisterCallback<PointerUpEvent>(
1142
+ OnDraggedFrameItemPointerUp
1143
+ );
1144
+ _draggedFrameElement.RemoveFromClassList("frame-item-dragged");
1145
+ _draggedFrameElement = null;
1146
+ }
1147
+ _draggedFrameOriginalDataIndex = -1;
1148
+
1149
+ if (_frameDropPlaceholder != null)
1150
+ {
1151
+ if (_frameDropPlaceholder.parent == _framesContainer)
1152
+ {
1153
+ _framesContainer.Remove(_frameDropPlaceholder);
1154
+ }
1155
+
1156
+ _frameDropPlaceholder.style.visibility = Visibility.Hidden;
1157
+ }
1158
+ DragAndDrop.SetGenericData("DraggedFrameDataIndex", null);
1159
+ }
1160
+
1161
+ private void UpdateFpsDebugLabelForActiveLayer()
1162
+ {
1163
+ if (_activeEditorLayer == null)
1164
+ {
1165
+ _fpsDebugLabel.text = "Detected FPS Info: No active clip.";
1166
+ return;
1167
+ }
1168
+
1169
+ _fpsDebugLabel.text =
1170
+ $"Active Clip FPS (Original): {FormatFps(_activeEditorLayer.OriginalClipFps)}fps. Preview uses global FPS.";
1171
+ }
1172
+
1173
+ private static string FormatFps(float fps)
1174
+ {
1175
+ return fps.ToString("F1");
1176
+ }
1177
+
1178
+ private void RebuildFramesListUI()
1179
+ {
1180
+ _framesContainer.Clear();
1181
+
1182
+ if (_frameDropPlaceholder != null && _frameDropPlaceholder.parent == _framesContainer)
1183
+ {
1184
+ _framesContainer.Remove(_frameDropPlaceholder);
1185
+ }
1186
+
1187
+ if (_activeEditorLayer?.Sprites == null)
1188
+ {
1189
+ return;
1190
+ }
1191
+
1192
+ for (int i = 0; i < _activeEditorLayer.Sprites.Count; i++)
1193
+ {
1194
+ Sprite sprite = _activeEditorLayer.Sprites[i];
1195
+
1196
+ VisualElement frameElement = new();
1197
+ frameElement.AddToClassList("frame-item");
1198
+
1199
+ Image frameImage = new() { sprite = sprite, scaleMode = ScaleMode.ScaleToFit };
1200
+ frameImage.AddToClassList("frame-image");
1201
+ frameElement.Add(frameImage);
1202
+
1203
+ VisualElement frameInfo = new();
1204
+ frameInfo.AddToClassList("frame-info");
1205
+ frameInfo.Add(new Label($"Frame: {i + 1}"));
1206
+ frameInfo.Add(new Label($"Sprite: {(sprite != null ? sprite.name : "(None)")}"));
1207
+ frameElement.Add(frameInfo);
1208
+
1209
+ IntegerField indexField = new(null) { value = i + 1 };
1210
+ indexField.AddToClassList("frame-index-field");
1211
+ frameElement.Add(indexField);
1212
+
1213
+ VisualElement orderFieldContainer = new()
1214
+ {
1215
+ style =
1216
+ {
1217
+ flexDirection = FlexDirection.Row,
1218
+ alignItems = Align.Center,
1219
+ marginLeft = StyleKeyword.Auto,
1220
+ },
1221
+ };
1222
+
1223
+ Label orderLabel = new("Order:") { style = { marginRight = 3 } };
1224
+ orderFieldContainer.Add(orderLabel);
1225
+ orderFieldContainer.Add(indexField);
1226
+ frameElement.Add(orderFieldContainer);
1227
+
1228
+ int currentDataIndex = i;
1229
+ frameElement.RegisterCallback<PointerDownEvent>(evt =>
1230
+ OnFrameItemPointerDown(evt, frameElement, currentDataIndex)
1231
+ );
1232
+
1233
+ indexField.userData = currentDataIndex;
1234
+
1235
+ indexField.RegisterCallback<FocusInEvent>(_ =>
1236
+ {
1237
+ CleanupFrameDragState(InvalidPointerId);
1238
+ CleanupLoadedClipDragState(InvalidPointerId);
1239
+ });
1240
+
1241
+ indexField.RegisterCallback<FocusOutEvent>(_ =>
1242
+ OnFrameIndexFieldChanged(indexField)
1243
+ );
1244
+ indexField.RegisterCallback<KeyDownEvent>(evt =>
1245
+ {
1246
+ if (evt.keyCode is KeyCode.Return or KeyCode.KeypadEnter)
1247
+ {
1248
+ OnFrameIndexFieldChanged(indexField);
1249
+ indexField.Blur();
1250
+ }
1251
+ });
1252
+
1253
+ _framesContainer.Add(frameElement);
1254
+ }
1255
+ }
1256
+
1257
+ private void OnFrameIndexFieldChanged(IntegerField field)
1258
+ {
1259
+ CleanupFrameDragState(InvalidPointerId);
1260
+ CleanupLoadedClipDragState(InvalidPointerId);
1261
+ if (_activeEditorLayer?.Sprites == null)
1262
+ {
1263
+ return;
1264
+ }
1265
+
1266
+ int originalDataIndex = (int)field.userData;
1267
+ int newUiIndex = field.value;
1268
+ int newRequestedDataIndex = newUiIndex - 1;
1269
+ int newClampedDataIndex = Mathf.Clamp(
1270
+ newRequestedDataIndex,
1271
+ 0,
1272
+ _activeEditorLayer.Sprites.Count - 1
1273
+ );
1274
+
1275
+ if (newClampedDataIndex != originalDataIndex)
1276
+ {
1277
+ if (originalDataIndex < 0 || originalDataIndex >= _activeEditorLayer.Sprites.Count)
1278
+ {
1279
+ this.LogWarn(
1280
+ $"Original index {originalDataIndex} out of bounds. Rebuilding UI to correct."
1281
+ );
1282
+ RebuildFramesListUI();
1283
+ return;
1284
+ }
1285
+ Sprite spriteToMove = _activeEditorLayer.Sprites[originalDataIndex];
1286
+ _activeEditorLayer.Sprites.RemoveAt(originalDataIndex);
1287
+ _activeEditorLayer.Sprites.Insert(newClampedDataIndex, spriteToMove);
1288
+ RebuildFramesListUI();
1289
+ RecreatePreviewImage();
1290
+ }
1291
+ else if (newUiIndex - 1 != newClampedDataIndex)
1292
+ {
1293
+ RebuildFramesListUI();
1294
+ }
1295
+ }
1296
+
1297
+ private void OnFrameItemPointerDown(
1298
+ PointerDownEvent evt,
1299
+ VisualElement frameElement,
1300
+ int originalDataIndex
1301
+ )
1302
+ {
1303
+ if (evt.button != 0 || _draggedFrameElement != null)
1304
+ {
1305
+ return;
1306
+ }
1307
+
1308
+ if (
1309
+ _activeEditorLayer == null
1310
+ || originalDataIndex < 0
1311
+ || originalDataIndex >= _activeEditorLayer.Sprites.Count
1312
+ )
1313
+ {
1314
+ this.LogError(
1315
+ $"OnFrameItemPointerDown: Invalid originalDataIndex ({originalDataIndex}) or no active layer. Sprite count: {_activeEditorLayer?.Sprites?.Count ?? -1}"
1316
+ );
1317
+ return;
1318
+ }
1319
+
1320
+ _draggedFrameElement = frameElement;
1321
+ _draggedFrameOriginalDataIndex = originalDataIndex;
1322
+
1323
+ try
1324
+ {
1325
+ _draggedFrameElement.RegisterCallback<PointerUpEvent>(OnDraggedFrameItemPointerUp);
1326
+ _draggedFrameElement.AddToClassList("frame-item-dragged");
1327
+
1328
+ DragAndDrop.PrepareStartDrag();
1329
+ DragAndDrop.SetGenericData("DraggedFrameDataIndex", _draggedFrameOriginalDataIndex);
1330
+
1331
+ Object dragContextObject =
1332
+ _activeEditorLayer.SourceClip ?? (Object)CreateInstance<ScriptableObject>();
1333
+ if (dragContextObject == null)
1334
+ {
1335
+ this.LogError($"Failed to create dragContextObject for frame drag.");
1336
+
1337
+ _draggedFrameElement.ReleasePointer(evt.pointerId);
1338
+ _draggedFrameElement.UnregisterCallback<PointerUpEvent>(
1339
+ OnDraggedFrameItemPointerUp
1340
+ );
1341
+ _draggedFrameElement.RemoveFromClassList("frame-item-dragged");
1342
+ _draggedFrameElement = null;
1343
+ return;
1344
+ }
1345
+ DragAndDrop.objectReferences = new[] { dragContextObject };
1346
+
1347
+ Sprite spriteBeingDragged = _activeEditorLayer.Sprites[originalDataIndex];
1348
+ string dragTitle;
1349
+ if (spriteBeingDragged != null)
1350
+ {
1351
+ dragTitle = !string.IsNullOrWhiteSpace(spriteBeingDragged.name)
1352
+ ? spriteBeingDragged.name
1353
+ : $"Unnamed Sprite Frame {originalDataIndex + 1}";
1354
+ }
1355
+ else
1356
+ {
1357
+ dragTitle = $"Empty Frame {originalDataIndex + 1}";
1358
+ }
1359
+
1360
+ if (string.IsNullOrWhiteSpace(dragTitle))
1361
+ {
1362
+ dragTitle = "Dragging Frame";
1363
+ }
1364
+
1365
+ DragAndDrop.StartDrag(dragTitle);
1366
+ }
1367
+ catch (Exception e)
1368
+ {
1369
+ this.LogError(
1370
+ $"Exception during OnFrameItemPointerDown before StartDrag: {e.Message}\n{e.StackTrace}"
1371
+ );
1372
+
1373
+ if (_draggedFrameElement != null)
1374
+ {
1375
+ if (_draggedFrameElement.HasPointerCapture(evt.pointerId))
1376
+ {
1377
+ _draggedFrameElement.ReleasePointer(evt.pointerId);
1378
+ }
1379
+
1380
+ _draggedFrameElement.UnregisterCallback<PointerUpEvent>(
1381
+ OnDraggedFrameItemPointerUp
1382
+ );
1383
+ _draggedFrameElement.RemoveFromClassList("frame-item-dragged");
1384
+ _draggedFrameElement = null;
1385
+ }
1386
+ _draggedFrameOriginalDataIndex = -1;
1387
+ }
1388
+ }
1389
+
1390
+ private void OnDraggedFrameItemPointerUp(PointerUpEvent evt)
1391
+ {
1392
+ if (_draggedFrameElement == null || evt.currentTarget != _draggedFrameElement)
1393
+ {
1394
+ return;
1395
+ }
1396
+
1397
+ if (DragAndDrop.GetGenericData("DraggedFrameDataIndex") != null)
1398
+ {
1399
+ CleanupFrameDragState(evt.pointerId);
1400
+ }
1401
+ else if (
1402
+ _draggedFrameElement != null
1403
+ && _draggedFrameElement.HasPointerCapture(evt.pointerId)
1404
+ )
1405
+ {
1406
+ _draggedFrameElement.ReleasePointer(evt.pointerId);
1407
+ _draggedFrameElement.UnregisterCallback<PointerUpEvent>(
1408
+ OnDraggedFrameItemPointerUp
1409
+ );
1410
+ _draggedFrameElement.RemoveFromClassList("frame-item-dragged");
1411
+ _draggedFrameElement = null;
1412
+ }
1413
+ evt.StopPropagation();
1414
+ }
1415
+
1416
+ private void OnApplyFpsToPreviewClicked()
1417
+ {
1418
+ _currentPreviewFps = Mathf.Max(0.1f, _fpsField.value);
1419
+ _fpsField.SetValueWithoutNotify(_currentPreviewFps);
1420
+ if (_animationPreview != null)
1421
+ {
1422
+ _animationPreview.Fps = _currentPreviewFps;
1423
+ }
1424
+ }
1425
+
1426
+ private void OnSaveClipClicked()
1427
+ {
1428
+ if (_activeEditorLayer == null || _activeEditorLayer.SourceClip == null)
1429
+ {
1430
+ this.LogError($"No active animation clip to save.");
1431
+ return;
1432
+ }
1433
+
1434
+ AnimationClip clipToSave = _activeEditorLayer.SourceClip;
1435
+ string bindingPath = _activeEditorLayer.BindingPath;
1436
+
1437
+ EditorCurveBinding spriteBinding = default;
1438
+ bool bindingFound = false;
1439
+ EditorCurveBinding[] allBindings = AnimationUtility.GetObjectReferenceCurveBindings(
1440
+ clipToSave
1441
+ );
1442
+
1443
+ foreach (EditorCurveBinding b in allBindings)
1444
+ {
1445
+ if (
1446
+ b.type == typeof(SpriteRenderer)
1447
+ && b.propertyName == "m_Sprite"
1448
+ && (string.IsNullOrWhiteSpace(bindingPath) || b.path == bindingPath)
1449
+ )
1450
+ {
1451
+ spriteBinding = b;
1452
+ bindingFound = true;
1453
+ break;
1454
+ }
1455
+ }
1456
+
1457
+ if (!bindingFound)
1458
+ {
1459
+ foreach (EditorCurveBinding b in allBindings)
1460
+ {
1461
+ if (b.type == typeof(SpriteRenderer) && b.propertyName == "m_Sprite")
1462
+ {
1463
+ spriteBinding = b;
1464
+ bindingFound = true;
1465
+ this.LogWarn(
1466
+ $"Saving to first available m_Sprite binding on '{clipToSave.name}' as specific path '{bindingPath}' was not found or empty."
1467
+ );
1468
+ break;
1469
+ }
1470
+ }
1471
+ }
1472
+
1473
+ if (!bindingFound)
1474
+ {
1475
+ this.LogError(
1476
+ $"Cannot save '{clipToSave.name}': No SpriteRenderer m_Sprite binding found (Path Hint: '{bindingPath}'). Clip might be empty or not a sprite animation."
1477
+ );
1478
+ return;
1479
+ }
1480
+
1481
+ List<Sprite> spritesToSave = _activeEditorLayer.Sprites;
1482
+ ObjectReferenceKeyframe[] newKeyframes = new ObjectReferenceKeyframe[
1483
+ spritesToSave.Count
1484
+ ];
1485
+ float timePerFrame = _currentPreviewFps > 0 ? 1.0f / _currentPreviewFps : 0f;
1486
+
1487
+ for (int i = 0; i < spritesToSave.Count; i++)
1488
+ {
1489
+ newKeyframes[i] = new ObjectReferenceKeyframe
1490
+ {
1491
+ time = i * timePerFrame,
1492
+ value = spritesToSave[i],
1493
+ };
1494
+ }
1495
+
1496
+ Undo.RecordObject(clipToSave, "Modify Animation Clip Frames");
1497
+ AnimationUtility.SetObjectReferenceCurve(clipToSave, spriteBinding, newKeyframes);
1498
+ clipToSave.frameRate = _currentPreviewFps;
1499
+
1500
+ EditorUtility.SetDirty(clipToSave);
1501
+ AssetDatabase.SaveAssets();
1502
+
1503
+ this.Log(
1504
+ $"Animation clip '{clipToSave.name}' saved with {spritesToSave.Count} frames at {_currentPreviewFps} FPS."
1505
+ );
1506
+ }
1507
+
1508
+ private void OnDisable()
1509
+ {
1510
+ CleanupFrameDragState(InvalidPointerId);
1511
+ CleanupLoadedClipDragState(InvalidPointerId);
1512
+
1513
+ if (_animationPreview != null && _animationPreview.parent == _previewPanelHost)
1514
+ {
1515
+ _previewPanelHost.Remove(_animationPreview);
1516
+ }
1517
+ _animationPreview = null;
1518
+ }
1519
+ }
1520
+ #endif
1521
+ }