From a4b6a5417542ee19b56c11d1a57c66634d378f70 Mon Sep 17 00:00:00 2001 From: Acid Bubbles Date: Tue, 13 Sep 2022 12:37:12 -0400 Subject: [PATCH] Custom targets grouping --- VamTimeline.AtomAnimation.cslist | 1 + VamTimeline.csproj | 1 + meta.json | 1 + .../Animatables/Base/AnimationTargetBase.cs | 2 + .../Animatables/Base/IAtomAnimationTarget.cs | 1 + .../Animations/AtomAnimation.Playback.cs | 2 +- .../Animations/AtomAnimationClip.cs | 10 +-- .../Operations/AddAnimationOperations.cs | 6 +- .../Serialization/AtomAnimationSerializer.cs | 16 +++++ src/AtomPlugin.cs | 1 + src/UI/Screens/GroupingScreen.cs | 63 +++++++++++++++++++ src/UI/Screens/MoreScreen.cs | 1 + src/UI/Screens/ScreensManager.cs | 4 ++ tests/VamTimeline.Tests.cslist | 1 + 14 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 src/UI/Screens/GroupingScreen.cs diff --git a/VamTimeline.AtomAnimation.cslist b/VamTimeline.AtomAnimation.cslist index 894a1bc1..28366c21 100644 --- a/VamTimeline.AtomAnimation.cslist +++ b/VamTimeline.AtomAnimation.cslist @@ -116,6 +116,7 @@ src\UI\Screens\BulkScreen.cs src\UI\Screens\ControllerTargetSettingsScreen.cs src\UI\Screens\DiagnosticsScreen.cs src\UI\Screens\EditAnimationScreen.cs +src\UI\Screens\GroupingScreen.cs src\UI\Screens\HelpScreen.cs src\UI\Screens\ImportAssignScreen.cs src\UI\Screens\ImportExportScreen.cs diff --git a/VamTimeline.csproj b/VamTimeline.csproj index bb8c7961..4440f45d 100644 --- a/VamTimeline.csproj +++ b/VamTimeline.csproj @@ -191,6 +191,7 @@ + diff --git a/meta.json b/meta.json index f28661e0..f625d5b8 100644 --- a/meta.json +++ b/meta.json @@ -128,6 +128,7 @@ "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\ControllerTargetSettingsScreen.cs", "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\DiagnosticsScreen.cs", "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\EditAnimationScreen.cs", + "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\GroupingScreen.cs", "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\HelpScreen.cs", "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\ImportAssignScreen.cs", "Custom\\Scripts\\AcidBubbles\\Timeline\\src\\UI\\Screens\\ImportExportScreen.cs", diff --git a/src/AtomAnimations/Animatables/Base/AnimationTargetBase.cs b/src/AtomAnimations/Animatables/Base/AnimationTargetBase.cs index 69aeafea..a4236b25 100644 --- a/src/AtomAnimations/Animatables/Base/AnimationTargetBase.cs +++ b/src/AtomAnimations/Animatables/Base/AnimationTargetBase.cs @@ -52,6 +52,8 @@ public bool collapsed } } + public string group { get; set; } + public string name => animatableRef.name; public string GetShortName() => animatableRef.GetShortName(); public string GetFullName() => animatableRef.GetFullName(); diff --git a/src/AtomAnimations/Animatables/Base/IAtomAnimationTarget.cs b/src/AtomAnimations/Animatables/Base/IAtomAnimationTarget.cs index 542d3236..00cc6854 100644 --- a/src/AtomAnimations/Animatables/Base/IAtomAnimationTarget.cs +++ b/src/AtomAnimations/Animatables/Base/IAtomAnimationTarget.cs @@ -11,6 +11,7 @@ public interface IAtomAnimationTarget : IDisposable string name { get; } bool selected { get; set; } bool collapsed { get; set; } + string group { get; set; } IAtomAnimationClip clip { get; set; } void Validate(float animationLength); diff --git a/src/AtomAnimations/Animations/AtomAnimation.Playback.cs b/src/AtomAnimations/Animations/AtomAnimation.Playback.cs index 88a7a0b4..8430ad19 100644 --- a/src/AtomAnimations/Animations/AtomAnimation.Playback.cs +++ b/src/AtomAnimations/Animations/AtomAnimation.Playback.cs @@ -617,6 +617,7 @@ public void SampleParentedControllers(AtomAnimationClip source) if (simulationFrozen) return; if (_globalScaledWeight <= 0) return; // TODO: Index keep track if there is any parenting + if (source == null) return; var layers = GetMainAndBestSiblingPerLayer(playingAnimationSegmentId, source.animationNameId, source.animationSetId); for (var layerIndex = 0; layerIndex < layers.Count; layerIndex++) { @@ -637,7 +638,6 @@ public void SampleParentedControllers(AtomAnimationClip source) if (controller.currentPositionState != FreeControllerV3.PositionState.Off) controller.control.position = Vector3.Lerp(controller.control.position, targetPosition, _globalWeight); } - var rotationParentRB = ctrl.GetRotationParentRB(); if (!ReferenceEquals(rotationParentRB, null)) { diff --git a/src/AtomAnimations/Animations/AtomAnimationClip.cs b/src/AtomAnimations/Animations/AtomAnimationClip.cs index 9e620575..03237358 100644 --- a/src/AtomAnimations/Animations/AtomAnimationClip.cs +++ b/src/AtomAnimations/Animations/AtomAnimationClip.cs @@ -615,17 +615,19 @@ public void DirtyAll() public IEnumerable GetTargetGroups() { yield return targetTriggers; - foreach (var group in targetControllers.GroupBy(t => t.animatableRef.groupKey)) + foreach (var group in targetControllers.GroupBy(t => t.group ?? t.animatableRef.groupKey)) { - yield return new AtomAnimationTargetsList(group) { label = group.First().animatableRef.groupLabel }; + var first = group.First(); + yield return new AtomAnimationTargetsList(group) { label = first.group ?? first.animatableRef.groupLabel }; } foreach (var target in targetFloatParams) { target.animatableRef.EnsureAvailable(); } - foreach (var group in targetFloatParams.GroupBy(t => t.animatableRef.groupKey)) + foreach (var group in targetFloatParams.GroupBy(t => t.group ?? t.animatableRef.groupKey)) { - yield return new AtomAnimationTargetsList(group) { label = group.First().animatableRef.groupLabel }; + var first = group.First(); + yield return new AtomAnimationTargetsList(group) { label = first.group ?? first.animatableRef.groupLabel }; } } diff --git a/src/AtomAnimations/Operations/AddAnimationOperations.cs b/src/AtomAnimations/Operations/AddAnimationOperations.cs index 4c436085..ca3cc71b 100644 --- a/src/AtomAnimations/Operations/AddAnimationOperations.cs +++ b/src/AtomAnimations/Operations/AddAnimationOperations.cs @@ -69,6 +69,7 @@ public CreatedAnimation AddAnimation(AtomAnimationClip source, string animationN { if (!origTarget.animatableRef.EnsureAvailable(false)) continue; var newTarget = clip.Add(new JSONStorableFloatAnimationTarget(origTarget)); + newTarget.group = origTarget.group; newTarget.value.keys = new List(origTarget.value.keys); newTarget.dirty = true; } @@ -76,12 +77,12 @@ public CreatedAnimation AddAnimation(AtomAnimationClip source, string animationN foreach (var origTarget in source.targetTriggers) { var newTarget = clip.Add(new TriggersTrackAnimationTarget(origTarget.animatableRef, _animation.logger)); + newTarget.group = origTarget.group; foreach (var origTrigger in origTarget.triggersMap) { var trigger = newTarget.CreateKeyframe(origTrigger.Key); trigger.RestoreFromJSON(origTrigger.Value.GetJSON()); } - newTarget.dirty = true; } @@ -101,6 +102,7 @@ public CreatedAnimation AddAnimation(AtomAnimationClip source, string animationN { if (!origTarget.animatableRef.EnsureAvailable(false)) continue; var newTarget = clip.Add(origTarget.animatableRef); + newTarget.group = origTarget.group; newTarget.SetKeyframeToCurrent(0f); newTarget.SetKeyframeToCurrent(clip.animationLength); } @@ -108,6 +110,7 @@ public CreatedAnimation AddAnimation(AtomAnimationClip source, string animationN foreach (var origTarget in source.targetTriggers) { var newTarget = new TriggersTrackAnimationTarget(origTarget.animatableRef, _animation.logger); + newTarget.group = origTarget.group; newTarget.AddEdgeFramesIfMissing(clip.animationLength); clip.Add(newTarget); } @@ -183,6 +186,7 @@ private static FreeControllerV3AnimationTarget CopyTarget(AtomAnimationClip clip newTarget.weight = origTarget.weight; newTarget.controlPosition = origTarget.controlPosition; newTarget.controlRotation = origTarget.controlRotation; + newTarget.group = origTarget.group; return newTarget; } diff --git a/src/AtomAnimations/Serialization/AtomAnimationSerializer.cs b/src/AtomAnimations/Serialization/AtomAnimationSerializer.cs index ce332a91..f9ae332b 100644 --- a/src/AtomAnimations/Serialization/AtomAnimationSerializer.cs +++ b/src/AtomAnimations/Serialization/AtomAnimationSerializer.cs @@ -178,6 +178,7 @@ private void DeserializeClip(AtomAnimationClip clip, JSONClass clipJSON, Animata target.controlPosition = DeserializeBool(controllerJSON["ControlPosition"], true); target.controlRotation = DeserializeBool(controllerJSON["ControlRotation"], true); target.weight = DeserializeFloat(controllerJSON["Weight"], 1f); + target.group = DeserializeString(controllerJSON["Group"], null); if (controllerJSON.HasKey("Parent")) { var parentJSON = controllerJSON["Parent"].AsObject; @@ -232,6 +233,7 @@ private void DeserializeClip(AtomAnimationClip clip, JSONClass clipJSON, Animata SuperController.LogError($"Timeline: Float param {atom.name} / {storableId} / {floatParamName} was added more than once in clip {clip.animationNameQualified}. Dropping second instance."); continue; } + target.group = DeserializeString(paramJSON["Group"], null); var dirty = false; DeserializeCurve(target.value, paramJSON["Value"], ref dirty); target.AddEdgeFramesIfMissing(clip.animationLength); @@ -263,6 +265,7 @@ private void DeserializeClip(AtomAnimationClip clip, JSONClass clipJSON, Animata SuperController.LogError($"The triggers track {triggerTrackName} exists more than once in clip {clip.animationNameQualified}. Trigger keyframes may be overwritten."); } } + target.group = DeserializeString(triggerJSON["Group"], null); foreach (JSONClass entryJSON in triggerJSON["Triggers"].AsArray) { var trigger = target.CreateCustomTrigger(); @@ -547,6 +550,10 @@ private static void SerializeClipTargets(AtomAnimationClip clip, JSONClass clipJ { controllerJSON["Weight"] = target.weight.ToString(CultureInfo.InvariantCulture); } + if (target.group != null) + { + controllerJSON["Group"] = target.group; + } controllersJSON.Add(controllerJSON); } if(controllersJSON.Count > 0) @@ -570,10 +577,15 @@ private static void SerializeClipTargets(AtomAnimationClip clip, JSONClass clipJ } paramJSON["Atom"] = target.animatableRef.storable.containingAtom.uid; } + if (target.group != null) + { + paramJSON["Group"] = target.group; + } var min = target.animatableRef.floatParam?.min ?? target.animatableRef.assignMinValueOnBound; if (min != null) paramJSON["Min"] = min.Value.ToString(CultureInfo.InvariantCulture); var max = target.animatableRef.floatParam?.max ?? target.animatableRef.assignMaxValueOnBound; if (max != null) paramJSON["Max"] = max.Value.ToString(CultureInfo.InvariantCulture); + floatParamsJSON.Add(paramJSON); } if(floatParamsJSON.Count > 0) @@ -587,6 +599,10 @@ private static void SerializeClipTargets(AtomAnimationClip clip, JSONClass clipJ {"Name", target.name}, {"Live", target.animatableRef.live ? "1" : "0"}, }; + if (target.group != null) + { + triggerJSON["Group"] = target.group; + } var entriesJSON = new JSONArray(); foreach (var x in target.triggersMap.OrderBy(kvp => kvp.Key)) { diff --git a/src/AtomPlugin.cs b/src/AtomPlugin.cs index ea2cdcc2..bd588871 100644 --- a/src/AtomPlugin.cs +++ b/src/AtomPlugin.cs @@ -1103,6 +1103,7 @@ public void OnBindingsListRequested(List bindings) bindings.Add(new JSONStorableAction("OpenUI_MoreTab_Diagnostics", () => { ChangeScreen(DiagnosticsScreen.ScreenName, null); SelectAndOpenUI(); })); bindings.Add(new JSONStorableAction("OpenUI_MoreTab_Options", () => { ChangeScreen(OptionsScreen.ScreenName, null); SelectAndOpenUI(); })); bindings.Add(new JSONStorableAction("OpenUI_MoreTab_Logging", () => { ChangeScreen(LoggingScreen.ScreenName, null); SelectAndOpenUI(); })); + bindings.Add(new JSONStorableAction("OpenUI_MoreTab_Grouping", () => { ChangeScreen(GroupingScreen.ScreenName, null); SelectAndOpenUI(); })); bindings.Add(new JSONStorableAction("Toggle_DopeSheetMode", () => _ui.ToggleDopeSheetMode())); bindings.Add(new JSONStorableAction("Toggle_ExpandCollapseRightPanel", () => _ui.ToggleExpandCollapse())); bindings.Add(new JSONStorableAction("Toggle_SelectAllTargets", () => animationEditContext.SelectAll(!animationEditContext.current.GetAllTargets().Any(t => t.selected)))); diff --git a/src/UI/Screens/GroupingScreen.cs b/src/UI/Screens/GroupingScreen.cs new file mode 100644 index 00000000..e4a1e67e --- /dev/null +++ b/src/UI/Screens/GroupingScreen.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Text.RegularExpressions; + +namespace VamTimeline +{ + public class GroupingScreen : ScreenBase + { + public const string ScreenName = "Grouping"; + + private JSONStorableString _assignGroupJSON; + + public override string screenId => ScreenName; + + public override void Init(IAtomPlugin plugin, object arg) + { + base.Init(plugin, arg); + + CreateChangeScreenButton("< Back", MoreScreen.ScreenName); + + InitAssignToGroupUI(); + + prefabFactory.CreateButton("Assign to selected targets").button.onClick.AddListener(Assign); + + animation.animatables.onTargetsSelectionChanged.AddListener(OnTargetsSelectionChanged); + OnTargetsSelectionChanged(); + } + + private void InitAssignToGroupUI() + { + _assignGroupJSON = new JSONStorableString("Group name (empty for auto)", ""); + prefabFactory.CreateTextInput(_assignGroupJSON); + } + + private void OnTargetsSelectionChanged() + { + var groups = animationEditContext.GetSelectedTargets().Select(t => t.group).Distinct().ToList(); + if (groups.Count == 1) _assignGroupJSON.val = groups[0]; + } + + private void Assign() + { + var group = _assignGroupJSON.val; + if (string.IsNullOrEmpty(group)) group = null; + foreach (var target in animationEditContext.GetSelectedTargets()) + { + foreach (var c in animationEditContext.currentLayer) + { + var t = c.GetAllTargets().FirstOrDefault(x => x.TargetsSameAs(target)); + if (t == null) continue; + t.group = group; + } + } + current.onTargetsListChanged.Invoke(); + } + + public override void OnDestroy() + { + animation.animatables.onTargetsSelectionChanged.RemoveListener(OnTargetsSelectionChanged); + base.OnDestroy(); + } + } +} + diff --git a/src/UI/Screens/MoreScreen.cs b/src/UI/Screens/MoreScreen.cs index 2f2a19b5..1108a711 100644 --- a/src/UI/Screens/MoreScreen.cs +++ b/src/UI/Screens/MoreScreen.cs @@ -26,6 +26,7 @@ public override void Init(IAtomPlugin plugin, object arg) CreateChangeScreenButton("Bulk changes...", BulkScreen.ScreenName); CreateChangeScreenButton("Advanced keyframe tools...", AdvancedKeyframeToolsScreen.ScreenName); + CreateChangeScreenButton("Grouping...", GroupingScreen.ScreenName); prefabFactory.CreateSpacer(); diff --git a/src/UI/Screens/ScreensManager.cs b/src/UI/Screens/ScreensManager.cs index 3924d6ae..16193d47 100644 --- a/src/UI/Screens/ScreensManager.cs +++ b/src/UI/Screens/ScreensManager.cs @@ -213,6 +213,10 @@ private IEnumerator RefreshCurrentUIDeferred(string screen) case LoggingScreen.ScreenName: _current = screenContainer.AddComponent(); break; + case GroupingScreen.ScreenName: + _current = screenContainer.AddComponent(); + break; + default: throw new InvalidOperationException($"Unknown screen {screen}"); } diff --git a/tests/VamTimeline.Tests.cslist b/tests/VamTimeline.Tests.cslist index 182aec16..314a9cc0 100644 --- a/tests/VamTimeline.Tests.cslist +++ b/tests/VamTimeline.Tests.cslist @@ -116,6 +116,7 @@ ..\src\UI\Screens\ControllerTargetSettingsScreen.cs ..\src\UI\Screens\DiagnosticsScreen.cs ..\src\UI\Screens\EditAnimationScreen.cs +..\src\UI\Screens\GroupingScreen.cs ..\src\UI\Screens\HelpScreen.cs ..\src\UI\Screens\ImportAssignScreen.cs ..\src\UI\Screens\ImportExportScreen.cs