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

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.
@@ -1,4 +1,5 @@
1
- namespace WallstopStudios.UnityHelpers.Editor.Sprites
1
+ // ReSharper disable HeapView.CanAvoidClosure
2
+ namespace WallstopStudios.UnityHelpers.Editor.Sprites
2
3
  {
3
4
  #if UNITY_EDITOR
4
5
  using System;
@@ -13,7 +14,6 @@
13
14
  using UnityEditor.UIElements;
14
15
  using UnityEngine;
15
16
  using UnityEngine.UIElements;
16
- using Debug = UnityEngine.Debug;
17
17
  using Object = UnityEngine.Object;
18
18
 
19
19
  public sealed class SpriteSheetAnimationCreator : EditorWindow
@@ -43,6 +43,14 @@
43
43
  private Button _nextFrameButton;
44
44
  private Slider _previewScrubber;
45
45
 
46
+ private bool _isDraggingToSelectSprites;
47
+ private int _spriteSelectionDragStartIndex = -1;
48
+ private int _spriteSelectionDragCurrentIndex = -1;
49
+ private StyleColor _selectedThumbnailBackgroundColor = new(
50
+ new Color(0.2f, 0.5f, 0.8f, 0.4f)
51
+ );
52
+ private readonly StyleColor _defaultThumbnailBackgroundColor = new(StyleKeyword.Null);
53
+
46
54
  private bool _isPreviewing;
47
55
  private int _currentPreviewAnimDefIndex = -1;
48
56
  private int _currentPreviewSpriteIndex;
@@ -65,6 +73,8 @@
65
73
  public sealed class AnimationDefinition
66
74
  {
67
75
  public string Name = "New Animation";
76
+ public bool loop;
77
+ public float cycleOffset;
68
78
  public int StartSpriteIndex;
69
79
  public int EndSpriteIndex;
70
80
  public float DefaultFrameRate = 12f;
@@ -79,6 +89,8 @@
79
89
  public Label spriteCountLabel;
80
90
  public Button previewButton;
81
91
  public Button removeButton;
92
+ public Toggle loopingField;
93
+ public FloatField cycleOffsetField;
82
94
  }
83
95
 
84
96
  public SpriteSheetAnimationCreator()
@@ -161,7 +173,9 @@
161
173
  topSection.Add(_refreshSpritesButton);
162
174
  root.Add(topSection);
163
175
 
164
- Label thumbnailsLabel = new("Available Sprites:")
176
+ Label thumbnailsLabel = new(
177
+ "Available Sprites (Drag to select range for new animation):"
178
+ )
165
179
  {
166
180
  style =
167
181
  {
@@ -175,8 +189,8 @@
175
189
  {
176
190
  style =
177
191
  {
178
- height = ThumbnailSize + 20,
179
- minHeight = ThumbnailSize + 20,
192
+ height = ThumbnailSize + 20 + 10,
193
+ minHeight = ThumbnailSize + 20 + 10,
180
194
  borderTopWidth = 1,
181
195
  borderBottomWidth = 1,
182
196
  borderLeftWidth = 1,
@@ -196,6 +210,72 @@
196
210
  {
197
211
  style = { flexDirection = FlexDirection.Row },
198
212
  };
213
+
214
+ _spriteThumbnailsContainer.RegisterCallback<PointerMoveEvent>(evt =>
215
+ {
216
+ if (
217
+ _isDraggingToSelectSprites
218
+ && _spriteThumbnailsContainer.HasPointerCapture(evt.pointerId)
219
+ )
220
+ {
221
+ VisualElement currentElementOver = evt.target as VisualElement;
222
+
223
+ VisualElement thumbChild = currentElementOver;
224
+ while (thumbChild != null && thumbChild.parent != _spriteThumbnailsContainer)
225
+ {
226
+ thumbChild = thumbChild.parent;
227
+ }
228
+
229
+ if (
230
+ thumbChild is { userData: int hoveredIndex }
231
+ && _spriteSelectionDragCurrentIndex != hoveredIndex
232
+ )
233
+ {
234
+ _spriteSelectionDragCurrentIndex = hoveredIndex;
235
+ UpdateSpriteSelectionHighlight();
236
+ }
237
+ }
238
+ });
239
+
240
+ _spriteThumbnailsContainer.RegisterCallback<PointerUpEvent>(
241
+ evt =>
242
+ {
243
+ if (
244
+ evt.button == 0
245
+ && _isDraggingToSelectSprites
246
+ && _spriteThumbnailsContainer.HasPointerCapture(evt.pointerId)
247
+ )
248
+ {
249
+ _spriteThumbnailsContainer.ReleasePointer(evt.pointerId);
250
+ _isDraggingToSelectSprites = false;
251
+
252
+ if (
253
+ _spriteSelectionDragStartIndex != -1
254
+ && _spriteSelectionDragCurrentIndex != -1
255
+ )
256
+ {
257
+ int start = Mathf.Min(
258
+ _spriteSelectionDragStartIndex,
259
+ _spriteSelectionDragCurrentIndex
260
+ );
261
+ int end = Mathf.Max(
262
+ _spriteSelectionDragStartIndex,
263
+ _spriteSelectionDragCurrentIndex
264
+ );
265
+
266
+ if (start <= end)
267
+ {
268
+ CreateAnimationDefinitionFromSelection(start, end);
269
+ }
270
+ }
271
+ ClearSpriteSelectionHighlight();
272
+ _spriteSelectionDragStartIndex = -1;
273
+ _spriteSelectionDragCurrentIndex = -1;
274
+ }
275
+ },
276
+ TrickleDown.TrickleDown
277
+ );
278
+
199
279
  _spriteThumbnailsScrollView.Add(_spriteThumbnailsContainer);
200
280
  root.Add(_spriteThumbnailsScrollView);
201
281
 
@@ -424,6 +504,115 @@
424
504
  SetPreviewFrame(nextFrame);
425
505
  }
426
506
 
507
+ private void UpdateSpriteSelectionHighlight()
508
+ {
509
+ if (
510
+ !_isDraggingToSelectSprites
511
+ || _spriteSelectionDragStartIndex == -1
512
+ || _spriteSelectionDragCurrentIndex == -1
513
+ )
514
+ {
515
+ ClearSpriteSelectionHighlight();
516
+ return;
517
+ }
518
+
519
+ int minIdx = Mathf.Min(
520
+ _spriteSelectionDragStartIndex,
521
+ _spriteSelectionDragCurrentIndex
522
+ );
523
+ int maxIdx = Mathf.Max(
524
+ _spriteSelectionDragStartIndex,
525
+ _spriteSelectionDragCurrentIndex
526
+ );
527
+
528
+ for (int i = 0; i < _spriteThumbnailsContainer.childCount; i++)
529
+ {
530
+ VisualElement thumb = _spriteThumbnailsContainer.ElementAt(i);
531
+ if (thumb.userData is int thumbIndex)
532
+ {
533
+ if (thumbIndex >= minIdx && thumbIndex <= maxIdx)
534
+ {
535
+ thumb.style.backgroundColor = _selectedThumbnailBackgroundColor;
536
+ thumb.style.borderBottomColor =
537
+ _selectedThumbnailBackgroundColor.value * 1.5f;
538
+ thumb.style.borderTopColor = _selectedThumbnailBackgroundColor.value * 1.5f;
539
+ thumb.style.borderLeftColor =
540
+ _selectedThumbnailBackgroundColor.value * 1.5f;
541
+ thumb.style.borderRightColor =
542
+ _selectedThumbnailBackgroundColor.value * 1.5f;
543
+ }
544
+ else
545
+ {
546
+ thumb.style.backgroundColor = _defaultThumbnailBackgroundColor;
547
+ thumb.style.borderBottomColor = Color.clear;
548
+ thumb.style.borderTopColor = Color.clear;
549
+ thumb.style.borderLeftColor = Color.clear;
550
+ thumb.style.borderRightColor = Color.clear;
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ private void ClearSpriteSelectionHighlight()
557
+ {
558
+ for (int i = 0; i < _spriteThumbnailsContainer.childCount; i++)
559
+ {
560
+ VisualElement thumb = _spriteThumbnailsContainer.ElementAt(i);
561
+ thumb.style.backgroundColor = _defaultThumbnailBackgroundColor;
562
+ thumb.style.borderBottomColor = Color.clear;
563
+ thumb.style.borderTopColor = Color.clear;
564
+ thumb.style.borderLeftColor = Color.clear;
565
+ thumb.style.borderRightColor = Color.clear;
566
+ }
567
+ }
568
+
569
+ private void CreateAnimationDefinitionFromSelection(
570
+ int startSpriteIndex,
571
+ int endSpriteIndex
572
+ )
573
+ {
574
+ if (
575
+ startSpriteIndex < 0
576
+ || endSpriteIndex < 0
577
+ || startSpriteIndex >= _availableSprites.Count
578
+ || endSpriteIndex >= _availableSprites.Count
579
+ )
580
+ {
581
+ this.LogWarn(
582
+ $"Invalid sprite indices for new animation definition from selection."
583
+ );
584
+ return;
585
+ }
586
+
587
+ AnimationDefinition newDefinition = new()
588
+ {
589
+ Name =
590
+ _selectedSpriteSheet != null
591
+ ? $"{_selectedSpriteSheet.name}_Anim_{_animationDefinitions.Count}"
592
+ : $"New_Animation_{_animationDefinitions.Count}",
593
+ StartSpriteIndex = startSpriteIndex,
594
+ EndSpriteIndex = endSpriteIndex,
595
+ DefaultFrameRate = 12f,
596
+ };
597
+
598
+ newDefinition.FrameRateCurve = AnimationCurve.Constant(
599
+ 0,
600
+ 1,
601
+ newDefinition.DefaultFrameRate
602
+ );
603
+
604
+ _animationDefinitions.Add(newDefinition);
605
+ UpdateSpritesForDefinition(newDefinition);
606
+ _currentPreviewAnimDefIndex = _animationDefinitions.Count - 1;
607
+ StartOrUpdateCurrentPreview(newDefinition);
608
+ _animationDefinitionsListView.Rebuild();
609
+
610
+ if (_animationDefinitionsListView.itemsSource.Count > 0)
611
+ {
612
+ _animationDefinitionsListView.ScrollToItem(_animationDefinitions.Count - 1);
613
+ }
614
+ }
615
+
427
616
  private void OnSpriteSheetSelected(ChangeEvent<Object> evt)
428
617
  {
429
618
  _selectedSpriteSheet = evt.newValue as Texture2D;
@@ -540,7 +729,20 @@
540
729
  Sprite sprite = _availableSprites[i];
541
730
  VisualElement thumbContainer = new()
542
731
  {
543
- style = { alignItems = Align.Center, marginRight = 5 },
732
+ style =
733
+ {
734
+ alignItems = Align.Center,
735
+ marginRight = 5,
736
+ paddingBottom = 2,
737
+ borderBottomWidth = 1,
738
+ borderLeftWidth = 1,
739
+ borderRightWidth = 1,
740
+ borderTopWidth = 1,
741
+ borderBottomColor = Color.clear,
742
+ borderTopColor = Color.clear,
743
+ borderLeftColor = Color.clear,
744
+ borderRightColor = Color.clear,
745
+ },
544
746
  };
545
747
  Image img = new()
546
748
  {
@@ -550,6 +752,36 @@
550
752
  };
551
753
  thumbContainer.Add(img);
552
754
  thumbContainer.Add(new Label($"{i}") { style = { fontSize = 9 } });
755
+
756
+ int currentIndex = i;
757
+ thumbContainer.userData = currentIndex;
758
+
759
+ thumbContainer.RegisterCallback<PointerDownEvent>(evt =>
760
+ {
761
+ if (evt.button == 0)
762
+ {
763
+ _isDraggingToSelectSprites = true;
764
+ _spriteSelectionDragStartIndex = currentIndex;
765
+ _spriteSelectionDragCurrentIndex = currentIndex;
766
+ UpdateSpriteSelectionHighlight();
767
+
768
+ _spriteThumbnailsContainer.CapturePointer(evt.pointerId);
769
+ evt.StopPropagation();
770
+ }
771
+ });
772
+
773
+ thumbContainer.RegisterCallback<PointerEnterEvent>(_ =>
774
+ {
775
+ if (
776
+ _isDraggingToSelectSprites
777
+ && _spriteThumbnailsContainer.HasPointerCapture(PointerId.mousePointerId)
778
+ && _spriteSelectionDragCurrentIndex != currentIndex
779
+ )
780
+ {
781
+ _spriteSelectionDragCurrentIndex = currentIndex;
782
+ UpdateSpriteSelectionHighlight();
783
+ }
784
+ });
553
785
  _spriteThumbnailsContainer.Add(thumbContainer);
554
786
  }
555
787
  }
@@ -562,7 +794,7 @@
562
794
  }
563
795
  }
564
796
 
565
- private VisualElement MakeAnimationDefinitionItem()
797
+ private static VisualElement MakeAnimationDefinitionItem()
566
798
  {
567
799
  VisualElement container = new()
568
800
  {
@@ -597,6 +829,10 @@
597
829
  {
598
830
  style = { flexDirection = FlexDirection.Row, alignItems = Align.Center },
599
831
  };
832
+ VisualElement fourthRow = new()
833
+ {
834
+ style = { flexDirection = FlexDirection.Row, alignItems = Align.Center },
835
+ };
600
836
 
601
837
  TextField nameField = new("Name:")
602
838
  {
@@ -657,9 +893,33 @@
657
893
  thirdRow.Add(fpsField);
658
894
  thirdRow.Add(curveField);
659
895
 
896
+ Toggle looping = new("Looping:")
897
+ {
898
+ style =
899
+ {
900
+ flexGrow = 1,
901
+ flexShrink = 1,
902
+ marginRight = 5,
903
+ },
904
+ };
905
+
906
+ FloatField cycleOffset = new("Cycle Offset:")
907
+ {
908
+ style =
909
+ {
910
+ flexGrow = 1,
911
+ flexShrink = 1,
912
+ marginRight = 5,
913
+ },
914
+ };
915
+
916
+ fourthRow.Add(looping);
917
+ fourthRow.Add(cycleOffset);
918
+
660
919
  container.Add(firstRow);
661
920
  container.Add(secondRow);
662
921
  container.Add(thirdRow);
922
+ container.Add(fourthRow);
663
923
 
664
924
  container.userData = new AnimationDefUITags
665
925
  {
@@ -671,6 +931,8 @@
671
931
  spriteCountLabel = spriteCountLabel,
672
932
  previewButton = previewButton,
673
933
  removeButton = removeButton,
934
+ looping = looping,
935
+ cycleOffset = cycleOffset,
674
936
  };
675
937
  return container;
676
938
  }
@@ -685,11 +947,13 @@
685
947
  public Label spriteCountLabel;
686
948
  public Button previewButton;
687
949
  public Button removeButton;
950
+ public Toggle looping;
951
+ public FloatField cycleOffset;
688
952
  }
689
953
 
690
954
  private void BindAnimationDefinitionItem(VisualElement element, int index)
691
955
  {
692
- AnimationDefinition def = _animationDefinitions[index];
956
+ AnimationDefinition definition = _animationDefinitions[index];
693
957
  if (element.userData is not AnimationDefUITags tags)
694
958
  {
695
959
  this.LogError(
@@ -698,143 +962,178 @@
698
962
  return;
699
963
  }
700
964
 
701
- def.nameField?.UnregisterValueChangedCallback(
702
- def.nameField.userData as EventCallback<ChangeEvent<string>>
965
+ definition.nameField?.UnregisterValueChangedCallback(
966
+ definition.nameField.userData as EventCallback<ChangeEvent<string>>
703
967
  );
704
968
 
705
- def.startIndexField?.UnregisterValueChangedCallback(
706
- def.startIndexField.userData as EventCallback<ChangeEvent<int>>
969
+ definition.startIndexField?.UnregisterValueChangedCallback(
970
+ definition.startIndexField.userData as EventCallback<ChangeEvent<int>>
707
971
  );
708
972
 
709
- def.endIndexField?.UnregisterValueChangedCallback(
710
- def.endIndexField.userData as EventCallback<ChangeEvent<int>>
973
+ definition.endIndexField?.UnregisterValueChangedCallback(
974
+ definition.endIndexField.userData as EventCallback<ChangeEvent<int>>
711
975
  );
712
976
 
713
- def.defaultFrameRateField?.UnregisterValueChangedCallback(
714
- def.defaultFrameRateField.userData as EventCallback<ChangeEvent<float>>
977
+ definition.defaultFrameRateField?.UnregisterValueChangedCallback(
978
+ definition.defaultFrameRateField.userData as EventCallback<ChangeEvent<float>>
715
979
  );
716
980
 
717
- def.frameRateCurveField?.UnregisterValueChangedCallback(
718
- def.frameRateCurveField.userData as EventCallback<ChangeEvent<AnimationCurve>>
981
+ definition.frameRateCurveField?.UnregisterValueChangedCallback(
982
+ definition.frameRateCurveField.userData
983
+ as EventCallback<ChangeEvent<AnimationCurve>>
719
984
  );
720
985
 
721
- if (def.removeButton != null)
986
+ if (definition.removeButton != null)
722
987
  {
723
- def.removeButton.clicked -= (Action)def.removeButton.userData;
988
+ definition.removeButton.clicked -= (Action)definition.removeButton.userData;
724
989
  }
725
990
 
726
- if (def.previewButton != null)
991
+ if (definition.previewButton != null)
727
992
  {
728
- def.previewButton.clicked -= (Action)def.previewButton.userData;
993
+ definition.previewButton.clicked -= (Action)definition.previewButton.userData;
729
994
  }
730
995
 
731
- def.nameField = tags.nameField;
732
- def.startIndexField = tags.startIndexField;
733
- def.endIndexField = tags.endIndexField;
734
- def.defaultFrameRateField = tags.defaultFrameRateField;
735
- def.frameRateCurveField = tags.frameRateCurveField;
736
- def.spriteCountLabel = tags.spriteCountLabel;
737
- def.removeButton = tags.removeButton;
738
- def.previewButton = tags.previewButton;
996
+ definition.loopingField?.UnregisterValueChangedCallback(
997
+ (EventCallback<ChangeEvent<bool>>)definition.loopingField.userData
998
+ );
999
+ definition.cycleOffsetField?.UnregisterValueChangedCallback(
1000
+ (EventCallback<ChangeEvent<float>>)definition.cycleOffsetField.userData
1001
+ );
739
1002
 
740
- def.nameField.SetValueWithoutNotify(def.Name);
1003
+ definition.nameField = tags.nameField;
1004
+ definition.startIndexField = tags.startIndexField;
1005
+ definition.endIndexField = tags.endIndexField;
1006
+ definition.defaultFrameRateField = tags.defaultFrameRateField;
1007
+ definition.frameRateCurveField = tags.frameRateCurveField;
1008
+ definition.spriteCountLabel = tags.spriteCountLabel;
1009
+ definition.removeButton = tags.removeButton;
1010
+ definition.previewButton = tags.previewButton;
1011
+ definition.loopingField = tags.looping;
1012
+ definition.cycleOffsetField = tags.cycleOffset;
1013
+
1014
+ definition.nameField.SetValueWithoutNotify(definition.Name);
741
1015
  EventCallback<ChangeEvent<string>> nameChangeCallback = evt =>
742
1016
  {
743
- def.Name = evt.newValue;
1017
+ definition.Name = evt.newValue;
744
1018
  };
745
- def.nameField.RegisterValueChangedCallback(nameChangeCallback);
746
- def.nameField.userData = nameChangeCallback;
1019
+ definition.nameField.RegisterValueChangedCallback(nameChangeCallback);
1020
+ definition.nameField.userData = nameChangeCallback;
747
1021
 
748
- def.startIndexField.SetValueWithoutNotify(def.StartSpriteIndex);
1022
+ definition.startIndexField.SetValueWithoutNotify(definition.StartSpriteIndex);
749
1023
  EventCallback<ChangeEvent<int>> startChangeCallback = evt =>
750
1024
  {
751
- def.StartSpriteIndex = Mathf.Clamp(
1025
+ definition.StartSpriteIndex = Mathf.Clamp(
752
1026
  evt.newValue,
753
1027
  0,
754
1028
  _availableSprites.Count > 0 ? _availableSprites.Count - 1 : 0
755
1029
  );
756
- if (def.StartSpriteIndex > def.EndSpriteIndex && 0 < _availableSprites.Count)
1030
+ if (
1031
+ definition.StartSpriteIndex > definition.EndSpriteIndex
1032
+ && 0 < _availableSprites.Count
1033
+ )
757
1034
  {
758
- def.EndSpriteIndex = def.StartSpriteIndex;
1035
+ definition.EndSpriteIndex = definition.StartSpriteIndex;
759
1036
  }
760
1037
 
761
- def.startIndexField.SetValueWithoutNotify(def.StartSpriteIndex);
762
- UpdateSpritesForDefinition(def);
1038
+ definition.startIndexField.SetValueWithoutNotify(definition.StartSpriteIndex);
1039
+ UpdateSpritesForDefinition(definition);
763
1040
  if (_currentPreviewAnimDefIndex == index)
764
1041
  {
765
- StartOrUpdateCurrentPreview(def);
1042
+ StartOrUpdateCurrentPreview(definition);
766
1043
  }
767
1044
  };
768
- def.startIndexField.RegisterValueChangedCallback(startChangeCallback);
769
- def.startIndexField.userData = startChangeCallback;
1045
+ definition.startIndexField.RegisterValueChangedCallback(startChangeCallback);
1046
+ definition.startIndexField.userData = startChangeCallback;
770
1047
 
771
- def.endIndexField.SetValueWithoutNotify(def.EndSpriteIndex);
1048
+ definition.endIndexField.SetValueWithoutNotify(definition.EndSpriteIndex);
772
1049
  EventCallback<ChangeEvent<int>> endChangeCallback = evt =>
773
1050
  {
774
- def.EndSpriteIndex = Mathf.Clamp(
1051
+ definition.EndSpriteIndex = Mathf.Clamp(
775
1052
  evt.newValue,
776
1053
  0,
777
1054
  _availableSprites.Count > 0 ? _availableSprites.Count - 1 : 0
778
1055
  );
779
- if (def.EndSpriteIndex < def.StartSpriteIndex && 0 < _availableSprites.Count)
1056
+ if (
1057
+ definition.EndSpriteIndex < definition.StartSpriteIndex
1058
+ && 0 < _availableSprites.Count
1059
+ )
780
1060
  {
781
- def.StartSpriteIndex = def.EndSpriteIndex;
1061
+ definition.StartSpriteIndex = definition.EndSpriteIndex;
782
1062
  }
783
1063
 
784
- def.endIndexField.SetValueWithoutNotify(def.EndSpriteIndex);
785
- UpdateSpritesForDefinition(def);
1064
+ definition.endIndexField.SetValueWithoutNotify(definition.EndSpriteIndex);
1065
+ UpdateSpritesForDefinition(definition);
786
1066
  if (_currentPreviewAnimDefIndex == index)
787
1067
  {
788
- StartOrUpdateCurrentPreview(def);
1068
+ StartOrUpdateCurrentPreview(definition);
789
1069
  }
790
1070
  };
791
- def.endIndexField.RegisterValueChangedCallback(endChangeCallback);
792
- def.endIndexField.userData = endChangeCallback;
1071
+ definition.endIndexField.RegisterValueChangedCallback(endChangeCallback);
1072
+ definition.endIndexField.userData = endChangeCallback;
793
1073
 
794
- def.defaultFrameRateField.SetValueWithoutNotify(def.DefaultFrameRate);
1074
+ definition.defaultFrameRateField.SetValueWithoutNotify(definition.DefaultFrameRate);
795
1075
  EventCallback<ChangeEvent<float>> fpsChangeCallback = evt =>
796
1076
  {
797
- def.DefaultFrameRate = Mathf.Max(0.1f, evt.newValue);
798
- def.defaultFrameRateField.SetValueWithoutNotify(def.DefaultFrameRate);
1077
+ definition.DefaultFrameRate = Mathf.Max(0.1f, evt.newValue);
1078
+ definition.defaultFrameRateField.SetValueWithoutNotify(definition.DefaultFrameRate);
799
1079
 
800
- if (IsCurveConstant(def.FrameRateCurve))
1080
+ if (IsCurveConstant(definition.FrameRateCurve))
801
1081
  {
802
- def.FrameRateCurve = AnimationCurve.Constant(0, 1, def.DefaultFrameRate);
803
- def.frameRateCurveField.SetValueWithoutNotify(def.FrameRateCurve);
1082
+ definition.FrameRateCurve = AnimationCurve.Constant(
1083
+ 0,
1084
+ 1,
1085
+ definition.DefaultFrameRate
1086
+ );
1087
+ definition.frameRateCurveField.SetValueWithoutNotify(definition.FrameRateCurve);
804
1088
  }
805
1089
  if (_currentPreviewAnimDefIndex == index)
806
1090
  {
807
- StartOrUpdateCurrentPreview(def);
1091
+ StartOrUpdateCurrentPreview(definition);
808
1092
  }
809
1093
  };
810
- def.defaultFrameRateField.RegisterValueChangedCallback(fpsChangeCallback);
811
- def.defaultFrameRateField.userData = fpsChangeCallback;
1094
+ definition.defaultFrameRateField.RegisterValueChangedCallback(fpsChangeCallback);
1095
+ definition.defaultFrameRateField.userData = fpsChangeCallback;
812
1096
 
813
- def.frameRateCurveField.SetValueWithoutNotify(def.FrameRateCurve);
1097
+ definition.frameRateCurveField.SetValueWithoutNotify(definition.FrameRateCurve);
814
1098
  EventCallback<ChangeEvent<AnimationCurve>> curveChangeCallback = evt =>
815
1099
  {
816
- def.FrameRateCurve = evt.newValue;
1100
+ definition.FrameRateCurve = evt.newValue;
817
1101
  if (_currentPreviewAnimDefIndex == index)
818
1102
  {
819
- StartOrUpdateCurrentPreview(def);
1103
+ StartOrUpdateCurrentPreview(definition);
820
1104
  }
821
1105
  };
822
- def.frameRateCurveField.RegisterValueChangedCallback(curveChangeCallback);
823
- def.frameRateCurveField.userData = curveChangeCallback;
1106
+ definition.frameRateCurveField.RegisterValueChangedCallback(curveChangeCallback);
1107
+ definition.frameRateCurveField.userData = curveChangeCallback;
824
1108
 
825
1109
  Action removeAction = () => RemoveAnimationDefinition(index);
826
- def.removeButton.clicked += removeAction;
827
- def.removeButton.userData = removeAction;
1110
+ definition.removeButton.clicked += removeAction;
1111
+ definition.removeButton.userData = removeAction;
828
1112
 
829
1113
  Action previewAction = () =>
830
1114
  {
831
1115
  _currentPreviewAnimDefIndex = index;
832
- StartOrUpdateCurrentPreview(def);
1116
+ StartOrUpdateCurrentPreview(definition);
1117
+ };
1118
+ definition.previewButton.clicked += previewAction;
1119
+ definition.previewButton.userData = previewAction;
1120
+
1121
+ EventCallback<ChangeEvent<bool>> loopingChangeCallback = evt =>
1122
+ {
1123
+ definition.loop = evt.newValue;
833
1124
  };
834
- def.previewButton.clicked += previewAction;
835
- def.previewButton.userData = previewAction;
836
1125
 
837
- UpdateSpritesForDefinition(def);
1126
+ definition.loopingField.RegisterValueChangedCallback(loopingChangeCallback);
1127
+ definition.loopingField.userData = loopingChangeCallback;
1128
+
1129
+ EventCallback<ChangeEvent<float>> cycleOffsetChangeCallback = evt =>
1130
+ {
1131
+ definition.cycleOffset = evt.newValue;
1132
+ };
1133
+ definition.cycleOffsetField.RegisterValueChangedCallback(cycleOffsetChangeCallback);
1134
+ definition.cycleOffsetField.userData = cycleOffsetChangeCallback;
1135
+
1136
+ UpdateSpritesForDefinition(definition);
838
1137
  }
839
1138
 
840
1139
  private static bool IsCurveConstant(AnimationCurve curve)
@@ -880,18 +1179,35 @@
880
1179
 
881
1180
  private void AddAnimationDefinition()
882
1181
  {
883
- AnimationDefinition newDef = new();
1182
+ AnimationDefinition newDefinition = new();
884
1183
  if (_selectedSpriteSheet != null)
885
1184
  {
886
- newDef.Name = $"{_selectedSpriteSheet.name}_Anim_{_animationDefinitions.Count}";
1185
+ newDefinition.Name =
1186
+ $"{_selectedSpriteSheet.name}_Anim_{_animationDefinitions.Count}";
887
1187
  }
888
1188
  if (0 < _availableSprites.Count)
889
1189
  {
890
- newDef.EndSpriteIndex = _availableSprites.Count - 1;
1190
+ if (0 < _animationDefinitions.Count)
1191
+ {
1192
+ int nextStartIndex = _animationDefinitions[^1].EndSpriteIndex + 1;
1193
+ if (_availableSprites.Count - 1 < nextStartIndex)
1194
+ {
1195
+ nextStartIndex = 0;
1196
+ }
1197
+ newDefinition.StartSpriteIndex = nextStartIndex;
1198
+ }
1199
+
1200
+ newDefinition.EndSpriteIndex = _availableSprites.Count - 1;
891
1201
  }
892
- newDef.FrameRateCurve = AnimationCurve.Constant(0, 1, newDef.DefaultFrameRate);
893
- _animationDefinitions.Add(newDef);
894
- UpdateSpritesForDefinition(newDef);
1202
+ newDefinition.FrameRateCurve = AnimationCurve.Constant(
1203
+ 0,
1204
+ 1,
1205
+ newDefinition.DefaultFrameRate
1206
+ );
1207
+ _animationDefinitions.Add(newDefinition);
1208
+ _currentPreviewAnimDefIndex = _animationDefinitions.Count - 1;
1209
+ StartOrUpdateCurrentPreview(newDefinition);
1210
+ UpdateSpritesForDefinition(newDefinition);
895
1211
  _animationDefinitionsListView.Rebuild();
896
1212
  }
897
1213
 
@@ -1068,12 +1384,17 @@
1068
1384
  Directory.CreateDirectory(animationsFolder);
1069
1385
  }
1070
1386
 
1387
+ if (!animationsFolder.StartsWith("Assets", StringComparison.OrdinalIgnoreCase))
1388
+ {
1389
+ animationsFolder = DirectoryHelper.AbsoluteToUnityRelativePath(animationsFolder);
1390
+ }
1391
+
1071
1392
  int createdCount = 0;
1072
- foreach (AnimationDefinition def in _animationDefinitions)
1393
+ foreach (AnimationDefinition definition in _animationDefinitions)
1073
1394
  {
1074
- if (def.SpritesToAnimate.Count == 0)
1395
+ if (definition.SpritesToAnimate.Count == 0)
1075
1396
  {
1076
- Debug.LogWarning($"Skipping animation '{def.Name}' as it has no sprites.");
1397
+ this.LogWarn($"Skipping animation '{definition.Name}' as it has no sprites.");
1077
1398
  continue;
1078
1399
  }
1079
1400
 
@@ -1087,21 +1408,21 @@
1087
1408
  };
1088
1409
 
1089
1410
  ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[
1090
- def.SpritesToAnimate.Count
1411
+ definition.SpritesToAnimate.Count
1091
1412
  ];
1092
1413
  float currentTime = 0f;
1093
- AnimationCurve curve = def.FrameRateCurve;
1414
+ AnimationCurve curve = definition.FrameRateCurve;
1094
1415
  if (curve == null || curve.keys.Length == 0)
1095
1416
  {
1096
- Debug.LogWarning(
1097
- $"Animation '{def.Name}' has an invalid FrameRateCurve. Falling back to DefaultFrameRate."
1417
+ this.LogWarn(
1418
+ $"Animation '{definition.Name}' has an invalid FrameRateCurve. Falling back to DefaultFrameRate."
1098
1419
  );
1099
- curve = AnimationCurve.Constant(0, 1, def.DefaultFrameRate);
1420
+ curve = AnimationCurve.Constant(0, 1, definition.DefaultFrameRate);
1100
1421
  }
1101
1422
 
1102
1423
  if (curve.keys.Length == 0)
1103
1424
  {
1104
- curve.AddKey(0, def.DefaultFrameRate);
1425
+ curve.AddKey(0, definition.DefaultFrameRate);
1105
1426
  }
1106
1427
 
1107
1428
  float curveDuration = curve.keys.LastOrDefault().time;
@@ -1110,26 +1431,26 @@
1110
1431
  curveDuration = 1f;
1111
1432
  }
1112
1433
 
1113
- for (int i = 0; i < def.SpritesToAnimate.Count; ++i)
1434
+ for (int i = 0; i < definition.SpritesToAnimate.Count; ++i)
1114
1435
  {
1115
1436
  keyframes[i] = new ObjectReferenceKeyframe
1116
1437
  {
1117
1438
  time = currentTime,
1118
- value = def.SpritesToAnimate[i],
1439
+ value = definition.SpritesToAnimate[i],
1119
1440
  };
1120
1441
 
1121
- if (i < def.SpritesToAnimate.Count - 1)
1442
+ if (i < definition.SpritesToAnimate.Count - 1)
1122
1443
  {
1123
1444
  float normalizedTimeForCurve =
1124
- def.SpritesToAnimate.Count > 1
1125
- ? (float)i / (def.SpritesToAnimate.Count - 1)
1445
+ definition.SpritesToAnimate.Count > 1
1446
+ ? (float)i / (definition.SpritesToAnimate.Count - 1)
1126
1447
  : 0;
1127
1448
  float timeForCurveEval = normalizedTimeForCurve * curveDuration;
1128
1449
 
1129
1450
  float fps = curve.Evaluate(timeForCurveEval);
1130
1451
  if (fps <= 0)
1131
1452
  {
1132
- fps = def.DefaultFrameRate;
1453
+ fps = definition.DefaultFrameRate;
1133
1454
  }
1134
1455
 
1135
1456
  if (fps <= 0)
@@ -1144,15 +1465,16 @@
1144
1465
  AnimationUtility.SetObjectReferenceCurve(clip, spriteBinding, keyframes);
1145
1466
 
1146
1467
  AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(clip);
1147
- settings.loopTime = true;
1468
+ settings.loopTime = definition.loop;
1469
+ settings.cycleOffset = definition.cycleOffset;
1148
1470
  AnimationUtility.SetAnimationClipSettings(clip, settings);
1149
1471
 
1150
- string animName = string.IsNullOrEmpty(def.Name) ? "UnnamedAnim" : def.Name;
1472
+ string animName = string.IsNullOrEmpty(definition.Name)
1473
+ ? "UnnamedAnim"
1474
+ : definition.Name;
1151
1475
 
1152
- foreach (char character in Path.GetInvalidFileNameChars())
1153
- {
1154
- animName = animName.Replace(character, '_');
1155
- }
1476
+ animName = Path.GetInvalidFileNameChars()
1477
+ .Aggregate(animName, (current, character) => current.Replace(character, '_'));
1156
1478
  string assetPath = Path.Combine(animationsFolder, $"{animName}.anim");
1157
1479
  assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath);
1158
1480
 
@@ -1169,9 +1491,6 @@
1169
1491
  $"{createdCount} animation(s) created in:\n{animationsFolder}",
1170
1492
  "OK"
1171
1493
  );
1172
- EditorGUIUtility.PingObject(
1173
- AssetDatabase.LoadAssetAtPath<Object>(animationsFolder)
1174
- );
1175
1494
  }
1176
1495
  else
1177
1496
  {