├── omnisharp.json ├── .gitignore ├── screenshots ├── timeline-bulk.png ├── timeline-edit.png ├── timeline-more.png ├── timeline-advanced.png ├── timeline-options.png ├── timeline-targets.png ├── timeline-controller.png ├── timeline-performance.png ├── timeline-sequencing.png ├── timeline-add-animation.png ├── timeline-edit-animation.png ├── timeline-controller-settings.png └── timeline-manage-animations.png ├── tests ├── VamTimeline.Tests.cslist ├── Framework │ ├── ITestClass.cs │ ├── TestsEnumerator.cs │ ├── Test.cs │ └── TestContext.cs ├── Plugin │ ├── TestsIndex.cs │ └── TestPlugin.cs ├── Specs │ ├── AnimationTests.cs │ └── TargetsHelper.cs └── Unit │ └── AtomAnimations │ └── Targets │ └── FreeControllerAnimationTargetTests.cs ├── VamTimeline.AtomAnimation.cslist ├── .gitmodules ├── .vscode ├── settings.json └── vam.code-snippets ├── src ├── Interop │ ├── ITimelineListener.cs │ ├── IRemoteControllerPlugin.cs │ ├── IRemoteAtomPlugin.cs │ ├── StorableNames.cs │ ├── RectTransformExtensions.cs │ └── SyncProxy.cs ├── UI │ ├── Components │ │ ├── AnimatableFrames │ │ │ ├── IAnimationTargetFrameComponent.cs │ │ │ └── FreeControllerV2AnimationTargetFrameComponent.cs │ │ ├── Zoom │ │ │ ├── ZoomStateModes.cs │ │ │ ├── ZoomStyle.cs │ │ │ ├── ZoomTime.cs │ │ │ ├── ZoomControlGraphics.cs │ │ │ └── Zoom.cs │ │ ├── Styling │ │ │ └── StyleBase.cs │ │ ├── UIPerformance.cs │ │ ├── Scrubber │ │ │ ├── ScrubberStyle.cs │ │ │ └── ScrubberMarkers.cs │ │ ├── Clickable.cs │ │ ├── Listener.cs │ │ ├── Curves │ │ │ └── CurvesStyle.cs │ │ ├── GradientImage.cs │ │ ├── SimpleSlider.cs │ │ ├── ScreenTabs.cs │ │ ├── UIVertexHelper.cs │ │ ├── DopeSheet │ │ │ └── DopeSheetStyle.cs │ │ ├── LineDrawer3D.cs │ │ ├── MiniButton.cs │ │ └── CurveTypePopup.cs │ └── Screens │ │ ├── LockedScreen.cs │ │ ├── GlobalTriggersScreen.cs │ │ ├── AddAnimationsScreen.cs │ │ ├── GroupingScreen.cs │ │ ├── MoreScreen.cs │ │ ├── DefaultsScreen.cs │ │ ├── ScreenBase.cs │ │ ├── AddScreenBase.cs │ │ ├── AddSharedSegmentScreen.cs │ │ ├── LoggingScreen.cs │ │ └── HelpScreen.cs ├── AtomAnimations │ ├── Animations │ │ ├── TimeModes.cs │ │ ├── IAtomAnimationTargetsList.cs │ │ ├── AtomAnimationTargetsList.cs │ │ ├── IAtomAnimationClip.cs │ │ ├── SimpleTrigger.cs │ │ ├── AtomAnimation.Queuing.cs │ │ ├── UnitySpecific.cs │ │ ├── VamOverlaysFadeManager.cs │ │ └── AtomAnimation.Building.cs │ ├── Operations │ │ ├── Reduction │ │ │ ├── ReducerBucket.cs │ │ │ ├── ReduceProgress.cs │ │ │ ├── ReduceSettings.cs │ │ │ ├── ITargetReduceProcessor.cs │ │ │ ├── TargetReduceProcessorBase.cs │ │ │ ├── FloatParamTargetReduceProcessor.cs │ │ │ └── ControllerTargetReduceProcessor.cs │ │ ├── TargetsOperations.cs │ │ ├── KeyframesOperations.cs │ │ ├── SegmentsOperations.cs │ │ ├── LayersOperations.cs │ │ ├── OperationsFactory.cs │ │ └── SilentImportOperations.cs │ ├── BezierCurves │ │ ├── Smoothing │ │ │ ├── IBezierAnimationCurveSmoothing.cs │ │ │ ├── BezierAnimationCurveSmoothingBase.cs │ │ │ └── BezierAnimationCurveSmoothingNonLooping.cs │ │ ├── BezierKeyframe.cs │ │ └── CurveTypeValues.cs │ ├── Utils │ │ ├── TimelinePrefabs.cs │ │ ├── StringMap.cs │ │ ├── FloatExtensions.cs │ │ └── QuaternionExtensions.cs │ ├── Animatables │ │ ├── Base │ │ │ ├── AnimatableRefBase.cs │ │ │ ├── IAtomAnimationTarget.cs │ │ │ └── AnimationTargetBase.cs │ │ ├── CurvesBase │ │ │ ├── ICurveAnimationTarget.cs │ │ │ └── CurveAnimationTargetBase.cs │ │ ├── Triggers │ │ │ └── TriggersTrackRef.cs │ │ ├── FreeControllerV3s │ │ │ └── FreeControllerV3Ref.cs │ │ └── AnimatablesRegistry.cs │ ├── Editing │ │ ├── AtomAnimationBackup.cs │ │ └── TimelineDefaults.cs │ ├── Logging │ │ └── Logger.cs │ └── Clipboard │ │ └── AtomClipboardEntry.cs └── IAtomPlugin.cs ├── VamTimeline.Controller.cslist ├── .github ├── FUNDING.yml └── workflows │ └── package.yml ├── README.md ├── .editorconfig └── Update-FilesLists.ps1 /omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .idea/ 4 | *.user 5 | *.fav 6 | -------------------------------------------------------------------------------- /screenshots/timeline-bulk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-bulk.png -------------------------------------------------------------------------------- /screenshots/timeline-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-edit.png -------------------------------------------------------------------------------- /screenshots/timeline-more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-more.png -------------------------------------------------------------------------------- /tests/VamTimeline.Tests.cslist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/tests/VamTimeline.Tests.cslist -------------------------------------------------------------------------------- /VamTimeline.AtomAnimation.cslist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/VamTimeline.AtomAnimation.cslist -------------------------------------------------------------------------------- /screenshots/timeline-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-advanced.png -------------------------------------------------------------------------------- /screenshots/timeline-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-options.png -------------------------------------------------------------------------------- /screenshots/timeline-targets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-targets.png -------------------------------------------------------------------------------- /screenshots/timeline-controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-controller.png -------------------------------------------------------------------------------- /screenshots/timeline-performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-performance.png -------------------------------------------------------------------------------- /screenshots/timeline-sequencing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-sequencing.png -------------------------------------------------------------------------------- /screenshots/timeline-add-animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-add-animation.png -------------------------------------------------------------------------------- /screenshots/timeline-edit-animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-edit-animation.png -------------------------------------------------------------------------------- /screenshots/timeline-controller-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-controller-settings.png -------------------------------------------------------------------------------- /screenshots/timeline-manage-animations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidbubbles/vam-timeline/HEAD/screenshots/timeline-manage-animations.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/External/curve-editor"] 2 | path = src/External/curve-editor 3 | url = https://github.com/acidbubbles/vam-curve-editor.git 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "bin": true, 4 | "obj": true 5 | }, 6 | "omnisharp.enableRoslynAnalyzers": true, 7 | "omnisharp.enableEditorConfigSupport": true 8 | } -------------------------------------------------------------------------------- /tests/Framework/ITestClass.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VamTimeline 4 | { 5 | public interface ITestClass 6 | { 7 | IEnumerable GetTests(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Interop/ITimelineListener.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public interface ITimelineListener 4 | { 5 | void OnTimelineAnimationReady(MVRScript storable); 6 | void OnTimelineAnimationDisabled(MVRScript storable); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/UI/Components/AnimatableFrames/IAnimationTargetFrameComponent.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public interface IAnimationTargetFrameComponent 6 | { 7 | GameObject gameObject { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/TimeModes.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public static class TimeModes 4 | { 5 | public const int UnityTime = 0; 6 | public const int RealTimeLegacy = 1; 7 | public const int RealTime = 2; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/UI/Components/Zoom/ZoomStateModes.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public static class ZoomStateModes 4 | { 5 | public const int ResizeBeginMode = 1; 6 | public const int ResizeEndMode = 2; 7 | public const int MoveMode = 3; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/ReducerBucket.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public struct ReducerBucket 4 | { 5 | public int from; 6 | public int to; 7 | public int keyWithLargestDelta; 8 | public float largestDelta; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/AtomAnimations/BezierCurves/Smoothing/IBezierAnimationCurveSmoothing.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VamTimeline 4 | { 5 | public interface IBezierAnimationCurveSmoothing 6 | { 7 | bool looping { get; } 8 | void AutoComputeControlPoints(List keys); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/IAtomAnimationTargetsList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VamTimeline 4 | { 5 | public interface IAtomAnimationTargetsList 6 | { 7 | int Count { get; } 8 | string label { get; } 9 | 10 | IEnumerable GetTargets(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /VamTimeline.Controller.cslist: -------------------------------------------------------------------------------- 1 | src\Interop\RectTransformExtensions.cs 2 | src\Interop\IRemoteAtomPlugin.cs 3 | src\Interop\IRemoteControllerPlugin.cs 4 | src\Interop\ITimelineListener.cs 5 | src\Interop\StorableNames.cs 6 | src\Interop\SyncProxy.cs 7 | src\UI\Components\GradientImage.cs 8 | src\Controller\SimpleSignUI.cs 9 | src\ControllerPlugin.cs 10 | -------------------------------------------------------------------------------- /src/Interop/IRemoteControllerPlugin.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public interface IRemoteControllerPlugin : ITimelineListener 4 | { 5 | // TODO: Would it be better to use events instead? 6 | void OnTimelineAnimationParametersChanged(JSONStorable storable); 7 | void OnTimelineTimeChanged(JSONStorable storable); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/ReduceProgress.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public struct ReduceProgress 4 | { 5 | public float startTime; 6 | public float nowTime; 7 | public float stepsDone; 8 | public float stepsTotal; 9 | public float timeLeft => ((nowTime - startTime) / stepsDone) * (stepsTotal - stepsDone); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Interop/IRemoteAtomPlugin.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace VamTimeline 6 | { 7 | public interface IRemoteAtomPlugin : ITimelineListener 8 | { 9 | void VamTimelineConnectController(Dictionary dict); 10 | void VamTimelineRequestControlPanel(GameObject container); 11 | void OnTimelineEvent(object[] e); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/ReduceSettings.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class ReduceSettings 4 | { 5 | public bool removeFlats; 6 | public bool simplify; 7 | public float minMeaningfulDistance; 8 | public float minMeaningfulRotation; 9 | public float minMeaningfulFloatParamRangeRatio; 10 | public bool round; 11 | public int fps; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/UI/Components/Styling/StyleBase.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class StyleBase 6 | { 7 | // Global 8 | public Font Font { get; } 9 | public virtual Color FontColor { get; } = new Color(0, 0, 0); 10 | public virtual Color BackgroundColor { get; } = new Color(0.721f, 0.682f, 0.741f); 11 | 12 | public StyleBase() 13 | { 14 | Font = (Font)Resources.GetBuiltinResource(typeof(Font), "Arial.ttf"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Plugin/TestsIndex.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public static class TestsIndex 4 | { 5 | public static TestsEnumerator GetAllTests() 6 | { 7 | return new TestsEnumerator(new ITestClass[]{ 8 | new BezierAnimationCurveTests(), 9 | new FreeControllerAnimationTargetTests(), 10 | new AnimationTests(), 11 | new ResizeAnimationOperationTests(), 12 | new ImportOperationTests() 13 | }); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/UI/Components/UIPerformance.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public static class UIPerformance 6 | { 7 | public const int HighFrequency = 2; 8 | public const int LowFrequency = 6; 9 | 10 | public static bool ShouldSkip(int everyNFrames) 11 | { 12 | return Time.frameCount % everyNFrames != 0; 13 | } 14 | 15 | public static bool ShouldRun(int everyNFrames) 16 | { 17 | return Time.frameCount % everyNFrames == 0; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/UI/Components/Scrubber/ScrubberStyle.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class ScrubberStyle : StyleBase 6 | { 7 | // Scrubber 8 | public float Padding { get; } = 16f; 9 | public Color ScrubberColor { get; } = new Color(0.88f, 0.84f, 0.86f); 10 | public float ScrubberSize { get; } = 2f; 11 | public Color SecondsColor { get; } = new Color(0.50f, 0.48f, 0.48f); 12 | public float SecondsSize { get; } = 4f; 13 | public Color SecondFractionsColor { get; } = new Color(0.65f, 0.63f, 0.63f); 14 | public float SecondFractionsSize { get; } = 2.5f; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/ITargetReduceProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public interface ITargetReduceProcessor 4 | { 5 | ICurveAnimationTarget target { get; } 6 | void Branch(); 7 | void Commit(); 8 | ReducerBucket CreateBucket(int from, int to); 9 | void CopyToBranch(int sourceKey, int curveType = CurveTypeValues.Undefined, float time = -1); 10 | void AverageToBranch(float keyTime, int fromKey, int toKey); 11 | void FlattenToBranch(int sectionStart, int key); 12 | bool IsStable(int key1, int key2); 13 | float GetComparableNormalizedValue(int key); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: acidbubbles 4 | patreon: acidbubbles 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/AtomAnimations/Utils/TimelinePrefabs.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public static class TimelinePrefabs 6 | { 7 | public static readonly Transform cube = InitCube(); 8 | 9 | private static Transform InitCube() 10 | { 11 | var go = GameObject.CreatePrimitive(PrimitiveType.Cube); 12 | go.SetActive(false); 13 | var cs = go.GetComponent(); 14 | Object.DestroyImmediate(cs); 15 | return go.transform; 16 | } 17 | 18 | public static void Destroy() 19 | { 20 | Object.Destroy(cube.gameObject); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/AtomAnimationTargetsList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace VamTimeline 5 | { 6 | public class AtomAnimationTargetsList : List, IAtomAnimationTargetsList 7 | where T : IAtomAnimationTarget 8 | { 9 | public string label { get; set; } 10 | 11 | public AtomAnimationTargetsList() 12 | { 13 | } 14 | 15 | public AtomAnimationTargetsList(IEnumerable values) 16 | : base(values) 17 | { 18 | } 19 | 20 | public IEnumerable GetTargets() 21 | { 22 | return this.Cast(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/AtomAnimations/Utils/StringMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VamTimeline 4 | { 5 | public static class StringMap 6 | { 7 | private static int _nextId = 1; 8 | private static readonly Dictionary _ids = new Dictionary(); 9 | 10 | public static int ToId(this string name) 11 | { 12 | if (name == null) 13 | { 14 | return -1; 15 | } 16 | 17 | int id; 18 | if (_ids.TryGetValue(name, out id)) 19 | return id; 20 | 21 | id = _nextId; 22 | _nextId++; 23 | _ids.Add(name, id); 24 | return id; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/Base/AnimatableRefBase.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine.Events; 2 | 3 | namespace VamTimeline 4 | { 5 | public interface IAnimatableRefWithTransform 6 | { 7 | bool selectedPosition { get; set; } 8 | bool selectedRotation { get; set; } 9 | } 10 | 11 | public abstract class AnimatableRefBase 12 | { 13 | public bool selected { get; set; } 14 | 15 | public readonly UnityEvent onSelectedChanged = new UnityEvent(); 16 | 17 | public bool collapsed { get; set; } 18 | 19 | public abstract string name { get; } 20 | public abstract object groupKey { get; } 21 | public abstract string groupLabel { get; } 22 | public abstract string GetShortName(); 23 | public abstract string GetFullName(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/CurvesBase/ICurveAnimationTarget.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VamTimeline 4 | { 5 | public interface ICurveAnimationTarget : IAtomAnimationTarget 6 | { 7 | bool recording { get; set; } 8 | BezierAnimationCurve GetLeadCurve(); 9 | IEnumerable GetCurves(); 10 | int SetKeyframeToCurrent(float time, bool makeDirty = true); 11 | int AddKeyframeAtTime(float time, bool makeDirty = true); 12 | void ChangeCurveByTime(float time, int curveType, bool dirty = true); 13 | int GetKeyframeCurveTypeByTime(float time); 14 | ICurveAnimationTarget Clone(bool copyKeyframes); 15 | void RestoreFrom(ICurveAnimationTarget backup); 16 | void IncreaseCapacity(int capacity); 17 | void TrimCapacity(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Framework/TestsEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace VamTimeline 5 | { 6 | public class TestsEnumerator : IEnumerable 7 | { 8 | private readonly ITestClass[] _testClasses; 9 | 10 | public TestsEnumerator(ITestClass[] testClasses) 11 | { 12 | _testClasses = testClasses; 13 | } 14 | 15 | public IEnumerator GetEnumerator() 16 | { 17 | return ((IEnumerable)this).GetEnumerator(); 18 | } 19 | 20 | IEnumerator IEnumerable.GetEnumerator() 21 | { 22 | foreach (var testClass in _testClasses) 23 | { 24 | foreach (var test in testClass.GetTests()) 25 | { 26 | yield return test; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/IAtomAnimationClip.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine.Events; 4 | 5 | namespace VamTimeline 6 | { 7 | public interface IAtomAnimationClip : IDisposable 8 | { 9 | string animationNameQualified { get; } 10 | bool loop { get; } 11 | bool playbackEnabled { get; } 12 | float playbackBlendWeight { get; } 13 | float playbackBlendWeightSmoothed { get; } 14 | bool temporarilyEnabled { get; } 15 | float clipTime { get; } 16 | float scaledWeight { get; } 17 | UnityEvent onTargetsListChanged { get; } 18 | float blendInDuration { get; } 19 | float animationLength { get; } 20 | 21 | bool loopPreserveLastFrame { get; } 22 | float loopBlendSelfDuration { get; } 23 | 24 | IEnumerable GetTargetGroups(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/UI/Components/Zoom/ZoomStyle.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class ZoomStyle : StyleBase 6 | { 7 | // Zoom 8 | public override Color BackgroundColor { get; } = new Color(0.521f, 0.482f, 0.541f); 9 | public override Color FontColor { get; } = new Color(0.821f, 0.782f, 0.841f); 10 | public float Padding { get; } = 16f; 11 | public float VerticalPadding => 4f; 12 | public float ScrubberWidth = 2f; 13 | public float DragSideWidth = 4f; 14 | public Color FullSectionColor { get; } = new Color(0.321f, 0.282f, 0.341f); 15 | public Color ZoomedSectionColor { get; } = new Color(0.1f, 0.1f, 0.1f); 16 | public Color ZoomedSectionHighlightColor { get; } = new Color(0.6f, 0.5f, 0.6f); 17 | public Color ScrubberColor { get; } = new Color(0.721f, 0.682f, 0.741f); 18 | public float ClickablePadding { get; } = 32f; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/UI/Components/Clickable.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Events; 3 | using UnityEngine.EventSystems; 4 | 5 | namespace VamTimeline 6 | { 7 | public class Clickable : MonoBehaviour, IPointerClickHandler 8 | { 9 | public readonly ClickableEvent onClick = new ClickableEvent(); 10 | public readonly ClickableEvent onRightClick = new ClickableEvent(); 11 | 12 | public void OnPointerClick(PointerEventData eventData) 13 | { 14 | if (eventData.button == PointerEventData.InputButton.Right) 15 | onRightClick.Invoke(eventData); 16 | else 17 | onClick.Invoke(eventData); 18 | } 19 | 20 | public void OnDestroy() 21 | { 22 | onClick.RemoveAllListeners(); 23 | onRightClick.RemoveAllListeners(); 24 | } 25 | 26 | public class ClickableEvent : UnityEvent 27 | { 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/Triggers/TriggersTrackRef.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class TriggersTrackRef : AnimatableRefBase 4 | { 5 | public override string name => _name; 6 | public override object groupKey => null; 7 | public override string groupLabel => "Triggers"; 8 | 9 | public bool live; 10 | public int animationLayerQualifiedId; 11 | private string _name; 12 | 13 | public TriggersTrackRef(int layerQualifiedId, string triggerTrackName) 14 | { 15 | animationLayerQualifiedId = layerQualifiedId; 16 | _name = triggerTrackName; 17 | } 18 | 19 | public override string GetShortName() => _name; 20 | public override string GetFullName() => _name; 21 | 22 | public void SetName(string value) => _name = value; 23 | 24 | public bool Targets(int layerQualifiedId, string triggerTrackName) 25 | { 26 | return animationLayerQualifiedId == layerQualifiedId && _name == triggerTrackName; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/UI/Components/Zoom/ZoomTime.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class ZoomTime : MaskableGraphic 7 | { 8 | public ZoomStyle style; 9 | public float animationLength { get; set; } 10 | public float time { get; set; } 11 | 12 | protected override void OnPopulateMesh(VertexHelper vh) 13 | { 14 | vh.Clear(); 15 | 16 | if(animationLength == 0) return; 17 | 18 | var rect = rectTransform.rect; 19 | var width = rect.width; 20 | 21 | var x1 = (time / animationLength) * width; 22 | var timeWidth = style.ScrubberWidth / 2f; 23 | 24 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(style.ScrubberColor, 25 | new Vector2(rect.xMin + x1 - timeWidth, rect.yMin), 26 | new Vector2(rect.xMin + x1 + timeWidth, rect.yMin), 27 | new Vector2(rect.xMin + x1 + timeWidth, rect.yMax), 28 | new Vector2(rect.xMin + x1 - timeWidth, rect.yMax) 29 | )); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virt-A-Mate Timeline 2 | 3 | An animation timeline with keyframe and controllable curves 4 | 5 | > Check out [Timeline on Virt-A-Mate Hub](https://hub.virtamate.com/resources/timeline.94/) 6 | 7 | ## Installing 8 | 9 | Download `AcidBubbles.Timeline.(version).var` from [Releases](https://github.com/acidbubbles/vam-timeline/releases) and put it into the `(VaM)/AddonPackages` folder. For versions before VaM 1.19, you can extract the `.var` file into your VaM install folder (so the files end up in `(VaM Install Folder)/Custom/Scripts/Acidbubbles/Timeline`). 10 | 11 | ## Documentation 12 | 13 | Head to the [wiki](https://github.com/acidbubbles/vam-timeline/wiki) for instructions. 14 | 15 | ## Contributing 16 | 17 | Contribution are welcome. This is a complex plugin with, sadly, no unit or automated tests, so reach out before developing anything complex, and validate often that you're on the right track. 18 | 19 | The paths to the VaM dll files in the `.csproj` file are relative, so clone into `(VaM Install Folder)/Custom/Scripts/Dev/vam-timeline` for example. 20 | 21 | ## License 22 | 23 | [GNU GPLv3](LICENSE.md) 24 | -------------------------------------------------------------------------------- /src/UI/Components/Listener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.Events; 4 | 5 | namespace VamTimeline 6 | { 7 | public class Listener : MonoBehaviour 8 | { 9 | private bool _attached; 10 | private UnityAction _fn; 11 | private UnityEvent _handler; 12 | 13 | public void OnEnable() 14 | { 15 | if (!_attached && _handler != null) 16 | { 17 | _handler.AddListener(_fn); 18 | _attached = true; 19 | } 20 | } 21 | 22 | public void OnDisable() 23 | { 24 | if (_attached && _handler != null) 25 | { 26 | _handler.RemoveListener(_fn); 27 | _attached = false; 28 | } 29 | } 30 | 31 | public void Bind(UnityEvent handler, UnityAction fn) 32 | { 33 | if (_handler != null) throw new InvalidOperationException("Listener already bound"); 34 | _fn = fn; 35 | _handler = handler; 36 | if(isActiveAndEnabled) OnEnable(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/IAtomPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using UnityEngine; 3 | 4 | namespace VamTimeline 5 | { 6 | public interface IMonoBehavior 7 | { 8 | bool isActiveAndEnabled { get; } 9 | Coroutine StartCoroutine(IEnumerator routine); 10 | void StopCoroutine(Coroutine routine); 11 | } 12 | 13 | public interface IMVRScript : IMonoBehavior 14 | { 15 | Atom containingAtom { get; } 16 | MVRPluginManager manager { get; } 17 | } 18 | 19 | public interface IAtomPlugin : IMVRScript, IRemoteAtomPlugin 20 | { 21 | AtomAnimation animation { get; } 22 | AtomAnimationEditContext animationEditContext { get; } 23 | Logger logger { get; } 24 | AtomAnimationSerializer serializer { get; } 25 | PeerManager peers { get; } 26 | OperationsFactory operations { get; } 27 | 28 | JSONStorableAction deleteJSON { get; } 29 | JSONStorableAction cutJSON { get; } 30 | JSONStorableAction copyJSON { get; } 31 | JSONStorableAction pasteJSON { get; } 32 | 33 | void ChangeScreen(string screenName, object screenArg); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: VamTimelinePackage 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Get the version 16 | id: get_version 17 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 18 | - name: Zip the release package 19 | id: zip 20 | run: | 21 | mkdir -p publish/Custom/Scripts/AcidBubbles/Timeline 22 | cp -r src publish/Custom/Scripts/AcidBubbles/Timeline/ 23 | cp *.cslist publish/Custom/Scripts/AcidBubbles/Timeline/ 24 | cp meta.json publish/ 25 | sed -i 's/v0.0.0/${{ steps.get_version.outputs.VERSION }}/' publish/meta.json 26 | cd publish 27 | zip -r "AcidBubbles.Timeline.${{ github.run_number }}.var" * 28 | - name: GitHub release 29 | uses: softprops/action-gh-release@v1 30 | if: startsWith(github.ref, 'refs/tags/') 31 | with: 32 | draft: true 33 | files: publish/AcidBubbles.Timeline.${{ github.run_number }}.var 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /src/UI/Screens/LockedScreen.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class LockedScreen : ScreenBase 4 | { 5 | public const string ScreenName = "Locked"; 6 | 7 | public override string screenId => ScreenName; 8 | 9 | public override void Init(IAtomPlugin plugin, object arg) 10 | { 11 | base.Init(plugin, arg); 12 | 13 | InitExplanation(); 14 | } 15 | 16 | private void InitExplanation() 17 | { 18 | string text; 19 | if(animationEditContext.locked) 20 | text = @"Enable ""Edit Mode"" to make modifications to this animation"; 21 | else if (!plugin.isActiveAndEnabled) 22 | text = "Enable the Timeline plugin as well as the containing atom to make modifications to this animation"; 23 | else 24 | text = "Could not identify a lock reason"; 25 | 26 | var textJSON = new JSONStorableString("Help", $@" 27 | Locked 28 | 29 | {text} 30 | "); 31 | var textUI = prefabFactory.CreateTextField(textJSON); 32 | textUI.height = 350f; 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /tests/Specs/AnimationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace VamTimeline 5 | { 6 | public class AnimationTests : ITestClass 7 | { 8 | public IEnumerable GetTests() 9 | { 10 | yield return new Test(nameof(EmptyAnimation), EmptyAnimation); 11 | } 12 | 13 | public IEnumerable EmptyAnimation(TestContext context) 14 | { 15 | context.Assert(context.animation.clips.Count, 1, "Only one clip"); 16 | context.Assert(context.animation.clips.Count, 1, "Only one clip state"); 17 | context.animation.PlayClip(context.animation.GetDefaultClip(), true); 18 | yield return 0f; 19 | context.Assert(context.animation.isPlaying, "Play should set isPlaying to true"); 20 | context.Assert(context.animation.clips[0].playbackEnabled, "Clips is enabled"); 21 | context.animation.StopAll(); 22 | yield return 0f; 23 | context.Assert(!context.animation.isPlaying, "Stop should set isPlaying to false"); 24 | context.Assert(!context.animation.clips[0].playbackEnabled, "Clip is disabled"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Framework/Test.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Text; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | namespace VamTimeline 8 | { 9 | public class Test 10 | { 11 | public readonly string name; 12 | private readonly Func _run; 13 | 14 | public Test(string name, Func run) 15 | { 16 | this.name = name; 17 | _run = run; 18 | } 19 | 20 | public IEnumerable Run(MVRScript testPlugin, StringBuilder output) 21 | { 22 | var go = new GameObject(); 23 | go.transform.SetParent(testPlugin.gameObject.transform, false); 24 | 25 | var animation = go.AddComponent(); 26 | animation.CreateClip(AtomAnimationClip.DefaultAnimationName, AtomAnimationClip.DefaultAnimationLayer, AtomAnimationClip.DefaultAnimationSegment); 27 | animation.RebuildAnimationNow(); 28 | 29 | var context = new TestContext(go, output, animation); 30 | 31 | foreach (var x in _run(context)) 32 | yield return x; 33 | 34 | Object.Destroy(go); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/Base/IAtomAnimationTarget.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine.Events; 3 | 4 | namespace VamTimeline 5 | { 6 | public interface IAtomAnimationTarget : IDisposable 7 | { 8 | UnityEvent onAnimationKeyframesDirty { get; } 9 | UnityEvent onAnimationKeyframesRebuilt { get; } 10 | bool dirty { get; set; } 11 | string name { get; } 12 | bool selected { get; set; } 13 | bool collapsed { get; set; } 14 | string group { get; set; } 15 | IAtomAnimationClip clip { get; set; } 16 | AnimatableRefBase animatableRefBase { get; } 17 | 18 | void Validate(float animationLength); 19 | 20 | void StartBulkUpdates(); 21 | void EndBulkUpdates(); 22 | 23 | bool TargetsSameAs(IAtomAnimationTarget target); 24 | string GetShortName(); 25 | string GetFullName(); 26 | 27 | float[] GetAllKeyframesTime(); 28 | float GetTimeClosestTo(float time); 29 | bool HasKeyframe(float time); 30 | void DeleteFrame(float time); 31 | void AddEdgeFramesIfMissing(float animationLength); 32 | 33 | ISnapshot GetSnapshot(float time); 34 | void SetSnapshot(float time, ISnapshot snapshot); 35 | 36 | void SelectInVam(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/UI/Components/Curves/CurvesStyle.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class CurvesStyle : StyleBase 6 | { 7 | public static CurvesStyle Default() 8 | { 9 | return new CurvesStyle(); 10 | } 11 | 12 | public float Padding { get; } = 16f; 13 | 14 | // Curves 15 | public float CurveLineSize { get; } = 2f; 16 | public float HandleSize { get; } = 5f; 17 | 18 | // Scrubber 19 | public float ScrubberSize { get; } = 2f; 20 | public Color ScrubberColor { get; } = new Color(0.88f, 0.84f, 0.86f); 21 | 22 | // Guides 23 | public float ZeroLineSize { get; } = 2f; 24 | public float SecondLineSize { get; } = 2f; 25 | public float BorderSize { get; } = 2f; 26 | public Color ZeroLineColor { get; } = new Color(0.5f, 0.5f, 0.55f); 27 | public Color SecondLineColor { get; } = new Color(0.6f, 0.6f, 0.65f); 28 | public Color BorderColor { get; } = new Color(0.9f, 0.8f, 0.95f); 29 | public Color CurveLineColorX { get; } = new Color(1.0f, 0.2f, 0.2f); 30 | public Color CurveLineColorY { get; } = new Color(0.2f, 1.0f, 0.2f); 31 | public Color CurveLineColorZ { get; } = new Color(0.2f, 0.2f, 1.0f); 32 | public Color CurveLineColorFloat { get; } = new Color(1f, 1f, 1f); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/vam.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your vam-timeline workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "Log Message": { 19 | "scope": "csharp", 20 | "prefix": "logm", 21 | "body": [ 22 | "SuperController.LogMessage($\"$0\");" 23 | ], 24 | "description": "Write to VaM's log" 25 | }, 26 | "Log Array": { 27 | "scope": "csharp", 28 | "prefix": "loga", 29 | "body": [ 30 | "SuperController.LogMessage($\"${1:prefix}: {string.Join(\", \", ${2:list}.Select(x => $\"{${3:prop}}\").ToArray())}\");" 31 | ], 32 | "description": "Write an array's values to VaM's log" 33 | } 34 | } -------------------------------------------------------------------------------- /src/UI/Screens/GlobalTriggersScreen.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class GlobalTriggersScreen : ScreenBase 4 | { 5 | public const string ScreenName = "Global Triggers"; 6 | 7 | public override string screenId => ScreenName; 8 | 9 | public override void Init(IAtomPlugin plugin, object arg) 10 | { 11 | base.Init(plugin, arg); 12 | 13 | CreateChangeScreenButton("< Back", MoreScreen.ScreenName); 14 | 15 | prefabFactory.CreateButton("On Clips Changed").button.onClick.AddListener(()=> 16 | { 17 | animation.clipListChangedTrigger.trigger.triggerActionsParent = popupParent; 18 | animation.clipListChangedTrigger.trigger.OpenTriggerActionsPanel(); 19 | }); 20 | prefabFactory.CreateButton("On Is Playing Changed").button.onClick.AddListener(()=> 21 | { 22 | animation.isPlayingChangedTrigger.trigger.triggerActionsParent = popupParent; 23 | animation.isPlayingChangedTrigger.trigger.OpenTriggerActionsPanel(); 24 | }); 25 | prefabFactory.CreateButton("On Current Animation Changed").button.onClick.AddListener(()=> 26 | { 27 | animation.currentAnimationChangedTrigger.trigger.triggerActionsParent = popupParent; 28 | animation.currentAnimationChangedTrigger.trigger.OpenTriggerActionsPanel(); 29 | }); 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/TargetReduceProcessorBase.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public abstract class TargetReduceProcessorBase where T : class, ICurveAnimationTarget 4 | { 5 | protected readonly T source; 6 | protected readonly ReduceSettings settings; 7 | protected T branch; 8 | 9 | protected TargetReduceProcessorBase(T source, ReduceSettings settings) 10 | { 11 | this.source = source; 12 | this.settings = settings; 13 | } 14 | 15 | public void Branch() 16 | { 17 | branch = source.Clone(false) as T; 18 | } 19 | 20 | public void Commit() 21 | { 22 | source.RestoreFrom(branch); 23 | branch = null; 24 | } 25 | 26 | public ReducerBucket CreateBucket(int from, int to) 27 | { 28 | var bucket = new ReducerBucket 29 | { 30 | from = from, 31 | to = to, 32 | keyWithLargestDelta = -1 33 | }; 34 | for (var i = from; i <= to; i++) 35 | { 36 | var delta = GetComparableNormalizedValue(i); 37 | if (delta > bucket.largestDelta) 38 | { 39 | bucket.largestDelta = delta; 40 | bucket.keyWithLargestDelta = i; 41 | } 42 | } 43 | return bucket; 44 | } 45 | 46 | public abstract float GetComparableNormalizedValue(int key); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/UI/Screens/AddAnimationsScreen.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class AddAnimationsScreen : ScreenBase 4 | { 5 | public const string ScreenName = "Add animations"; 6 | 7 | public override string screenId => ScreenName; 8 | 9 | #region Init 10 | 11 | public override void Init(IAtomPlugin plugin, object arg) 12 | { 13 | base.Init(plugin, arg); 14 | 15 | // Right side 16 | 17 | CreateChangeScreenButton($"< Back to {AnimationsScreen.ScreenName}", AnimationsScreen.ScreenName); 18 | 19 | prefabFactory.CreateSpacer(); 20 | prefabFactory.CreateHeader("Create", 1); 21 | 22 | CreateChangeScreenButton("Create animation...", AddClipScreen.ScreenName); 23 | CreateChangeScreenButton("Create layer...", AddLayerScreen.ScreenName); 24 | if (animation.index.useSegment) 25 | CreateChangeScreenButton("Create segment...", AddSegmentScreen.ScreenName); 26 | else 27 | CreateChangeScreenButton("Use segments...", AddSegmentScreen.ScreenName); 28 | 29 | prefabFactory.CreateSpacer(); 30 | prefabFactory.CreateHeader("More", 1); 31 | 32 | CreateChangeScreenButton("Import from file...", ImportExportScreen.ScreenName); 33 | CreateChangeScreenButton("Manage/reorder animations...", ManageAnimationsScreen.ScreenName); 34 | } 35 | 36 | #endregion 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/AtomAnimations/Editing/AtomAnimationBackup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace VamTimeline 6 | { 7 | public class AtomAnimationBackup 8 | { 9 | public static readonly AtomAnimationBackup singleton = new AtomAnimationBackup(); 10 | 11 | private AtomAnimationClip _owner; 12 | private List _backup; 13 | public string backupTime { get; private set; } 14 | 15 | public bool HasBackup(AtomAnimationClip owner) 16 | { 17 | return _backup != null && _owner == owner; 18 | } 19 | 20 | public void TakeBackup(AtomAnimationClip owner) 21 | { 22 | ClearBackup(); 23 | _owner = owner; 24 | _backup = owner.GetAllCurveTargets().Where(t => t.selected).Select(t => t.Clone(true)).ToList(); 25 | backupTime = DateTime.Now.ToShortTimeString(); 26 | } 27 | 28 | public void RestoreBackup(AtomAnimationClip owner) 29 | { 30 | if (!HasBackup(owner)) return; 31 | var targets = owner.GetAllCurveTargets().Where(t => t.selected).ToList(); 32 | foreach (var backup in _backup) 33 | { 34 | var target = targets.FirstOrDefault(t => t.TargetsSameAs(backup)); 35 | target?.RestoreFrom(backup); 36 | } 37 | } 38 | 39 | public void ClearBackup() 40 | { 41 | _owner = null; 42 | _backup = null; 43 | backupTime = null; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AtomAnimations/BezierCurves/BezierKeyframe.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace VamTimeline 4 | { 5 | public struct BezierKeyframe 6 | { 7 | public const int NullKeyframeCurveType = -1; 8 | public static readonly BezierKeyframe NullKeyframe = new BezierKeyframe(0, 0, -1); 9 | 10 | public float time; 11 | public float value; 12 | public float controlPointIn; 13 | public float controlPointOut; 14 | public int curveType; 15 | 16 | public BezierKeyframe(float time, float value, int curveType) 17 | : this(time, value, curveType, value, value) 18 | { 19 | 20 | } 21 | 22 | public BezierKeyframe(float time, float value, int curveType, float controlPointIn, float controlPointOut) 23 | { 24 | this.time = time; 25 | this.value = value; 26 | this.curveType = curveType; 27 | this.controlPointIn = controlPointIn; 28 | this.controlPointOut = controlPointOut; 29 | } 30 | 31 | [MethodImpl(256)] 32 | public bool IsNull() 33 | { 34 | return curveType == NullKeyframeCurveType; 35 | } 36 | 37 | [MethodImpl(256)] 38 | public bool HasValue() 39 | { 40 | return curveType != NullKeyframeCurveType; 41 | } 42 | 43 | public override string ToString() 44 | { 45 | return $"{time: 0.000}: {value:0.000} ({CurveTypeValues.FromInt(curveType)}, {controlPointIn:0.0}/{controlPointOut:0.0})"; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/UI/Components/GradientImage.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class GradientImage : MaskableGraphic 7 | { 8 | private Color _top; 9 | private Color _bottom; 10 | 11 | public Color top 12 | { 13 | get 14 | { 15 | return _top; 16 | } 17 | set 18 | { 19 | _top = value; 20 | SetVerticesDirty(); 21 | } 22 | } 23 | public Color bottom 24 | { 25 | get 26 | { 27 | return _bottom; 28 | } 29 | set 30 | { 31 | _bottom = value; 32 | SetVerticesDirty(); 33 | } 34 | } 35 | 36 | protected override void OnPopulateMesh(VertexHelper vh) 37 | { 38 | vh.Clear(); 39 | 40 | var rect = rectTransform.rect; 41 | vh.AddUIVertexQuad(new[] 42 | { 43 | CreateVertex(new Vector2(rect.xMin, rect.yMin), bottom), 44 | CreateVertex(new Vector2(rect.xMin, rect.yMax), top), 45 | CreateVertex(new Vector2(rect.xMax, rect.yMax), top), 46 | CreateVertex(new Vector2(rect.xMax, rect.yMin), bottom) 47 | }); 48 | } 49 | 50 | private static UIVertex CreateVertex(Vector2 pos, Color color) 51 | { 52 | var vert = UIVertex.simpleVert; 53 | vert.color = color; 54 | vert.position = pos; 55 | return vert; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Interop/StorableNames.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public static class StorableNames 4 | { 5 | public const string Animation = "Animation"; 6 | public const string Segment = "Segment"; 7 | public const string NextAnimation = "Next Animation"; 8 | public const string PreviousAnimation = "Previous Animation"; 9 | public const string NextAnimationInMainLayer = "Next Animation (Main Layer)"; 10 | public const string PreviousAnimationInMainLayer = "Previous Animation (Main Layer)"; 11 | public const string NextSegment = "Next Segment"; 12 | public const string PreviousSegment = "Previous Segment"; 13 | public const string Scrubber = "Scrubber"; 14 | public const string Time = "Set Time"; 15 | public const string Play = "Play"; 16 | public const string PlayIfNotPlaying = "Play If Not Playing"; 17 | public const string PlayCurrentClip = "Play Current Clip"; 18 | public const string IsPlaying = "Is Playing"; 19 | public const string Stop = "Stop"; 20 | public const string StopIfPlaying = "Stop If Playing"; 21 | public const string StopAndReset = "Stop And Reset"; 22 | public const string CreateQueue = "Create Queue"; 23 | public const string AddToQueue = "Add To Queue"; 24 | public const string PlayQueue = "Play Queue"; 25 | public const string ClearQueue = "Clear Queue"; 26 | public const string NextFrame = "Next Frame"; 27 | public const string PreviousFrame = "Previous Frame"; 28 | public const string Speed = "Speed"; 29 | public const string Weight = "Weight"; 30 | public const string Locked = "Locked"; 31 | public const string Paused = "Paused"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/TargetsOperations.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace VamTimeline 4 | { 5 | public class TargetsOperations 6 | { 7 | private readonly Atom _containingAtom; 8 | private readonly AtomAnimation _animation; 9 | private readonly AtomAnimationClip _clip; 10 | 11 | public TargetsOperations(Atom containingAtom, AtomAnimation animation, AtomAnimationClip clip) 12 | { 13 | _containingAtom = containingAtom; 14 | _animation = animation; 15 | _clip = clip; 16 | } 17 | 18 | public FreeControllerV3AnimationTarget Add(FreeControllerV3 fc) 19 | { 20 | if (fc == null) return null; 21 | var target = _clip.targetControllers.FirstOrDefault(t => t.animatableRef.Targets(fc)); 22 | if (target != null) return target; 23 | foreach (var clip in _animation.index.ByLayerQualified(_clip.animationLayerQualifiedId)) 24 | { 25 | var t = clip.AddController(_animation.animatables.GetOrCreateController(fc, fc.containingAtom == _containingAtom), true, true); 26 | if (t == null) continue; 27 | t.SetKeyframeToCurrent(0f); 28 | t.SetKeyframeToCurrent(clip.animationLength); 29 | if (clip == _clip) target = t; 30 | } 31 | return target; 32 | } 33 | 34 | public void AddSelectedController() 35 | { 36 | var selected = SuperController.singleton.GetSelectedController(); 37 | if (selected == null || selected.containingAtom != _containingAtom) return; 38 | if (_animation.index.ByController().Any(kvp => kvp.Key.Targets(selected))) return; 39 | Add(selected); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | # Styles 10 | 11 | dotnet_naming_style.pascal_case.capitalization = pascal_case 12 | 13 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 14 | dotnet_naming_style.prefix_underscore.required_prefix = _ 15 | 16 | # Const 17 | 18 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 19 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 20 | dotnet_naming_symbols.constant_fields.required_modifiers = const 21 | 22 | dotnet_naming_rule.const_pascal_case.symbols = constant_fields 23 | dotnet_naming_rule.const_pascal_case.capitalization = pascal_case 24 | dotnet_naming_rule.const_pascal_case.severity = suggestion 25 | 26 | # Private 27 | 28 | dotnet_naming_symbols.private_fields.applicable_kinds = field 29 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 30 | 31 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 32 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 33 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion 34 | 35 | # Public Properties 36 | 37 | dotnet_naming_symbols.public_fields.applicable_kinds = field, property 38 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public, protected 39 | 40 | dotnet_naming_rule.public_camel_case.symbols = public_fields 41 | dotnet_naming_rule.public_camel_case.capitalization = camel_case 42 | dotnet_naming_rule.public_camel_case.severity = suggestion -------------------------------------------------------------------------------- /src/Interop/RectTransformExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using UnityEngine; 3 | 4 | namespace VamTimeline 5 | { 6 | [SuppressMessage("ReSharper", "UnusedMember.Global")] 7 | public static class RectTransformExtensions 8 | { 9 | public static void StretchParent(this RectTransform rect) 10 | { 11 | rect.anchorMin = Vector2.zero; 12 | rect.anchorMax = Vector2.one; 13 | rect.anchoredPosition = new Vector2(0, 0); 14 | rect.sizeDelta = new Vector2(0, 0); 15 | } 16 | 17 | public static void StretchTop(this RectTransform rect) 18 | { 19 | rect.anchorMin = new Vector2(0, 1); 20 | rect.anchorMax = new Vector2(1, 1); 21 | rect.anchoredPosition = new Vector2(0, 0); 22 | rect.sizeDelta = new Vector2(0, 0); 23 | } 24 | 25 | public static void StretchBottom(this RectTransform rect) 26 | { 27 | rect.anchorMin = new Vector2(0, 0); 28 | rect.anchorMax = new Vector2(1, 0); 29 | rect.anchoredPosition = new Vector2(0, 0); 30 | rect.sizeDelta = new Vector2(0, 0); 31 | } 32 | 33 | public static void StretchLeft(this RectTransform rect) 34 | { 35 | rect.anchorMin = new Vector2(0, 0); 36 | rect.anchorMax = new Vector2(0, 1); 37 | rect.anchoredPosition = new Vector2(0, 0); 38 | rect.sizeDelta = new Vector2(0, 0); 39 | } 40 | 41 | public static void StretchCenter(this RectTransform rect) 42 | { 43 | rect.anchorMin = new Vector2(0.5f, 0); 44 | rect.anchorMax = new Vector2(0.5f, 1); 45 | rect.anchoredPosition = new Vector2(0, 0); 46 | rect.sizeDelta = new Vector2(0, 0); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Update-FilesLists.ps1: -------------------------------------------------------------------------------- 1 | cd (Split-Path ($MyInvocation.MyCommand.Path)) 2 | 3 | # Get Atom animation files 4 | $atomAnimationFiles = ( ` 5 | ls ./src/*.cs -Recurse ` 6 | | ? { -not $_.FullName.Contains("Controller\") -and $_.Name -ne "ControllerPlugin.cs" } ` 7 | | % { $_.FullName.Substring((pwd).Path.Length + 1) } ` 8 | ) 9 | 10 | # VamTimeline.AtomAnimation.cslist 11 | $atomAnimationFiles > .\VamTimeline.AtomAnimation.cslist 12 | 13 | # VamTimeline.csproj 14 | ( Get-Content ".\VamTimeline.csproj" -Raw ) -Replace "(?sm)(?<=^ +`r?`n).*?(?=`r?`n +)", ` 15 | [System.String]::Join("`r`n", ($atomAnimationFiles | % { " " } ) ) ` 16 | | Set-Content ".\VamTimeline.csproj" -NoNewline 17 | 18 | # meta.json 19 | $allFiles = (ls ./src/*.cs -Recurse) + (ls *.cslist) ` 20 | | % { $_.FullName.Substring((pwd).Path.Length + 1) } 21 | ( Get-Content ".\meta.json" -Raw ) -Replace "(?sm)(?<=^ `"contentList`": \[`r?`n).*?(?=`r?`n \],)", ` 22 | [System.String]::Join("`r`n", ($allFiles | % { " `"Custom\\Scripts\\AcidBubbles\\Timeline\\$($_.Replace("\", "\\"))`"," } ) ).Trim(",") ` 23 | | Set-Content ".\meta.json" -NoNewline 24 | 25 | # tests/VamTimeline.Tests.cslist 26 | $testFiles = ( ` 27 | ls ./tests/*.cs -Recurse ` 28 | | % { $_.FullName.Substring((pwd).Path.Length + 7) } ` 29 | ) 30 | $testFilesAndDependencies = ( ` 31 | $atomAnimationFiles ` 32 | | ? { -not $_.EndsWith("\AtomPlugin.cs") } ` 33 | | % { "..\$_" } ` 34 | ) + $testFiles 35 | $testFilesAndDependencies > .\tests\VamTimeline.Tests.cslist 36 | 37 | ( Get-Content ".\VamTimeline.csproj" -Raw ) -Replace "(?sm)(?<=^ +`r?`n).*?(?=`r?`n +)", ` 38 | [System.String]::Join("`r`n", ($testFiles | % { " " } ) ) ` 39 | | Set-Content ".\VamTimeline.csproj" -NoNewline -------------------------------------------------------------------------------- /src/UI/Components/SimpleSlider.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.Events; 3 | using UnityEngine.EventSystems; 4 | 5 | namespace VamTimeline 6 | { 7 | public class SimpleSlider : UIBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler, IEndDragHandler 8 | { 9 | public class SliderChangeEvent : UnityEvent { } 10 | public readonly SliderChangeEvent onChange = new SliderChangeEvent(); 11 | public bool interacting { get; private set; } 12 | 13 | public void OnPointerDown(PointerEventData eventData) 14 | { 15 | eventData.useDragThreshold = false; 16 | DispatchOnChange(eventData); 17 | } 18 | 19 | public void OnBeginDrag(PointerEventData eventData) 20 | { 21 | interacting = true; 22 | DispatchOnChange(eventData); 23 | } 24 | 25 | public void OnDrag(PointerEventData eventData) 26 | { 27 | DispatchOnChange(eventData); 28 | } 29 | 30 | public void OnEndDrag(PointerEventData eventData) 31 | { 32 | DispatchOnChange(eventData); 33 | interacting = false; 34 | } 35 | 36 | protected override void OnDestroy() 37 | { 38 | onChange.RemoveAllListeners(); 39 | base.OnDestroy(); 40 | } 41 | 42 | private void DispatchOnChange(PointerEventData eventData) 43 | { 44 | Vector2 localPosition; 45 | var rectTransform = GetComponent(); 46 | if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, eventData.pressEventCamera, out localPosition)) 47 | return; 48 | var rect = rectTransform.rect; 49 | var ratio = Mathf.Clamp01((localPosition.x + rect.width / 2f) / rect.width); 50 | onChange.Invoke(ratio); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/KeyframesOperations.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace VamTimeline 5 | { 6 | public class KeyframesOperations 7 | { 8 | private readonly AtomAnimationClip _clip; 9 | 10 | public KeyframesOperations(AtomAnimationClip clip) 11 | { 12 | _clip = clip; 13 | } 14 | 15 | public void RemoveAll(IEnumerable targets) 16 | { 17 | foreach (var target in targets) 18 | { 19 | RemoveAll(target); 20 | } 21 | } 22 | 23 | public void RemoveAll(IAtomAnimationTarget target, bool includeEdges = false, float fromTime = 0f) 24 | { 25 | target.StartBulkUpdates(); 26 | try 27 | { 28 | foreach (var time in target.GetAllKeyframesTime()) 29 | { 30 | if (time < fromTime) continue; 31 | if (!includeEdges && (time <= 0f || time >= _clip.animationLength)) continue; 32 | target.DeleteFrame(time); 33 | } 34 | } 35 | finally 36 | { 37 | target.EndBulkUpdates(); 38 | } 39 | } 40 | 41 | public void AddSelectedController() 42 | { 43 | var selected = SuperController.singleton.GetSelectedController(); 44 | if (selected == null) return; 45 | var target = _clip.targetControllers.FirstOrDefault(t => t.animatableRef.controller == selected); 46 | target?.SetKeyframeToCurrent(_clip.clipTime.Snap()); 47 | } 48 | 49 | public void AddAllControllers() 50 | { 51 | foreach (var target in _clip.targetControllers) 52 | { 53 | target.SetKeyframeToCurrent(_clip.clipTime.Snap()); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AtomAnimations/BezierCurves/CurveTypeValues.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace VamTimeline 5 | { 6 | public static class CurveTypeValues 7 | { 8 | public const int Undefined = -1; 9 | public const int LeaveAsIs = 0; 10 | public const int Flat = 1; 11 | public const int FlatLong = 9; 12 | public const int Linear = 2; 13 | public const int SmoothLocal = 3; 14 | public const int Constant = 8; 15 | public const int Bounce = 4; 16 | public const int LinearFlat = 5; 17 | public const int FlatLinear = 6; 18 | public const int CopyPrevious = 7; 19 | public const int SmoothGlobal = 10; 20 | 21 | private static readonly Dictionary _labelMap = new Dictionary 22 | { 23 | {SmoothLocal, "Smooth (Local)"}, 24 | {SmoothGlobal, "Smooth (Global)"}, 25 | {Linear, "Linear"}, 26 | {Constant, "Constant"}, 27 | {Flat, "Flat"}, 28 | {FlatLong, "Flat (Long)"}, 29 | {Bounce, "Bounce"}, 30 | {LinearFlat, "Linear -> Flat"}, 31 | {FlatLinear, "Flat -> Linear"}, 32 | {CopyPrevious, "Copy Previous Keyframe"}, 33 | {LeaveAsIs, "Leave As-Is"} 34 | }; 35 | 36 | public static readonly List choicesList = new List 37 | { 38 | FromInt(SmoothGlobal), 39 | FromInt(SmoothLocal), 40 | FromInt(Linear), 41 | FromInt(Constant), 42 | FromInt(Flat), 43 | FromInt(Bounce), 44 | FromInt(LinearFlat), 45 | FromInt(FlatLinear) 46 | }; 47 | 48 | public static string FromInt(int v) 49 | { 50 | string r; 51 | if (_labelMap.TryGetValue(v, out r)) return r; 52 | return "?"; 53 | } 54 | 55 | public static int ToInt(string v) 56 | { 57 | return _labelMap.FirstOrDefault(l => l.Value == v).Key; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/AtomAnimations/Logging/Logger.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using UnityEngine; 3 | 4 | namespace VamTimeline 5 | { 6 | public class Logger 7 | { 8 | public bool clearOnPlay { get; set; } 9 | 10 | public bool general { get; set; } 11 | public readonly string generalCategory = "gen"; 12 | public bool triggersReceived { get; set; } 13 | public bool triggersInvoked { get; set; } 14 | public readonly string triggersCategory = "trig"; 15 | public bool sequencing { get; set; } 16 | public readonly string sequencingCategory = "seq"; 17 | public bool peersSync { get; set; } 18 | public readonly string peersSyncCategory = "peer"; 19 | 20 | public bool debug; 21 | public readonly string debugCategory = "dbg"; 22 | 23 | public bool showPlayInfoInHelpText; 24 | 25 | public Regex filter { get; set; } 26 | 27 | private readonly Atom _containingAtom; 28 | private float _startTime = Time.time; 29 | 30 | public Logger(Atom containingAtom) 31 | { 32 | _containingAtom = containingAtom; 33 | } 34 | 35 | public void Begin() 36 | { 37 | if(clearOnPlay) SuperController.singleton.ClearMessages(); 38 | _startTime = Time.time; 39 | } 40 | 41 | public void Log(string category, string message) 42 | { 43 | if (filter != null && !filter.IsMatch(message)) return; 44 | SuperController.LogMessage($"[{(Time.time - _startTime) % 100:00.000}|{_containingAtom.name}|{category}] {message}"); 45 | } 46 | 47 | public void EnableDefault() 48 | { 49 | general = true; 50 | triggersReceived = true; 51 | sequencing = true; 52 | peersSync = true; 53 | } 54 | 55 | public void ShowTemporaryMessage(string message) 56 | { 57 | SuperController.singleton.CancelInvoke(nameof(SuperController.HideTempHelp)); 58 | SuperController.singleton.ShowTempHelp(message); 59 | SuperController.singleton.Invoke(nameof(SuperController.HideTempHelp), 5); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AtomAnimations/Utils/FloatExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using UnityEngine; 4 | 5 | namespace VamTimeline 6 | { 7 | public static class FloatExtensions 8 | { 9 | [MethodImpl(256)] 10 | public static bool IsSameFrame(this float value, float time) 11 | { 12 | return value > time - 0.0005f && value < time + 0.0005f; 13 | } 14 | 15 | [MethodImpl(256)] 16 | public static float Snap(this float value, float range = 0f) 17 | { 18 | value = (float)(Math.Round(value * 1000f) / 1000f); 19 | 20 | if (value < 0f) return 0f; 21 | 22 | if (range == 0f) return value; 23 | 24 | var snapDelta = Mathf.Repeat(value, range); 25 | if (snapDelta == 0f) return value; 26 | 27 | value -= snapDelta; 28 | if (snapDelta > range / 2f) 29 | value += range; 30 | 31 | return value; 32 | } 33 | 34 | [MethodImpl(256)] 35 | public static int ToMilliseconds(this float value) 36 | { 37 | return (int)Math.Round(value * 1000f); 38 | } 39 | 40 | [MethodImpl(256)] 41 | public static float ExponentialScale(this float value, float midValue, float maxValue) 42 | { 43 | var m = maxValue / midValue; 44 | var c = Mathf.Log(Mathf.Pow(m - 1, 2)); 45 | var b = maxValue / (Mathf.Exp(c) - 1); 46 | var a = -1 * b; 47 | return a + b * Mathf.Exp(c * value); 48 | } 49 | 50 | public static float RoundToNearest(this float value, float modulo) 51 | { 52 | var half = modulo / 2; 53 | return value + half - (value + half) % modulo; 54 | } 55 | 56 | public static float Modulo(this float value, float mod) 57 | { 58 | return (value % mod + mod) % mod; 59 | } 60 | 61 | [MethodImpl(256)] 62 | public static float SmootherStep(this float x) 63 | { 64 | // Source: https://en.wikipedia.org/wiki/Smoothstep 65 | return x * x * x * (x * (x * 6 - 15) + 10); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/UI/Screens/GroupingScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace VamTimeline 4 | { 5 | public class GroupingScreen : ScreenBase 6 | { 7 | public const string ScreenName = "Grouping"; 8 | 9 | private JSONStorableString _assignGroupJSON; 10 | 11 | public override string screenId => ScreenName; 12 | 13 | public override void Init(IAtomPlugin plugin, object arg) 14 | { 15 | base.Init(plugin, arg); 16 | 17 | CreateChangeScreenButton("< Back", MoreScreen.ScreenName); 18 | 19 | InitAssignToGroupUI(); 20 | 21 | prefabFactory.CreateButton("Assign to selected targets").button.onClick.AddListener(Assign); 22 | 23 | animation.animatables.onTargetsSelectionChanged.AddListener(OnTargetsSelectionChanged); 24 | OnTargetsSelectionChanged(); 25 | } 26 | 27 | private void InitAssignToGroupUI() 28 | { 29 | _assignGroupJSON = new JSONStorableString("Group name (empty for auto)", ""); 30 | prefabFactory.CreateTextInput(_assignGroupJSON); 31 | } 32 | 33 | private void OnTargetsSelectionChanged() 34 | { 35 | var groups = animationEditContext.GetSelectedTargets().Select(t => t.group).Distinct().ToList(); 36 | if (groups.Count == 1) _assignGroupJSON.val = groups[0]; 37 | } 38 | 39 | private void Assign() 40 | { 41 | var group = _assignGroupJSON.val; 42 | if (string.IsNullOrEmpty(group)) group = null; 43 | foreach (var target in animationEditContext.GetSelectedTargets()) 44 | { 45 | foreach (var c in animationEditContext.currentLayer) 46 | { 47 | var t = c.GetAllTargets().FirstOrDefault(x => x.TargetsSameAs(target)); 48 | if (t == null) continue; 49 | t.group = group; 50 | } 51 | } 52 | current.onTargetsListChanged.Invoke(); 53 | } 54 | 55 | public override void OnDestroy() 56 | { 57 | animation.animatables.onTargetsSelectionChanged.RemoveListener(OnTargetsSelectionChanged); 58 | base.OnDestroy(); 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/UI/Components/ScreenTabs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.Events; 4 | using UnityEngine.UI; 5 | 6 | namespace VamTimeline 7 | { 8 | public class ScreenTabs : MonoBehaviour 9 | { 10 | public class TabSelectedEvent : UnityEvent { } 11 | 12 | public static ScreenTabs Create(Transform parent, Transform buttonPrefab) 13 | { 14 | var go = new GameObject(); 15 | go.transform.SetParent(parent, false); 16 | 17 | go.AddComponent().minHeight = 60f; 18 | 19 | var group = go.AddComponent(); 20 | group.spacing = 4f; 21 | group.childForceExpandWidth = true; 22 | group.childControlHeight = false; 23 | 24 | var tabs = go.AddComponent(); 25 | tabs.buttonPrefab = buttonPrefab; 26 | 27 | return tabs; 28 | } 29 | 30 | public readonly TabSelectedEvent onTabSelected = new TabSelectedEvent(); 31 | public List tabs = new List(); 32 | public Transform buttonPrefab; 33 | 34 | public UIDynamicButton Add(string name, Color color, string label = null, float preferredWidth = 0) 35 | { 36 | var rt = Instantiate(buttonPrefab, transform, false); 37 | 38 | var btn = rt.gameObject.GetComponent(); 39 | btn.name = name; 40 | btn.label = label ?? name; 41 | btn.buttonColor = color; 42 | btn.buttonText.fontSize = 26; 43 | 44 | if (preferredWidth > 0) 45 | { 46 | var layout = btn.gameObject.GetComponent(); 47 | layout.minWidth = preferredWidth; 48 | layout.preferredWidth = preferredWidth; 49 | } 50 | 51 | btn.button.onClick.AddListener(() => 52 | { 53 | Select(name); 54 | onTabSelected.Invoke(name); 55 | }); 56 | 57 | tabs.Add(btn); 58 | 59 | return btn; 60 | } 61 | 62 | public void Select(string screenName) 63 | { 64 | foreach (var btn in tabs) 65 | { 66 | btn.button.interactable = btn.name != screenName; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/UI/Components/UIVertexHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | 5 | namespace VamTimeline 6 | { 7 | /// 8 | /// Vertex Helper 9 | /// By Yoooi, modified by Acidbubbles 10 | /// Source: https://github.com/acidbubbles/vam-curve-editor 11 | /// 12 | public static class UIVertexHelper 13 | { 14 | public static void DrawLine(this VertexHelper vh, IList points, float thickness, Color color) 15 | { 16 | for (var i = 1; i < points.Count; i++) 17 | { 18 | var prev = points[i - 1]; 19 | var curr = points[i]; 20 | var angle = Mathf.Atan2(curr.y - prev.y, curr.x - prev.x) * Mathf.Rad2Deg; 21 | 22 | var v1 = prev + new Vector2(0, -thickness / 2); 23 | var v2 = prev + new Vector2(0, +thickness / 2); 24 | var v3 = curr + new Vector2(0, +thickness / 2); 25 | var v4 = curr + new Vector2(0, -thickness / 2); 26 | 27 | v1 = RotatePointAroundPivot(v1, prev, angle); 28 | v2 = RotatePointAroundPivot(v2, prev, angle); 29 | v3 = RotatePointAroundPivot(v3, curr, angle); 30 | v4 = RotatePointAroundPivot(v4, curr, angle); 31 | 32 | vh.AddUIVertexQuad(CreateVBO(color, v1, v2, v3, v4)); 33 | // vh.AddUIVertexQuad(CreateVBO(new Color(Random.Range(0, 1), Random.Range(0, 1f), Random.Range(0, 1)), new[] { v1, v2, v3, v4 })); 34 | } 35 | } 36 | 37 | public static UIVertex[] CreateVBO(Color color, params Vector2[] vertices) 38 | { 39 | var vbo = new UIVertex[vertices.Length]; 40 | for (var i = 0; i < vertices.Length; i++) 41 | { 42 | var vert = UIVertex.simpleVert; 43 | vert.color = color; 44 | vert.position = vertices[i]; 45 | vbo[i] = vert; 46 | } 47 | 48 | return vbo; 49 | } 50 | 51 | private static Vector3 RotatePointAroundPivot(Vector3 point, Vector3 pivot, Vector3 angles) 52 | => Quaternion.Euler(angles) * (point - pivot) + pivot; 53 | private static Vector2 RotatePointAroundPivot(Vector2 point, Vector2 pivot, float angle) 54 | => RotatePointAroundPivot(point, pivot, angle * Vector3.forward); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AtomAnimations/BezierCurves/Smoothing/BezierAnimationCurveSmoothingBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace VamTimeline 5 | { 6 | public abstract class BezierAnimationCurveSmoothingBase 7 | { 8 | protected float[] _w; // Weights 9 | protected float[] _p1; // Out 10 | protected float[] _p2; // In 11 | protected float[] _r; // rhs vector 12 | protected float[] _a; 13 | protected float[] _b; 14 | protected float[] _c; 15 | // protected float _totalTime; 16 | // protected float _totalDistance; 17 | 18 | [MethodImpl(256)] 19 | protected static float Weighting(BezierKeyframe k1, BezierKeyframe k2) 20 | { 21 | return k2.time - k1.time; 22 | // if (_totalDistance == 0) return 1f; 23 | // return Vector2.Distance(new Vector2(1f - (k1.time / _totalTime), k1.value / _totalDistance), new Vector2(1f - (k2.time / _totalTime), k2.value / _totalDistance)); 24 | } 25 | 26 | // protected void ComputeTimeAndDistance(List keys) 27 | // { 28 | // _totalTime = 0f; 29 | // _totalDistance = 0f; 30 | // for (var i = 1; i < keys.Count; i++) 31 | // { 32 | // _totalTime += keys[i].time - keys[i - 1].time; 33 | // _totalDistance += Mathf.Abs(keys[i].value - keys[i - 1].value); 34 | // } 35 | // } 36 | 37 | [MethodImpl(256)] 38 | protected void AssignComputedControlPointsToKeyframes(List keys, int n) 39 | { 40 | var key0 = keys[0]; 41 | if (key0.curveType != CurveTypeValues.LeaveAsIs) 42 | { 43 | key0.controlPointOut = _p1[0]; 44 | keys[0] = key0; 45 | } 46 | for (var i = 1; i < n; i++) 47 | { 48 | var keyi = keys[i]; 49 | if (keyi.curveType != CurveTypeValues.LeaveAsIs) 50 | { 51 | keyi.controlPointIn = _p2[i - 1]; 52 | keyi.controlPointOut = _p1[i]; 53 | keys[i] = keyi; 54 | } 55 | } 56 | var keyn = keys[n]; 57 | if (keyn.curveType != CurveTypeValues.LeaveAsIs) 58 | { 59 | keyn.controlPointIn = _p2[n - 1]; 60 | keys[n] = keyn; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Specs/TargetsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using UnityEngine; 3 | 4 | namespace VamTimeline 5 | { 6 | public class TargetsHelper 7 | { 8 | private readonly TestContext _context; 9 | 10 | public TargetsHelper(TestContext context) 11 | { 12 | _context = context; 13 | } 14 | 15 | public FreeControllerV3Ref GivenFreeController(string name = "Test Controller") 16 | { 17 | var existing = _context.animation.animatables.controllers.FirstOrDefault(c => c.name == name); 18 | if (existing != null) return existing; 19 | 20 | var controller = new GameObject(name); 21 | controller.SetActive(false); 22 | controller.transform.SetParent(_context.gameObject.transform, false); 23 | var fc = controller.AddComponent(); 24 | fc.UITransforms = new Transform[0]; 25 | fc.UITransformsPlayMode = new Transform[0]; 26 | var animatable = _context.animation.animatables.GetOrCreateController(fc, true); 27 | return animatable; 28 | } 29 | 30 | public JSONStorableFloatRef GivenFloatParam(string storableName = "Test Storable", string floatParamName = "Test Float") 31 | { 32 | var existing = _context.animation.animatables.storableFloats.FirstOrDefault(c => c.storableId == storableName && c.floatParamName == floatParamName); 33 | if (existing != null) return existing; 34 | 35 | var storableGo = new GameObject(storableName); 36 | storableGo.transform.SetParent(_context.gameObject.transform, false); 37 | var storable = storableGo.AddComponent(); 38 | var param = new JSONStorableFloat("Test", 0, 0, 1); 39 | storable.RegisterFloat(param); 40 | var animatable = _context.animation.animatables.GetOrCreateStorableFloat(storable, param, true); 41 | return animatable; 42 | } 43 | 44 | public TriggersTrackRef GivenTriggers(int animationLayerQualifiedId = 0, string name = "Test Triggers") 45 | { 46 | var existing = _context.animation.animatables.triggers.FirstOrDefault(c => c.animationLayerQualifiedId == animationLayerQualifiedId && c.name == name); 47 | if (existing != null) return existing; 48 | 49 | var animatable = _context.animation.animatables.GetOrCreateTriggerTrack(animationLayerQualifiedId, name); 50 | return animatable; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/UI/Screens/MoreScreen.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class MoreScreen : ScreenBase 6 | { 7 | public const string ScreenName = "More..."; 8 | 9 | public override string screenId => ScreenName; 10 | 11 | public override void Init(IAtomPlugin plugin, object arg) 12 | { 13 | base.Init(plugin, arg); 14 | 15 | prefabFactory.CreateHeader("More options", 1); 16 | 17 | CreateChangeScreenButton("Import / export animations...", ImportExportScreen.ScreenName); 18 | CreateChangeScreenButton("Convert VaM native scene anim...", MocapScreen.ScreenName); 19 | CreateChangeScreenButton("Record animation...", RecordScreen.ScreenName); 20 | CreateChangeScreenButton("Reduce keyframes...", ReduceScreen.ScreenName); 21 | CreateChangeScreenButton("Smooth keyframes...", SmoothScreen.ScreenName); 22 | 23 | prefabFactory.CreateSpacer(); 24 | 25 | CreateChangeScreenButton("Bulk changes...", BulkScreen.ScreenName); 26 | CreateChangeScreenButton("Advanced keyframe tools...", AdvancedKeyframeToolsScreen.ScreenName); 27 | CreateChangeScreenButton("Grouping...", GroupingScreen.ScreenName); 28 | CreateChangeScreenButton("Global Triggers...", GlobalTriggersScreen.ScreenName); 29 | 30 | prefabFactory.CreateSpacer(); 31 | 32 | CreateChangeScreenButton("Diagnostics and scene analysis...", DiagnosticsScreen.ScreenName); 33 | CreateChangeScreenButton("Options...", OptionsScreen.ScreenName); 34 | CreateChangeScreenButton("Logging...", LoggingScreen.ScreenName); 35 | CreateChangeScreenButton("Defaults...", DefaultsScreen.ScreenName); 36 | 37 | prefabFactory.CreateSpacer(); 38 | 39 | CreateChangeScreenButton("Built-in Help", HelpScreen.ScreenName); 40 | var helpButton = prefabFactory.CreateButton("[Browser] Online help"); 41 | helpButton.button.onClick.AddListener(() => Application.OpenURL("https://github.com/acidbubbles/vam-timeline/wiki")); 42 | 43 | var patreonBtn = prefabFactory.CreateButton("[Browser] Support me on Patreon!"); 44 | patreonBtn.textColor = new Color(0.97647f, 0.40784f, 0.32941f); 45 | patreonBtn.buttonColor = Color.white; 46 | patreonBtn.button.onClick.AddListener(() => Application.OpenURL("https://www.patreon.com/acidbubbles")); 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/UI/Components/DopeSheet/DopeSheetStyle.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class DopeSheetStyle : StyleBase 6 | { 7 | public static DopeSheetStyle Default() 8 | { 9 | return new DopeSheetStyle(); 10 | } 11 | 12 | // Global 13 | public Color LabelsBackgroundColor { get; } = new Color(0.600f, 0.580f, 0.620f); 14 | public Color GroupBackgroundColorTop { get; } = new Color(0.874f, 0.870f, 0.870f); 15 | public Color GroupBackgroundColorBottom { get; } = new Color(0.704f, 0.700f, 0.700f); 16 | public Color LabelBackgroundColorTop { get; } = new Color(0.924f, 0.920f, 0.920f); 17 | public Color LabelBackgroundColorBottom { get; } = new Color(0.724f, 0.720f, 0.720f); 18 | public Color LabelBackgroundColorTopSelected { get; } = new Color(0.924f, 0.920f, 0.920f); 19 | public Color LabelBackgroundColorBottomSelected { get; } = new Color(1, 1, 1); 20 | public float RowHeight { get; } = 30f; 21 | public float RowSpacing { get; } = 5f; 22 | public float LabelWidth { get; } = 150f; 23 | public float LabelHorizontalPadding { get; } = 6f; 24 | public float GroupToggleWidth { get; } = 30f; 25 | 26 | // Timeline 27 | public float ZoomHeight { get; } = 30f; 28 | public float TimelineHeight { get; } = 60f; 29 | public float ToolbarHeight => ZoomHeight + TimelineHeight; 30 | 31 | // Keyframes 32 | public Color KeyframesRowLineColor { get; } = new Color(0.650f, 0.650f, 0.650f); 33 | public Color KeyframesRowLineColorSelected { get; } = new Color(0.750f, 0.750f, 0.750f); 34 | public Color KeyframeColor { get; } = new Color(0.050f, 0.020f, 0.020f); 35 | public Color KeyframeColorCurrentBack { get; } = new Color(0.050f, 0.020f, 0.020f); 36 | public Color KeyframeColorCurrentFront { get; } = new Color(0.350f, 0.320f, 0.320f); 37 | public Color KeyframeColorSelectedBack { get; } = new Color(0.050f, 0.020f, 0.020f); 38 | public Color KeyframeColorSelectedFront { get; } = new Color(0.950f, 0.820f, 0.920f); 39 | public float KeyframeSize { get; } = 6f; 40 | public float KeyframeSizeSelectedBack { get; } = 11f; 41 | public float KeyframeSizeSelectedFront { get; } = 5f; 42 | public float KeyframesRowPadding { get; } = 16f; 43 | public float KeyframesRowLineSize { get; } = 1f; 44 | 45 | // Scrubber 46 | public Color ScrubberColor { get; } = new Color(0.88f, 0.84f, 0.86f); 47 | public float ScrubberSize { get; } = 9f; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/SegmentsOperations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VamTimeline 4 | { 5 | public class SegmentsOperations 6 | { 7 | private readonly AtomAnimation _animation; 8 | private readonly AtomAnimationClip _clip; 9 | 10 | public SegmentsOperations(AtomAnimation animation, AtomAnimationClip clip) 11 | { 12 | _animation = animation; 13 | _clip = clip; 14 | } 15 | 16 | public AtomAnimationClip Add(string clipName, string layerName, string segmentName, string position) 17 | { 18 | var clip = _animation.CreateClip(clipName, layerName, segmentName, GetPosition(_clip, position)); 19 | return clip; 20 | } 21 | 22 | public AtomAnimationClip AddShared(string clipName) 23 | { 24 | var clip = _animation.CreateClip(clipName, AtomAnimationClip.DefaultAnimationLayer, AtomAnimationClip.SharedAnimationSegment, 0); 25 | return clip; 26 | } 27 | 28 | private int GetPosition(AtomAnimationClip clip, string position) 29 | { 30 | switch (position) 31 | { 32 | case AddAnimationOperations.Positions.PositionFirst: 33 | { 34 | var index = _animation.clips.FindIndex(c => !c.isOnSharedSegment); 35 | return index == -1 ? 0 : index; 36 | } 37 | case AddAnimationOperations.Positions.PositionPrevious: 38 | { 39 | return _animation.clips.FindIndex(c => c.animationSegment == clip.animationSegment); 40 | } 41 | case AddAnimationOperations.Positions.PositionNext: 42 | { 43 | var segmentIndex = _animation.clips.FindIndex(c => c.animationSegment == clip.animationSegment); 44 | var index = _animation.clips.FindIndex(segmentIndex, c => c.animationSegment != clip.animationSegment); 45 | return index == -1 ? _animation.clips.Count : index; 46 | } 47 | case AddAnimationOperations.Positions.PositionLast: 48 | { 49 | return _animation.clips.Count; 50 | } 51 | case AddAnimationOperations.Positions.NotSpecified: 52 | { 53 | return _animation.clips.Count; 54 | } 55 | default: 56 | { 57 | throw new NotSupportedException($"Unknown position '{position}'"); 58 | } 59 | } 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/LayersOperations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Leap.Unity; 5 | 6 | namespace VamTimeline 7 | { 8 | public class LayersOperations 9 | { 10 | private readonly AtomAnimation _animation; 11 | private readonly AtomAnimationClip _clip; 12 | 13 | public LayersOperations(AtomAnimation animation, AtomAnimationClip clip) 14 | { 15 | _animation = animation; 16 | _clip = clip; 17 | } 18 | 19 | public AtomAnimationClip Add(string clipName, string layerName) 20 | { 21 | return _animation.CreateClip(clipName, layerName, _clip.animationSegment); 22 | } 23 | 24 | public List AddAndCarry(string layerName) 25 | { 26 | return _animation.index.ByLayerQualified(_clip.animationLayerQualifiedId) 27 | .Select(c => 28 | { 29 | var r = _animation.CreateClip(c.animationName, layerName, c.animationSegment); 30 | c.CopySettingsTo(r); 31 | return r; 32 | }) 33 | .ToList(); 34 | } 35 | 36 | public List SplitLayer(List targets, string layerName = null) 37 | { 38 | var created = new List(); 39 | if (layerName == null) 40 | layerName = GetSplitLayerName(_clip.animationLayer, _animation.index.segmentsById[_clip.animationSegmentId].layerNames); 41 | foreach (var sourceClip in _animation.index.ByLayerQualified(_clip.animationLayerQualifiedId).ToList()) 42 | { 43 | var newClip = _animation.CreateClip(sourceClip.animationName, layerName, _clip.animationSegment); 44 | sourceClip.CopySettingsTo(newClip); 45 | foreach (var t in sourceClip.GetAllTargets().Where(t => targets.Any(t.TargetsSameAs)).ToList()) 46 | { 47 | sourceClip.Remove(t); 48 | newClip.AddAny(t); 49 | } 50 | created.Add(newClip); 51 | } 52 | return created; 53 | } 54 | 55 | private static string GetSplitLayerName(string sourceLayerName, IList list) 56 | { 57 | for (var i = 1; i < 999; i++) 58 | { 59 | var animationName = $"{sourceLayerName} (Split {i})"; 60 | if (list.All(n => n != animationName)) return animationName; 61 | } 62 | return Guid.NewGuid().ToString(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/AtomAnimations/Utils/QuaternionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using UnityEngine; 3 | 4 | namespace VamTimeline 5 | { 6 | public static class QuaternionUtil 7 | { 8 | // https://wiki.unity3d.com/index.php/Averaging_Quaternions_and_Vectors 9 | 10 | [MethodImpl(256)] 11 | public static void AverageQuaternion(ref Vector4 cumulative, Quaternion newRotation, Quaternion firstRotation, float weight) 12 | { 13 | //Before we add the new rotation to the average (mean), we have to check whether the quaternion has to be inverted. Because 14 | //q and -q are the same rotation, but cannot be averaged, we have to make sure they are all the same. 15 | if (!AreQuaternionsClose(newRotation, firstRotation)) 16 | { 17 | newRotation = newRotation.InverseSignQuaternion(); 18 | } 19 | 20 | //Average the values 21 | cumulative.w += newRotation.w * weight; 22 | cumulative.x += newRotation.x * weight; 23 | cumulative.y += newRotation.y * weight; 24 | cumulative.z += newRotation.z * weight; 25 | } 26 | 27 | [MethodImpl(256)] 28 | public static Quaternion FromVector(Vector4 cumulative) 29 | { 30 | //note: if speed is an issue, you can skip the normalization step 31 | return NormalizeQuaternion(cumulative.x, cumulative.y, cumulative.z, cumulative.w); 32 | } 33 | 34 | [MethodImpl(256)] 35 | private static Quaternion NormalizeQuaternion(float x, float y, float z, float w) 36 | { 37 | var lengthD = 1.0f / (w * w + x * x + y * y + z * z); 38 | w *= lengthD; 39 | x *= lengthD; 40 | y *= lengthD; 41 | z *= lengthD; 42 | 43 | return new Quaternion(x, y, z, w); 44 | } 45 | 46 | //Changes the sign of the quaternion components. This is not the same as the inverse. 47 | [MethodImpl(256)] 48 | public static Quaternion InverseSignQuaternion(this Quaternion q) 49 | { 50 | return new Quaternion(-q.x, -q.y, -q.z, -q.w); 51 | } 52 | 53 | //Returns true if the two input quaternions are close to each other. This can 54 | //be used to check whether or not one of two quaternions which are supposed to 55 | //be very similar but has its component signs reversed (q has the same rotation as 56 | //-q) 57 | [MethodImpl(256)] 58 | private static bool AreQuaternionsClose(Quaternion q1, Quaternion q2) 59 | { 60 | 61 | var dot = Quaternion.Dot(q1, q2); 62 | return !(dot < 0.0f); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Framework/TestContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using UnityEngine; 5 | 6 | namespace VamTimeline 7 | { 8 | public class TestContext 9 | { 10 | private const string _assertionFailedMessage = "Assertion failed"; 11 | 12 | public readonly GameObject gameObject; 13 | public readonly AtomAnimation animation; 14 | public readonly Logger logger; 15 | 16 | private readonly StringBuilder _output; 17 | 18 | public TestContext(GameObject gameObject, StringBuilder output, AtomAnimation animation) 19 | { 20 | this.gameObject = gameObject; 21 | logger = new Logger(null); 22 | this.animation = animation; 23 | this.animation.logger = logger; 24 | _output = output; 25 | } 26 | 27 | public bool Assert(bool truthy, string message) 28 | { 29 | if (!truthy) _output.AppendLine(message); 30 | return truthy; 31 | } 32 | 33 | public bool Assert(T? actual, T expected, string message = _assertionFailedMessage) where T : struct 34 | { 35 | if (actual.Equals(expected)) return true; 36 | _output.AppendLine(message); 37 | _output.AppendLine($"Expected '{expected}', received '{actual}'"); 38 | return false; 39 | } 40 | 41 | public bool Assert(T actual, T expected, string message = _assertionFailedMessage) where T : struct 42 | { 43 | if (actual.Equals(expected)) return true; 44 | _output.AppendLine(message); 45 | _output.AppendLine($"Expected '{expected}', received '{actual}'"); 46 | return false; 47 | } 48 | 49 | public bool Assert(string actual, string expected, string message = _assertionFailedMessage) 50 | { 51 | if (actual == expected) return true; 52 | _output.AppendLine(message); 53 | _output.AppendLine($"Expected '{expected}', received '{actual}'"); 54 | return false; 55 | } 56 | 57 | public bool AssertList(IEnumerable actual, IEnumerable expected, string message = _assertionFailedMessage) 58 | { 59 | if (actual == null) 60 | { 61 | _output.AppendLine(message); 62 | _output.AppendLine($"Expected '{expected}', received null"); 63 | return false; 64 | } 65 | var actualStr = string.Join(", ", actual.Select(v => v.ToString()).ToArray()); 66 | var expectedStr = string.Join(", ", expected.Select(v => v.ToString()).ToArray()); 67 | return Assert(actualStr, expectedStr, message); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/UI/Components/Scrubber/ScrubberMarkers.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class ScrubberMarkers : MaskableGraphic 7 | { 8 | public ScrubberStyle style; 9 | 10 | public float length; 11 | public float offset; 12 | 13 | protected override void OnPopulateMesh(VertexHelper vh) 14 | { 15 | vh.Clear(); 16 | if (length == 0 || style == null) return; 17 | var width = rectTransform.rect.width - style.Padding * 2; 18 | var pixelsPerSecond = width / length; 19 | float timespan; 20 | if (pixelsPerSecond < 20) 21 | timespan = 100f; 22 | else if (pixelsPerSecond < 2) 23 | timespan = 10f; 24 | else 25 | timespan = 1f; 26 | var height = rectTransform.rect.height; 27 | var yMin = -height / 2f; 28 | const float yMax = -2f; 29 | const float yMaxSmall = -8f; 30 | var timespan25 = timespan * 0.25f; 31 | var timespan50 = timespan * 0.50f; 32 | var timespan75 = timespan * 0.75f; 33 | 34 | var offsetX = -width / 2f; 35 | var ratio = width / length; 36 | 37 | for (var s = -offset; s <= length; s += timespan) 38 | { 39 | DrawLine(vh, yMin, yMax, offsetX, ratio, s, style.SecondsSize, style.SecondsColor); 40 | 41 | if (s >= length) break; 42 | DrawLine(vh, yMin, yMaxSmall, offsetX, ratio, s + timespan25, style.SecondFractionsSize, style.SecondFractionsColor); 43 | DrawLine(vh, yMin, yMaxSmall, offsetX, ratio, s + timespan50, style.SecondFractionsSize, style.SecondFractionsColor); 44 | DrawLine(vh, yMin, yMaxSmall, offsetX, ratio, s + timespan75, style.SecondFractionsSize, style.SecondFractionsColor); 45 | } 46 | 47 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(style.SecondsColor, 48 | new Vector2(offsetX, yMin), 49 | new Vector2(-offsetX, yMin), 50 | new Vector2(-offsetX, yMin + style.SecondsSize), 51 | new Vector2(offsetX, yMin + style.SecondsSize) 52 | )); 53 | } 54 | 55 | private static void DrawLine(VertexHelper vh, float yMin, float yMax, float offsetX, float ratio, float time, float size, Color color) 56 | { 57 | var xMin = offsetX + time * ratio - size / 2f; 58 | var xMax = xMin + size; 59 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(color, 60 | new Vector2(xMin, yMin), 61 | new Vector2(xMax, yMin), 62 | new Vector2(xMax, yMax), 63 | new Vector2(xMin, yMax) 64 | )); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/UI/Components/LineDrawer3D.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class LineDrawer3D : MonoBehaviour 6 | { 7 | private bool _dirty; 8 | private Material _material; 9 | private Gradient _colorGradient; 10 | private Vector3[] _points; 11 | private Vector3[] _vertices; 12 | private Color[] _colors; 13 | private int[] _indices; 14 | private Vector2[] _uv; 15 | private Vector3[] _normals; 16 | private int _previousIndicesCount = -1; 17 | 18 | public Material material { get { return _material; } set { _material = value; _dirty = true; } } 19 | public Gradient colorGradient { get { return _colorGradient; } set { _colorGradient = value; _dirty = true; } } 20 | public Vector3[] points { get { return _points; } set { _points = value; _dirty = true; } } 21 | 22 | private readonly Mesh _mesh; 23 | 24 | public LineDrawer3D() 25 | { 26 | _mesh = new Mesh(); 27 | } 28 | 29 | public void Recalculate() 30 | { 31 | var verticesCount = points.Length * 2; 32 | if (_previousIndicesCount != verticesCount) 33 | { 34 | _colors = new Color[verticesCount]; 35 | _vertices = new Vector3[verticesCount]; 36 | _indices = new int[verticesCount]; 37 | _uv = new Vector2[verticesCount]; 38 | _normals = new Vector3[verticesCount]; 39 | } 40 | var p = 0; 41 | for (var i = 0; i < verticesCount - 2; i += 2) 42 | { 43 | if (_previousIndicesCount != verticesCount) 44 | { 45 | _colors[i] = colorGradient.Evaluate(p / (float)points.Length); 46 | _colors[i + 1] = colorGradient.Evaluate((p + 1) / (float)points.Length); 47 | } 48 | _vertices[i] = points[p]; 49 | _vertices[i + 1] = points[p + 1]; 50 | p++; 51 | } 52 | for (var i = 0; i < verticesCount; i++) 53 | { 54 | _indices[i] = i; 55 | } 56 | _mesh.vertices = _vertices; 57 | _mesh.colors = _colors; 58 | _mesh.uv = _uv; 59 | _mesh.normals = _normals; 60 | _mesh.SetIndices(_indices, MeshTopology.Lines, 0); 61 | _mesh.RecalculateBounds(); 62 | _previousIndicesCount = verticesCount; 63 | } 64 | 65 | public void Update() 66 | { 67 | if (_dirty) 68 | { 69 | if (points != null) Recalculate(); 70 | _dirty = false; 71 | } 72 | Graphics.DrawMesh(_mesh, transform.parent.localToWorldMatrix, material, 0); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/Base/AnimationTargetBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine.Events; 3 | 4 | namespace VamTimeline 5 | { 6 | public abstract class AnimationTargetBase : IDisposable where TAnimatableRef : AnimatableRefBase 7 | { 8 | public AnimatableRefBase animatableRefBase => animatableRef; 9 | public TAnimatableRef animatableRef { get; } 10 | public UnityEvent onAnimationKeyframesDirty { get; } = new UnityEvent(); 11 | public UnityEvent onAnimationKeyframesRebuilt { get; } = new UnityEvent(); 12 | 13 | public IAtomAnimationClip clip { get; set; } 14 | 15 | private int _bulk; 16 | private bool _dirty = true; 17 | 18 | public bool dirty 19 | { 20 | get 21 | { 22 | return _dirty; 23 | } 24 | set 25 | { 26 | _dirty = value; 27 | if (value && _bulk == 0) 28 | onAnimationKeyframesDirty.Invoke(); 29 | } 30 | } 31 | 32 | public virtual bool selected 33 | { 34 | get 35 | { 36 | return animatableRef.selected; 37 | } 38 | set 39 | { 40 | if (animatableRef.selected == value) return; 41 | animatableRef.selected = value; 42 | animatableRef.onSelectedChanged.Invoke(); 43 | } 44 | } 45 | 46 | public bool collapsed 47 | { 48 | get 49 | { 50 | return animatableRef.collapsed; 51 | } 52 | set 53 | { 54 | animatableRef.collapsed = value; 55 | } 56 | } 57 | 58 | public string group { get; set; } 59 | 60 | public string name => animatableRef.name; 61 | public virtual string GetShortName() => animatableRef.GetShortName(); 62 | public virtual string GetFullName() => animatableRef.GetFullName(); 63 | 64 | protected AnimationTargetBase(TAnimatableRef animatableRef) 65 | { 66 | this.animatableRef = animatableRef; 67 | } 68 | 69 | public void StartBulkUpdates() 70 | { 71 | _bulk++; 72 | } 73 | public void EndBulkUpdates() 74 | { 75 | if (_bulk == 0) 76 | throw new InvalidOperationException("There is no bulk update in progress"); 77 | _bulk--; 78 | if (_bulk == 0 && dirty) 79 | dirty = true; 80 | } 81 | 82 | public virtual void SelectInVam() 83 | { 84 | } 85 | 86 | public virtual void Dispose() 87 | { 88 | onAnimationKeyframesDirty.RemoveAllListeners(); 89 | onAnimationKeyframesRebuilt.RemoveAllListeners(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/UI/Components/Zoom/ZoomControlGraphics.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class ZoomControlGraphics : MaskableGraphic 7 | { 8 | public ZoomStyle style; 9 | public AtomAnimationEditContext animationEditContext; 10 | public int mode; 11 | 12 | protected override void OnPopulateMesh(VertexHelper vh) 13 | { 14 | vh.Clear(); 15 | 16 | if(animationEditContext.current.animationLength == 0) return; 17 | 18 | var rect = rectTransform.rect; 19 | var padding = style.Padding; 20 | rect.xMin += padding; 21 | rect.xMax -= padding; 22 | var width = rect.width; 23 | 24 | var x1 = (animationEditContext.scrubberRange.rangeBegin / animationEditContext.current.animationLength) * width; 25 | var x2 = (animationEditContext.scrubberRange.rangeDuration / animationEditContext.current.animationLength) * width; 26 | 27 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(style.FullSectionColor, 28 | new Vector2(rect.xMin, rect.yMin), 29 | new Vector2(rect.xMax, rect.yMin), 30 | new Vector2(rect.xMax, rect.yMax), 31 | new Vector2(rect.xMin, rect.yMax) 32 | )); 33 | 34 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(mode == ZoomStateModes.MoveMode ? style.ZoomedSectionHighlightColor : style.ZoomedSectionColor, 35 | new Vector2(rect.xMin + x1, rect.yMin), 36 | new Vector2(rect.xMin + x1 + x2, rect.yMin), 37 | new Vector2(rect.xMin + x1 + x2, rect.yMax), 38 | new Vector2(rect.xMin + x1, rect.yMax) 39 | )); 40 | 41 | if (mode == ZoomStateModes.ResizeBeginMode) 42 | { 43 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(style.ZoomedSectionHighlightColor, 44 | new Vector2(rect.xMin + x1 - style.DragSideWidth, rect.yMin), 45 | new Vector2(rect.xMin + x1 + style.DragSideWidth, rect.yMin), 46 | new Vector2(rect.xMin + x1 + style.DragSideWidth, rect.yMax), 47 | new Vector2(rect.xMin + x1 - style.DragSideWidth, rect.yMax) 48 | )); 49 | } 50 | else if (mode == ZoomStateModes.ResizeEndMode) 51 | { 52 | vh.AddUIVertexQuad(UIVertexHelper.CreateVBO(style.ZoomedSectionHighlightColor, 53 | new Vector2(rect.xMin + x1 + x2 - style.DragSideWidth, rect.yMin), 54 | new Vector2(rect.xMin + x1 + x2 + style.DragSideWidth, rect.yMin), 55 | new Vector2(rect.xMin + x1 + x2 + style.DragSideWidth, rect.yMax), 56 | new Vector2(rect.xMin + x1 + x2 - style.DragSideWidth, rect.yMax) 57 | )); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/UI/Screens/DefaultsScreen.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace VamTimeline 5 | { 6 | public class DefaultsScreen : ScreenBase 7 | { 8 | public const string ScreenName = "Defaults"; 9 | 10 | public override string screenId => ScreenName; 11 | 12 | public override void Init(IAtomPlugin plugin, object arg) 13 | { 14 | base.Init(plugin, arg); 15 | 16 | CreateChangeScreenButton("< Back", MoreScreen.ScreenName); 17 | 18 | var createDefaultsBtn = prefabFactory.CreateButton("Save as default"); 19 | createDefaultsBtn.button.onClick.AddListener(() => 20 | { 21 | TimelineDefaults.singleton.Save(); 22 | SuperController.LogMessage($"Timeline: Settings saved to '{TimelineDefaults.DefaultsPath}'."); 23 | }); 24 | 25 | var deleteDefaultsBtn = prefabFactory.CreateButton("Delete current defaults"); 26 | deleteDefaultsBtn.button.onClick.AddListener(() => 27 | { 28 | TimelineDefaults.singleton.Delete(); 29 | SuperController.LogMessage("Timeline: Settings deleted. Reload to revert to defaults options."); 30 | }); 31 | 32 | prefabFactory.CreateHeader("Current", 2); 33 | { 34 | var sb = new StringBuilder(); 35 | TimelineDefaults.singleton.GetJSON().ToString("", sb); 36 | var currentDefaultsJSON = new JSONStorableString("Live", sb.ToString()); 37 | var currentDefaultsUI = prefabFactory.CreateTextField(currentDefaultsJSON); 38 | currentDefaultsUI.height = 400f; 39 | } 40 | 41 | prefabFactory.CreateHeader("On Disk", 2); 42 | { 43 | if (TimelineDefaults.singleton.Exists()) 44 | { 45 | var sb = new StringBuilder(); 46 | try 47 | { 48 | var json = SuperController.singleton.LoadJSON(TimelineDefaults.DefaultsPath); 49 | 50 | json.ToString("", sb); 51 | } 52 | catch (Exception exc) 53 | { 54 | sb.AppendLine($"An error occured while trying to open the file: {exc}"); 55 | } 56 | var currentDefaultsJSON = new JSONStorableString("OnDisk", sb.ToString()); 57 | var currentDefaultsUI = prefabFactory.CreateTextField(currentDefaultsJSON); 58 | currentDefaultsUI.height = 400f; 59 | } 60 | else 61 | { 62 | var currentDefaultsJSON = new JSONStorableString("OnDisk", "[FILE NOT FOUND]"); 63 | var currentDefaultsUI = prefabFactory.CreateTextField(currentDefaultsJSON); 64 | currentDefaultsUI.height = 60f; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/OperationsFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VamTimeline 4 | { 5 | public class OperationsFactory 6 | { 7 | private readonly Atom _containingAtom; 8 | private readonly AtomAnimation _animation; 9 | private readonly AtomAnimationClip _clip; 10 | private readonly PeerManager _peerManager; 11 | private readonly AtomAnimationSerializer _serializer; 12 | 13 | public OperationsFactory(Atom containingAtom, AtomAnimation animation, AtomAnimationClip clip, PeerManager peerManager, AtomAnimationSerializer serializer) 14 | { 15 | if (containingAtom == null) throw new ArgumentNullException(nameof(containingAtom)); 16 | _containingAtom = containingAtom; 17 | if (animation == null) throw new ArgumentNullException(nameof(animation)); 18 | _animation = animation; 19 | if (clip == null) throw new ArgumentNullException(nameof(clip)); 20 | _clip = clip; 21 | if (peerManager == null) throw new ArgumentNullException(nameof(peerManager)); 22 | _peerManager = peerManager; 23 | _serializer = serializer; 24 | } 25 | 26 | public ResizeAnimationOperations Resize() 27 | { 28 | return new ResizeAnimationOperations(); 29 | } 30 | 31 | public TargetsOperations Targets() 32 | { 33 | return new TargetsOperations(_containingAtom, _animation, _clip); 34 | } 35 | 36 | public KeyframesOperations Keyframes() 37 | { 38 | return new KeyframesOperations(_clip); 39 | } 40 | 41 | public LayersOperations Layers() 42 | { 43 | return new LayersOperations(_animation, _clip); 44 | } 45 | 46 | public SegmentsOperations Segments() 47 | { 48 | return new SegmentsOperations(_animation, _clip); 49 | } 50 | 51 | public ImportOperations Import() 52 | { 53 | return new ImportOperations(_animation); 54 | } 55 | 56 | public AddAnimationOperations AddAnimation() 57 | { 58 | return new AddAnimationOperations(_animation, _clip); 59 | } 60 | 61 | public OffsetOperations Offset() 62 | { 63 | return new OffsetOperations(_clip); 64 | } 65 | 66 | public MocapImportOperations MocapImport() 67 | { 68 | return new MocapImportOperations(_containingAtom, _animation, _clip); 69 | } 70 | 71 | public ReduceOperations Reduce(ReduceSettings settings) 72 | { 73 | return new ReduceOperations(_clip, settings); 74 | } 75 | 76 | public RecordOperations Record() 77 | { 78 | return new RecordOperations(_animation, _clip, _peerManager); 79 | } 80 | 81 | public SilentImportOperations SilentImport() 82 | { 83 | return new SilentImportOperations(_containingAtom, _animation, _serializer); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/UI/Screens/ScreenBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.Events; 4 | 5 | namespace VamTimeline 6 | { 7 | public abstract class ScreenBase : MonoBehaviour 8 | { 9 | public class ScreenChangeRequestEventArgs { public string screenName; public object screenArg; } 10 | public class ScreenChangeRequestedEvent : UnityEvent { } 11 | 12 | protected static readonly Color NavButtonColor = new Color(0.8f, 0.7f, 0.8f); 13 | 14 | public readonly ScreenChangeRequestedEvent onScreenChangeRequested = new ScreenChangeRequestedEvent(); 15 | public readonly UnityEvent onScreenReloadRequested = new UnityEvent(); 16 | public Transform popupParent; 17 | public abstract string screenId { get; } 18 | 19 | protected AtomAnimation animation => plugin.animation; 20 | protected AtomAnimationEditContext animationEditContext => plugin.animationEditContext; 21 | protected AtomAnimationClip current => animationEditContext.current; 22 | protected IList currentLayer => animationEditContext.currentLayer; 23 | protected AtomAnimationsClipsIndex.IndexedSegment currentSegment => animationEditContext.currentSegment; 24 | protected OperationsFactory operations => plugin.operations; 25 | 26 | protected IAtomPlugin plugin; 27 | protected VamPrefabFactory prefabFactory; 28 | protected bool disposing; 29 | 30 | public virtual void Init(IAtomPlugin plugin, object arg) 31 | { 32 | this.plugin = plugin; 33 | prefabFactory = gameObject.AddComponent(); 34 | prefabFactory.plugin = plugin; 35 | plugin.animationEditContext.onCurrentAnimationChanged.AddListener(OnCurrentAnimationChanged); 36 | } 37 | 38 | protected virtual void OnCurrentAnimationChanged(AtomAnimationEditContext.CurrentAnimationChangedEventArgs args) 39 | { 40 | } 41 | 42 | protected UIDynamicButton CreateChangeScreenButton(string label, string screenName) 43 | { 44 | var ui = prefabFactory.CreateButton(label); 45 | ui.button.onClick.AddListener(() => ChangeScreen(screenName)); 46 | ui.buttonColor = NavButtonColor; 47 | return ui; 48 | } 49 | 50 | public void ReloadScreen() 51 | { 52 | onScreenReloadRequested.Invoke(); 53 | } 54 | 55 | public void ChangeScreen(string screenName, object screenArg = null) 56 | { 57 | onScreenChangeRequested.Invoke(new ScreenChangeRequestEventArgs { screenName = screenName, screenArg = screenArg }); 58 | } 59 | 60 | public virtual void OnDestroy() 61 | { 62 | prefabFactory.ClearConfirm(); 63 | disposing = true; 64 | onScreenChangeRequested.RemoveAllListeners(); 65 | onScreenReloadRequested.RemoveAllListeners(); 66 | plugin.animationEditContext.onCurrentAnimationChanged.RemoveListener(OnCurrentAnimationChanged); 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/AtomAnimations/Clipboard/AtomClipboardEntry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using SimpleJSON; 3 | using UnityEngine; 4 | 5 | namespace VamTimeline 6 | { 7 | public class AtomClipboard 8 | { 9 | public float time; 10 | public readonly IList entries = new List(); 11 | 12 | public void Clear() 13 | { 14 | time = 0f; 15 | entries.Clear(); 16 | } 17 | } 18 | 19 | public class AtomClipboardEntry 20 | { 21 | public float time; 22 | public List controllers; 23 | public List floatParams; 24 | public List triggers; 25 | 26 | public bool empty => 27 | controllers.Count == 0 && 28 | floatParams.Count == 0 && 29 | triggers.Count == 0; 30 | } 31 | 32 | public interface IClipboardEntry 33 | where TRef : AnimatableRefBase 34 | where TSnapshot : ISnapshot 35 | { 36 | TRef animatableRef { get; } 37 | TSnapshot snapshot { get; } 38 | } 39 | 40 | public class FloatParamValClipboardEntry : IClipboardEntry 41 | { 42 | public JSONStorableFloatRef animatableRef { get; set; } 43 | 44 | public FloatParamTargetSnapshot snapshot { get; set; } 45 | } 46 | 47 | public class FreeControllerV3ClipboardEntry : IClipboardEntry 48 | { 49 | public FreeControllerV3Ref animatableRef { get; set; } 50 | 51 | public TransformTargetSnapshot snapshot { get; set; } 52 | } 53 | 54 | public class TriggersClipboardEntry : IClipboardEntry 55 | { 56 | public TriggersTrackRef animatableRef { get; set; } 57 | 58 | public TriggerTargetSnapshot snapshot { get; set; } 59 | } 60 | 61 | public interface ISnapshot 62 | { 63 | } 64 | 65 | public class TransformTargetSnapshot : ISnapshot 66 | { 67 | public Vector3TargetSnapshot position; 68 | public QuaternionTargetSnapshot rotation; 69 | } 70 | 71 | public class Vector3TargetSnapshot : ISnapshot 72 | { 73 | public BezierKeyframe x; 74 | public BezierKeyframe y; 75 | public BezierKeyframe z; 76 | 77 | public Vector3 AsVector3() => new Vector3(x.value, y.value, z.value); 78 | } 79 | 80 | public class QuaternionTargetSnapshot : ISnapshot 81 | { 82 | public BezierKeyframe rotX; 83 | public BezierKeyframe rotY; 84 | public BezierKeyframe rotZ; 85 | public BezierKeyframe rotW; 86 | 87 | public Quaternion AsQuaternion() => new Quaternion(rotX.value, rotY.value, rotZ.value, rotW.value); 88 | } 89 | 90 | public class FloatParamTargetSnapshot : ISnapshot 91 | { 92 | public BezierKeyframe value; 93 | } 94 | 95 | public class TriggerTargetSnapshot : ISnapshot 96 | { 97 | public JSONClass json; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/UI/Screens/AddScreenBase.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public abstract class AddScreenBase : ScreenBase 4 | { 5 | private static bool _previousCreateInOtherAtoms; 6 | private static bool _previousAddAnother; 7 | 8 | protected JSONStorableString clipNameJSON; 9 | protected JSONStorableString layerNameJSON; 10 | protected JSONStorableString segmentNameJSON; 11 | protected JSONStorableBool createInOtherAtomsJSON; 12 | protected JSONStorableBool addAnotherJSON; 13 | protected JSONStorableStringChooser createPositionJSON; 14 | protected UIDynamicTextField clipNameUI; 15 | 16 | #region Init 17 | 18 | public override void Init(IAtomPlugin plugin, object arg) 19 | { 20 | base.Init(plugin, arg); 21 | 22 | // Right side 23 | 24 | CreateChangeScreenButton($"< Back to {AddAnimationsScreen.ScreenName}", AddAnimationsScreen.ScreenName); 25 | } 26 | 27 | protected void InitNewClipNameUI() 28 | { 29 | clipNameJSON = new JSONStorableString("New animation name", "", (string _) => OptionsUpdated()); 30 | clipNameUI = prefabFactory.CreateTextInput(clipNameJSON); 31 | } 32 | 33 | protected void InitNewLayerNameUI(string label = "New layer name") 34 | { 35 | layerNameJSON = new JSONStorableString(label, "", (string _) => OptionsUpdated()); 36 | prefabFactory.CreateTextInput(layerNameJSON); 37 | } 38 | 39 | protected void InitNewSegmentNameUI(string label = "New segment name") 40 | { 41 | segmentNameJSON = new JSONStorableString(label, "", (string _) => OptionsUpdated()); 42 | prefabFactory.CreateTextInput(segmentNameJSON); 43 | } 44 | 45 | protected void InitCreateInOtherAtomsUI(string label = "Create in other atoms") 46 | { 47 | createInOtherAtomsJSON = new JSONStorableBool(label, _previousCreateInOtherAtoms, val => _previousCreateInOtherAtoms = val); 48 | prefabFactory.CreateToggle(createInOtherAtomsJSON); 49 | } 50 | 51 | protected void InitAddAnotherUI() 52 | { 53 | addAnotherJSON = new JSONStorableBool("Stay in this screen", _previousAddAnother, val => _previousAddAnother = val); 54 | prefabFactory.CreateToggle(addAnotherJSON); 55 | } 56 | 57 | protected void InitNewPositionUI() 58 | { 59 | createPositionJSON = new JSONStorableStringChooser( 60 | "Add at position", 61 | AddAnimationOperations.Positions.all, 62 | AddAnimationOperations.Positions.PositionNext, 63 | "Add at position"); 64 | prefabFactory.CreatePopup(createPositionJSON, false, true); 65 | } 66 | 67 | #endregion 68 | 69 | protected override void OnCurrentAnimationChanged(AtomAnimationEditContext.CurrentAnimationChangedEventArgs args) 70 | { 71 | base.OnCurrentAnimationChanged(args); 72 | 73 | RefreshUI(); 74 | } 75 | 76 | protected virtual void RefreshUI() 77 | { 78 | OptionsUpdated(); 79 | } 80 | 81 | protected abstract void OptionsUpdated(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Plugin/TestPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics; 3 | using System.Text; 4 | 5 | namespace VamTimeline 6 | { 7 | public class TestPlugin : MVRScript 8 | { 9 | private StringBuilder _resultBuilder; 10 | private JSONStorableString _testFilterJSON; 11 | private JSONStorableString _resultJSON; 12 | private UIDynamicButton _runUI; 13 | 14 | public override void Init() 15 | { 16 | base.Init(); 17 | 18 | _testFilterJSON = new JSONStorableString("Test Filter", "CanImport_FullMismatch_ExistingSegment"); 19 | _resultJSON = new JSONStorableString("Test Results", "Running..."); 20 | 21 | _runUI = CreateButton("Run"); 22 | _runUI.button.onClick.AddListener(Run); 23 | 24 | Run(); 25 | } 26 | 27 | public override void InitUI() 28 | { 29 | base.InitUI(); 30 | if (UITransform == null) return; 31 | var scriptUI = UITransform.GetComponentInChildren(); 32 | 33 | _runUI.transform.SetParent(scriptUI.fullWidthUIContent.transform, false); 34 | 35 | var resultUI = CreateTextField(_resultJSON, true); 36 | resultUI.height = 800f; 37 | resultUI.transform.SetParent(scriptUI.fullWidthUIContent.transform, false); 38 | } 39 | 40 | private void Run() 41 | { 42 | _runUI.enabled = false; 43 | _resultBuilder = new StringBuilder(); 44 | _resultJSON.val = "Running..."; 45 | pluginLabelJSON.val = "Running..."; 46 | StartCoroutine(RunDeferred()); 47 | } 48 | 49 | private IEnumerator RunDeferred() 50 | { 51 | var globalSW = Stopwatch.StartNew(); 52 | var success = true; 53 | var counter = 0; 54 | yield return 0; 55 | foreach (Test test in TestsIndex.GetAllTests()) 56 | { 57 | if (!string.IsNullOrEmpty(_testFilterJSON.val) && !test.name.Contains(_testFilterJSON.val)) continue; 58 | 59 | var output = new StringBuilder(); 60 | var sw = Stopwatch.StartNew(); 61 | foreach (var x in test.Run(this, output)) 62 | { 63 | yield return x; 64 | } 65 | 66 | counter++; 67 | if (output.Length == 0) 68 | { 69 | if (sw.ElapsedMilliseconds > 20) 70 | { 71 | _resultBuilder.AppendLine($"{test.name} PASS {sw.ElapsedMilliseconds:0}ms (LONG)"); 72 | _resultJSON.val = _resultBuilder.ToString(); 73 | } 74 | } 75 | else 76 | { 77 | 78 | _resultBuilder.AppendLine($"{test.name} FAIL {sw.ElapsedMilliseconds:0}ms]"); 79 | _resultBuilder.AppendLine(output.ToString()); 80 | _resultJSON.val = _resultBuilder.ToString(); 81 | _resultBuilder.AppendLine($"FAIL [{globalSW.Elapsed}]"); 82 | success = false; 83 | break; 84 | } 85 | } 86 | globalSW.Stop(); 87 | _resultBuilder.AppendLine($"{(success ? "SUCCESS" : "FAIL")}; ran {counter} tests in {globalSW.Elapsed}"); 88 | _resultJSON.val = _resultBuilder.ToString(); 89 | pluginLabelJSON.val = (success ? "Success" : "Failed") + $" (ran in {globalSW.Elapsed.TotalSeconds:0.00}s)"; 90 | } 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/FloatParamTargetReduceProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class FloatParamTargetReduceProcessor : TargetReduceProcessorBase, ITargetReduceProcessor 6 | { 7 | ICurveAnimationTarget ITargetReduceProcessor.target => source; 8 | 9 | public FloatParamTargetReduceProcessor(JSONStorableFloatAnimationTarget source, ReduceSettings settings) 10 | : base(source, settings) 11 | { 12 | } 13 | 14 | 15 | public void CopyToBranch(int sourceKey, int curveType = CurveTypeValues.Undefined, float time = -1) 16 | { 17 | var sourceFrame = source.value.keys[sourceKey]; 18 | if (time < -Mathf.Epsilon) 19 | time = sourceFrame.time; 20 | var branchKey = branch.value.SetKeyframe(time, sourceFrame.value, CurveTypeValues.SmoothLocal); 21 | if(curveType != CurveTypeValues.Undefined) 22 | branch.ChangeCurveByTime(branchKey, curveType); 23 | branch.value.RecomputeKey(branchKey); 24 | } 25 | 26 | public void AverageToBranch(float keyTime, int fromKey, int toKey) 27 | { 28 | var value = 0f; 29 | var duration = source.value.GetKeyframeByKey(toKey).time - source.value.GetKeyframeByKey(fromKey).time; 30 | for (var key = fromKey; key < toKey; key++) 31 | { 32 | var frameDuration = source.value.GetKeyframeByKey(key + 1).time - source.value.GetKeyframeByKey(key).time; 33 | var weight = frameDuration / duration; 34 | value += source.value.GetKeyframeByKey(key).value * weight; 35 | } 36 | 37 | branch.SetKeyframe(keyTime, value, false); 38 | } 39 | 40 | public void FlattenToBranch(int sectionStart, int sectionEnd) 41 | { 42 | var avg = 0f; 43 | var div = 0f; 44 | for (var i = sectionStart; i <= sectionEnd; i++) 45 | { 46 | avg += source.value.GetKeyframeByKey(i).value; 47 | div += 1f; 48 | } 49 | avg /= div; 50 | 51 | var branchStart = branch.value.SetKeyframe(source.value.GetKeyframeByKey(sectionStart).time, avg, CurveTypeValues.FlatLinear); 52 | var branchEnd = branch.value.SetKeyframe(source.value.GetKeyframeByKey(sectionEnd).time, avg, CurveTypeValues.LinearFlat); 53 | branch.value.RecomputeKey(branchStart); 54 | branch.value.RecomputeKey(branchEnd); 55 | } 56 | 57 | public bool IsStable(int key1, int key2) 58 | { 59 | if (settings.minMeaningfulFloatParamRangeRatio <= 0) return false; 60 | var value1 = source.value.GetKeyframeByKey(key1).value; 61 | var value2 = source.value.GetKeyframeByKey(key2).value; 62 | return Mathf.Abs(value2 - value1) / (source.animatableRef.floatParam.max - source.animatableRef.floatParam.min) < (settings.minMeaningfulFloatParamRangeRatio / 10f); 63 | } 64 | 65 | public override float GetComparableNormalizedValue(int key) 66 | { 67 | var time = source.value.keys[key].time; 68 | // TODO: Normalize the delta values based on range 69 | float delta; 70 | if (settings.minMeaningfulFloatParamRangeRatio > 0) 71 | delta = Mathf.Abs( 72 | branch.value.Evaluate(time) - 73 | source.value.Evaluate(time) 74 | ) / (source.animatableRef.floatParam.max - source.animatableRef.floatParam.min) / settings.minMeaningfulFloatParamRangeRatio; 75 | else 76 | delta = 1f; 77 | return delta; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Interop/SyncProxy.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace VamTimeline 6 | { 7 | public class SyncProxy : IDisposable 8 | { 9 | public static SyncProxy Wrap(Dictionary dict) 10 | { 11 | return new SyncProxy(dict); 12 | } 13 | 14 | public readonly Dictionary dict; 15 | 16 | public bool connected 17 | { 18 | get { return Get(nameof(connected)); } 19 | set { Set(nameof(connected), value); } 20 | } 21 | 22 | public MVRScript storable 23 | { 24 | get { return Get(nameof(storable)); } 25 | set { Set(nameof(storable), value); } 26 | } 27 | 28 | public string label 29 | { 30 | get 31 | { 32 | var s = storable; 33 | var customLabel = s.pluginLabelJSON.val; 34 | return !string.IsNullOrEmpty(customLabel) 35 | ? $"{s.containingAtom.name}: {customLabel}" 36 | : s.containingAtom.name; 37 | } 38 | } 39 | 40 | // TODO: Instead, get from storable and cache 41 | public JSONStorableStringChooser animation 42 | { 43 | get { return Get(nameof(animation)); } 44 | set { Set(nameof(animation), value); } 45 | } 46 | 47 | public JSONStorableFloat time 48 | { 49 | get { return Get(nameof(time)); } 50 | set { Set(nameof(time), value); } 51 | } 52 | 53 | public JSONStorableBool isPlaying 54 | { 55 | get { return Get(nameof(isPlaying)); } 56 | set { Set(nameof(isPlaying), value); } 57 | } 58 | 59 | public JSONStorableAction play 60 | { 61 | get { return Get(nameof(play)); } 62 | set { Set(nameof(play), value); } 63 | } 64 | 65 | public JSONStorableAction playIfNotPlaying 66 | { 67 | get { return Get(nameof(playIfNotPlaying)); } 68 | set { Set(nameof(playIfNotPlaying), value); } 69 | } 70 | 71 | public JSONStorableAction stop 72 | { 73 | get { return Get(nameof(stop)); } 74 | set { Set(nameof(stop), value); } 75 | } 76 | 77 | public JSONStorableAction stopAndReset 78 | { 79 | get { return Get(nameof(stopAndReset)); } 80 | set { Set(nameof(stopAndReset), value); } 81 | } 82 | 83 | public JSONStorableAction nextFrame 84 | { 85 | get { return Get(nameof(nextFrame)); } 86 | set { Set(nameof(nextFrame), value); } 87 | } 88 | 89 | public JSONStorableAction previousFrame 90 | { 91 | get { return Get(nameof(previousFrame)); } 92 | set { Set(nameof(previousFrame), value); } 93 | } 94 | 95 | private SyncProxy(Dictionary dict) 96 | { 97 | this.dict = dict; 98 | } 99 | 100 | public SyncProxy() 101 | : this(new Dictionary()) 102 | { 103 | } 104 | 105 | private T Get(string key) 106 | { 107 | object val; 108 | return dict.TryGetValue(key, out val) ? (T)val : default(T); 109 | } 110 | 111 | private void Set(string key, T value) 112 | { 113 | dict[key] = value; 114 | } 115 | 116 | public void Dispose() 117 | { 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/UI/Components/MiniButton.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class MiniButtonStyle : StyleBase 7 | { 8 | public static MiniButtonStyle Default() 9 | { 10 | return new MiniButtonStyle(); 11 | } 12 | 13 | public Color LabelBackgroundColorTop { get; } = new Color(0.924f, 0.920f, 0.920f); 14 | public Color LabelBackgroundColorBottom { get; } = new Color(0.724f, 0.720f, 0.720f); 15 | public Color LabelBackgroundColorTopSelected { get; } = new Color(0.924f, 0.920f, 0.920f); 16 | public Color LabelBackgroundColorBottomSelected { get; } = new Color(1, 1, 1); 17 | } 18 | 19 | public class MiniButton : MonoBehaviour 20 | { 21 | private static readonly MiniButtonStyle _style = MiniButtonStyle.Default(); 22 | 23 | public static MiniButton Create(GameObject parent, string label) 24 | { 25 | var go = new GameObject("MiniButton"); 26 | go.transform.SetParent(parent.transform, false); 27 | 28 | var rect = go.AddComponent(); 29 | 30 | var miniButton = go.AddComponent(); 31 | miniButton.rectTransform = rect; 32 | 33 | miniButton.text.text = label; 34 | 35 | return miniButton; 36 | } 37 | 38 | public MiniButton() 39 | { 40 | CreateBackground(); 41 | CreateLabel(); 42 | } 43 | 44 | private void CreateBackground() 45 | { 46 | var child = new GameObject(); 47 | child.transform.SetParent(transform, false); 48 | 49 | var rect = child.AddComponent(); 50 | rect.StretchParent(); 51 | 52 | image = child.AddComponent(); 53 | image.top = _style.LabelBackgroundColorTop; 54 | image.bottom = _style.LabelBackgroundColorBottom; 55 | image.raycastTarget = true; 56 | 57 | clickable = child.AddComponent(); 58 | } 59 | 60 | private void CreateLabel() 61 | { 62 | var child = new GameObject(); 63 | child.transform.SetParent(transform, false); 64 | 65 | var rect = child.AddComponent(); 66 | rect.StretchParent(); 67 | 68 | text = child.AddComponent(); 69 | text.text = "Button"; 70 | text.font = _style.Font; 71 | text.fontSize = 20; 72 | text.color = _style.FontColor; 73 | text.alignment = TextAnchor.MiddleCenter; 74 | text.horizontalOverflow = HorizontalWrapMode.Wrap; 75 | text.resizeTextForBestFit = false; // Better but ugly if true 76 | text.raycastTarget = false; 77 | } 78 | 79 | public RectTransform rectTransform { get; set; } 80 | public Clickable clickable { get; set; } 81 | public GradientImage image { get; set; } 82 | public Text text; 83 | 84 | private bool _selected; 85 | 86 | public bool selected 87 | { 88 | get 89 | { 90 | return _selected; 91 | } 92 | set 93 | { 94 | if(_selected == value) return; 95 | _selected = value; 96 | SyncSelected(); 97 | } 98 | } 99 | 100 | private void SyncSelected() 101 | { 102 | if (_selected) 103 | { 104 | image.top = _style.LabelBackgroundColorTopSelected; 105 | image.bottom = _style.LabelBackgroundColorBottomSelected; 106 | } 107 | else 108 | { 109 | image.top = _style.LabelBackgroundColorTop; 110 | image.bottom = _style.LabelBackgroundColorBottom; 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/SimpleTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | using Object = UnityEngine.Object; 5 | 6 | namespace VamTimeline 7 | { 8 | public class SimpleTrigger : TriggerHandler 9 | { 10 | private readonly string _startName; 11 | private readonly string _stopName; 12 | public Trigger trigger { get; } 13 | 14 | public SimpleTrigger(string startName, string stopName) 15 | { 16 | _startName = startName; 17 | _stopName = stopName; 18 | 19 | trigger = new Trigger 20 | { 21 | handler = this 22 | }; 23 | SuperController.singleton.onAtomUIDRenameHandlers += OnAtomRename; 24 | } 25 | 26 | public void RemoveTrigger(Trigger _) 27 | { 28 | } 29 | 30 | public void DuplicateTrigger(Trigger _) 31 | { 32 | } 33 | 34 | public RectTransform CreateTriggerActionsUI() 35 | { 36 | var rt = Object.Instantiate(VamPrefabFactory.triggerActionsPrefab); 37 | 38 | var content = rt.Find("Content"); 39 | var transitionTab = content.Find("Tab2"); 40 | transitionTab.parent = null; 41 | Object.Destroy(transitionTab); 42 | var startTab = content.Find("Tab1"); 43 | startTab.GetComponentInChildren().text = _startName; 44 | var endTab = content.Find("Tab3"); 45 | if (_stopName != null) 46 | { 47 | var endTabRect = endTab.GetComponent(); 48 | endTabRect.offsetMin = new Vector2(264, endTabRect.offsetMin.y); 49 | endTabRect.offsetMax = new Vector2(560, endTabRect.offsetMax.y); 50 | endTab.GetComponentInChildren().text = _stopName; 51 | } 52 | else 53 | { 54 | endTab.gameObject.SetActive(false); 55 | } 56 | 57 | return rt; 58 | } 59 | 60 | public RectTransform CreateTriggerActionMiniUI() 61 | { 62 | var rt = Object.Instantiate(VamPrefabFactory.triggerActionMiniPrefab); 63 | return rt; 64 | } 65 | 66 | public RectTransform CreateTriggerActionDiscreteUI() 67 | { 68 | var rt = Object.Instantiate(VamPrefabFactory.triggerActionDiscretePrefab); 69 | return rt; 70 | } 71 | 72 | public RectTransform CreateTriggerActionTransitionUI() 73 | { 74 | return null; 75 | } 76 | 77 | public void RemoveTriggerActionUI(RectTransform rt) 78 | { 79 | if (rt != null) Object.Destroy(rt.gameObject); 80 | } 81 | 82 | private void OnAtomRename(string oldname, string newname) 83 | { 84 | trigger.SyncAtomNames(); 85 | } 86 | 87 | public void Dispose() 88 | { 89 | SuperController.singleton.onAtomUIDRenameHandlers -= OnAtomRename; 90 | } 91 | 92 | public void SetActive(bool active) 93 | { 94 | try 95 | { 96 | trigger.active = active; 97 | } 98 | catch (Exception exc) 99 | { 100 | SuperController.LogError($"Timeline: Error while activating global trigger: {exc}"); 101 | } 102 | } 103 | 104 | public void Trigger() 105 | { 106 | try 107 | { 108 | trigger.active = true; 109 | trigger.active = false; 110 | } 111 | catch (Exception exc) 112 | { 113 | SuperController.LogError($"Timeline: Error while activating global trigger: {exc}"); 114 | } 115 | } 116 | 117 | public void Update() 118 | { 119 | trigger.Update(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/UI/Screens/AddSharedSegmentScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace VamTimeline 5 | { 6 | public class AddSharedSegmentScreen : AddScreenBase 7 | { 8 | public const string ScreenName = "Add shared segment"; 9 | 10 | public override string screenId => ScreenName; 11 | 12 | private UIDynamicButton _createSharedSegmentUI; 13 | 14 | #region Init 15 | 16 | public override void Init(IAtomPlugin plugin, object arg) 17 | { 18 | base.Init(plugin, arg); 19 | 20 | prefabFactory.CreateSpacer(); 21 | prefabFactory.CreateHeader("Create", 1); 22 | 23 | InitNewClipNameUI(); 24 | InitNewLayerNameUI(); 25 | InitCreateSharedSegmentUI(); 26 | InitSendLayerToSharedSegmentUI(); 27 | 28 | RefreshUI(); 29 | } 30 | 31 | public void InitCreateSharedSegmentUI() 32 | { 33 | _createSharedSegmentUI = prefabFactory.CreateButton("Create shared segment"); 34 | _createSharedSegmentUI.button.onClick.AddListener(AddSharedSegment); 35 | } 36 | 37 | public void InitSendLayerToSharedSegmentUI() 38 | { 39 | var sendLayerToSharedUI = prefabFactory.CreateButton("Send layer to shared segment"); 40 | sendLayerToSharedUI.button.onClick.AddListener(SendLayerToShared); 41 | } 42 | 43 | #endregion 44 | 45 | #region Callbacks 46 | 47 | private void AddSharedSegment() 48 | { 49 | if (animation.index.segmentNames.Contains(AtomAnimationClip.SharedAnimationSegment)) 50 | { 51 | animationEditContext.SelectAnimation(animation.clips.First(c => c.animationSegment == AtomAnimationClip.SharedAnimationSegment)); 52 | return; 53 | } 54 | 55 | var clip = operations.Segments().AddShared(clipNameJSON.val); 56 | animationEditContext.SelectAnimation(clip); 57 | ChangeScreen(EditAnimationScreen.ScreenName); 58 | } 59 | 60 | public void SendLayerToShared() 61 | { 62 | if (current.animationSegment == AtomAnimationClip.SharedAnimationSegment) 63 | return; 64 | 65 | var currentTargets = new HashSet(currentLayer.SelectMany(c => c.GetAllTargets())); 66 | var reservedTargets = new HashSet(animation.clips.Where(c => c.animationSegment != current.animationSegment).SelectMany(c => c.GetAllTargets())); 67 | if (currentTargets.Any(t => reservedTargets.Any(t.TargetsSameAs))) 68 | { 69 | SuperController.LogError("Timeline: Cannot send current layer to the shared segment because some targets exists in the shared segment already or in another segment."); 70 | return; 71 | } 72 | 73 | foreach (var clip in currentLayer) 74 | { 75 | clip.animationSegment = AtomAnimationClip.SharedAnimationSegment; 76 | clip.animationLayer = layerNameJSON.val; 77 | } 78 | } 79 | 80 | #endregion 81 | 82 | protected override void RefreshUI() 83 | { 84 | base.RefreshUI(); 85 | 86 | clipNameJSON.val = animation.GetUniqueAnimationName(AtomAnimationClip.SharedAnimationSegmentId, "Shared 1"); 87 | layerNameJSON.val = AtomAnimationClip.DefaultAnimationLayer; 88 | } 89 | 90 | protected override void OptionsUpdated() 91 | { 92 | _createSharedSegmentUI.button.interactable = 93 | !animation.index.segmentNames.Contains(AtomAnimationClip.SharedAnimationSegment) && 94 | !string.IsNullOrEmpty(clipNameJSON.val) && 95 | animation.clips.All(c => c.animationName != clipNameJSON.val) && 96 | !string.IsNullOrEmpty(layerNameJSON.val); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/SilentImportOperations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using SimpleJSON; 4 | 5 | namespace VamTimeline 6 | { 7 | public class SilentImportOperations 8 | { 9 | private readonly Atom _containingAtom; 10 | private readonly AtomAnimation _animation; 11 | private readonly AtomAnimationSerializer _serializer; 12 | 13 | public SilentImportOperations(Atom containingAtom, AtomAnimation animation, AtomAnimationSerializer serializer) 14 | { 15 | _containingAtom = containingAtom; 16 | _animation = animation; 17 | _serializer = serializer; 18 | } 19 | 20 | public void Perform(JSONClass json, string conflictMode) 21 | { 22 | try 23 | { 24 | if (json["AtomType"]?.Value != _containingAtom.type) 25 | { 26 | SuperController.LogError($"Timeline: Loaded animation for {json["AtomType"]} but current atom type is {_containingAtom.type}"); 27 | return; 28 | } 29 | 30 | var serializationVersion = json.HasKey("SerializeVersion") ? json["SerializeVersion"].AsInt : 0; 31 | var clipsJSON = json["Clips"].AsArray; 32 | if (clipsJSON == null || clipsJSON.Count == 0) 33 | { 34 | SuperController.LogError("Timeline: No animations were found in the provided JSON."); 35 | return; 36 | } 37 | 38 | _animation.index.StartBulkUpdates(); 39 | try 40 | { 41 | foreach (JSONClass clipJSON in clipsJSON) 42 | { 43 | var importedClip = _serializer.DeserializeClip(clipJSON, _animation.animatables, _animation.logger, serializationVersion); 44 | 45 | var existingClip = _animation.GetClip(importedClip.animationSegment, importedClip.animationLayer, importedClip.animationName); 46 | 47 | if (existingClip != null) 48 | { 49 | if (conflictMode == AtomPlugin.SilentImportConflictModes.Overwrite) 50 | { 51 | _animation.RemoveClip(existingClip); 52 | _animation.AddClip(importedClip); 53 | } 54 | else if (conflictMode == AtomPlugin.SilentImportConflictModes.Rename) 55 | { 56 | importedClip.animationName = _animation.GetUniqueAnimationNameInLayer(existingClip); 57 | _animation.AddClip(importedClip); 58 | } 59 | else // Skip is the default 60 | { 61 | // Do nothing 62 | } 63 | } 64 | else 65 | { 66 | _animation.AddClip(importedClip); 67 | } 68 | } 69 | 70 | if (_animation.clips.Count == 1 && _animation.clips[0].IsEmpty()) 71 | _animation.RemoveClip(_animation.clips[0]); 72 | 73 | if (_animation.clips.Count == 0 || _animation.clips.All(c => c.animationSegmentId == AtomAnimationClip.SharedAnimationSegmentId)) 74 | _animation.CreateClip(AtomAnimationClip.DefaultAnimationName, AtomAnimationClip.DefaultAnimationLayer, AtomAnimationClip.DefaultAnimationSegment); 75 | } 76 | finally 77 | { 78 | _animation.index.EndBulkUpdates(); 79 | } 80 | 81 | _serializer.RestoreMissingTriggers(_animation); 82 | _animation.index.Rebuild(); 83 | _animation.onClipsListChanged.Invoke(); 84 | } 85 | catch (Exception exc) 86 | { 87 | SuperController.LogError($"Timeline: Silent import failed. {exc}"); 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/AtomAnimations/Editing/TimelineDefaults.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MVR.FileManagementSecure; 3 | using SimpleJSON; 4 | 5 | namespace VamTimeline 6 | { 7 | public class TimelineDefaults 8 | { 9 | private const string _timelineDirectory = "Saves\\PluginData\\Timeline"; 10 | public const string DefaultsPath = "Saves\\PluginData\\Timeline\\settings.json"; 11 | 12 | public static readonly TimelineDefaults singleton = new TimelineDefaults(); 13 | 14 | public bool Exists() 15 | { 16 | return FileManagerSecure.FileExists(DefaultsPath); 17 | } 18 | 19 | public void Save() 20 | { 21 | FileManagerSecure.CreateDirectory(_timelineDirectory); 22 | SuperController.singleton.SaveJSON(GetJSON(), DefaultsPath); 23 | } 24 | 25 | public JSONClass GetJSON() 26 | { 27 | var json = new JSONClass(); 28 | var screens = new JSONClass(); 29 | json["Screens"] = screens; 30 | 31 | screens[RecordScreen.ScreenName] = GetJSON(RecordScreenSettings.singleton); 32 | screens[ReduceScreen.ScreenName] = GetJSON(ReduceScreenSettings.singleton); 33 | 34 | return json; 35 | } 36 | 37 | private static JSONClass GetJSON(TimelineSettings settings) 38 | { 39 | var obj = new JSONClass(); 40 | settings.Save(obj); 41 | return obj; 42 | } 43 | 44 | public void Load() 45 | { 46 | if (!Exists()) return; 47 | var json = SuperController.singleton.LoadJSON(DefaultsPath).AsObject; 48 | if (json == null) return; 49 | 50 | if (json.HasKey("Screens")) 51 | { 52 | var screens = json["Screens"]; 53 | Load(RecordScreenSettings.singleton, screens[RecordScreen.ScreenName]); 54 | Load(ReduceScreenSettings.singleton, screens[ReduceScreen.ScreenName]); 55 | } 56 | } 57 | 58 | private static void Load(TimelineSettings settings, JSONNode node) 59 | { 60 | var obj = node.AsObject; 61 | if (obj == null) return; 62 | settings.Load(obj); 63 | } 64 | 65 | public void Delete() 66 | { 67 | FileManagerSecure.DeleteFile(DefaultsPath); 68 | } 69 | } 70 | 71 | public abstract class TimelineSettings 72 | { 73 | public abstract void Load(JSONClass json); 74 | public abstract void Save(JSONClass json); 75 | } 76 | 77 | public class TimelineSetting where T : struct 78 | { 79 | public string key { get; } 80 | public T defaultValue { get; } 81 | public T value { get; set; } 82 | 83 | public TimelineSetting(string key, T defaultValue) 84 | { 85 | this.key = key; 86 | this.defaultValue = defaultValue; 87 | value = defaultValue; 88 | } 89 | 90 | public void Load(JSONClass json) 91 | { 92 | if (!json.HasKey(key)) return; 93 | var entry = json[key]; 94 | if (typeof(T) == typeof(int)) 95 | value = (T)(object)entry.AsInt; 96 | else if (typeof(T) == typeof(bool)) 97 | value = (T)(object)entry.AsBool; 98 | else if (typeof(T) == typeof(float)) 99 | value = (T)(object)entry.AsFloat; 100 | else 101 | throw new NotSupportedException($"Type {value.GetType()} is not supported"); 102 | } 103 | 104 | public void Save(JSONClass json) 105 | { 106 | if (typeof(T) == typeof(int)) 107 | json[key].AsInt = (int)(object)value; 108 | else if (typeof(T) == typeof(bool)) 109 | json[key].AsBool = (bool)(object)value; 110 | else if (typeof(T) == typeof(float)) 111 | json[key].AsFloat = (float)(object)value; 112 | else 113 | throw new NotSupportedException($"Type {value.GetType()} is not supported"); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/UI/Screens/LoggingScreen.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace VamTimeline 4 | { 5 | public class LoggingScreen : ScreenBase 6 | { 7 | public const string ScreenName = "Logging"; 8 | 9 | public override string screenId => ScreenName; 10 | 11 | public override void Init(IAtomPlugin plugin, object arg) 12 | { 13 | base.Init(plugin, arg); 14 | 15 | CreateChangeScreenButton("< Back", MoreScreen.ScreenName); 16 | 17 | InitLoggingUI(); 18 | } 19 | 20 | private void InitLoggingUI() 21 | { 22 | prefabFactory.CreateHeader("Quick links", 1); 23 | 24 | var toggleAllJSON = new JSONStorableBool("Toggle all", false) 25 | { 26 | valNoCallback = plugin.logger.clearOnPlay || plugin.logger.general || plugin.logger.triggersReceived || plugin.logger.sequencing || plugin.logger.peersSync || plugin.logger.triggersInvoked 27 | }; 28 | prefabFactory.CreateToggle(toggleAllJSON); 29 | 30 | prefabFactory.CreateHeader("Logging inclusions", 1); 31 | 32 | var generalJSON = new JSONStorableBool("General", false, val => plugin.logger.general = val) { valNoCallback = plugin.logger.general }; 33 | prefabFactory.CreateToggle(generalJSON); 34 | 35 | var triggersReceivedJSON = new JSONStorableBool("Triggers (Received)", false, val => plugin.logger.triggersReceived = val) { valNoCallback = plugin.logger.triggersReceived }; 36 | prefabFactory.CreateToggle(triggersReceivedJSON); 37 | 38 | var triggersInvokedJSON = new JSONStorableBool("Triggers (Invoked)", false, val => plugin.logger.triggersInvoked = val) { valNoCallback = plugin.logger.triggersInvoked }; 39 | prefabFactory.CreateToggle(triggersInvokedJSON); 40 | 41 | var sequencingJSON = new JSONStorableBool("Sequencing", false, val => plugin.logger.sequencing= val) { valNoCallback = plugin.logger.sequencing}; 42 | prefabFactory.CreateToggle(sequencingJSON); 43 | 44 | var peerSyncJSON = new JSONStorableBool("Peer syncing", false, val => plugin.logger.peersSync = val) { valNoCallback = plugin.logger.peersSync }; 45 | prefabFactory.CreateToggle(peerSyncJSON); 46 | 47 | var filterJSON = new JSONStorableString("Filter", "", val => 48 | { 49 | if (string.IsNullOrEmpty(val)) 50 | { 51 | plugin.logger.filter = null; 52 | return; 53 | } 54 | var regex = new Regex(val, RegexOptions.Compiled); 55 | plugin.logger.filter = regex; 56 | }){valNoCallback = plugin.logger.filter?.ToString()}; 57 | prefabFactory.CreateTextInput(filterJSON); 58 | 59 | prefabFactory.CreateSpacer(); 60 | prefabFactory.CreateHeader("Logging options", 1); 61 | 62 | var clearOnPlayJSON = new JSONStorableBool("Clear on play", false, val => plugin.logger.clearOnPlay = val) { valNoCallback = plugin.logger.clearOnPlay }; 63 | prefabFactory.CreateToggle(clearOnPlayJSON); 64 | 65 | var showCurrentAnimationJSON = new JSONStorableBool("Show what's playing in help text", false, val => plugin.logger.showPlayInfoInHelpText = val) { valNoCallback = plugin.logger.showPlayInfoInHelpText }; 66 | prefabFactory.CreateToggle(showCurrentAnimationJSON); 67 | 68 | prefabFactory.CreateSpacer(); 69 | prefabFactory.CreateHeader("Sync to other atoms", 1); 70 | 71 | var syncOtherAtoms = prefabFactory.CreateButton("Sync logging settings on all atoms"); 72 | syncOtherAtoms.button.onClick.AddListener(() => plugin.peers.SendLoggingSettings()); 73 | 74 | toggleAllJSON.setCallbackFunction = val => 75 | { 76 | clearOnPlayJSON.val = val; 77 | generalJSON.val = val; 78 | triggersInvokedJSON.val = val; 79 | triggersReceivedJSON.val = val; 80 | sequencingJSON.val = val; 81 | peerSyncJSON.val = val; 82 | }; 83 | } 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/AtomAnimation.Queuing.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace VamTimeline 5 | { 6 | public partial class AtomAnimation 7 | { 8 | #region Queueing 9 | 10 | private string _queueName; 11 | private readonly List _queue = new List(); 12 | private AtomAnimationClip _queueCurrent; 13 | private AtomAnimationClip _queueNext; 14 | private string _queueNextQueueName; 15 | private int _queueNextTimes = 1; 16 | private bool _processingQueue; 17 | 18 | public string GetStringifiedQueue() 19 | { 20 | var queueCount = _queue.Count; 21 | if (queueCount == 0) 22 | { 23 | if (_queueCurrent != null) 24 | { 25 | var finishingSb = new StringBuilder(); 26 | finishingSb.AppendLine("Queue finishing..."); 27 | if (_queueCurrent != null) 28 | { 29 | finishingSb.AppendLine($"▶ {_queueCurrent.animationName} (Current)"); 30 | } 31 | if (_queueNext != null) 32 | { 33 | finishingSb.AppendLine($"1: {_queueNext.animationName} (Next)"); 34 | } 35 | 36 | return finishingSb.ToString(); 37 | } 38 | 39 | if (_queueName == null) 40 | return "Queue is empty"; 41 | else 42 | return $"Queue pending {_queueName}"; 43 | } 44 | 45 | var sb = new StringBuilder(); 46 | if (_queueNext != null) queueCount++; 47 | sb.AppendLine($"Queue with {queueCount} items ({_queueName ?? "unnamed"})"); 48 | 49 | var qI = 1; 50 | if (_queueCurrent != null) 51 | { 52 | sb.AppendLine($"▶ {_queueCurrent.animationName} (Current)"); 53 | } 54 | if (_queueNext != null) 55 | { 56 | sb.AppendLine($"{qI++}: {_queueNext.animationName} (Next)"); 57 | } 58 | foreach(var clip in _queue) 59 | { 60 | sb.AppendLine($"{qI++}: {clip.animationName}"); 61 | } 62 | return sb.ToString(); 63 | } 64 | 65 | public void CreateQueue(string name) 66 | { 67 | ClearQueue(); 68 | _queueName = name; 69 | onQueueUpdated.Invoke(); 70 | } 71 | 72 | public void AddToQueue(AtomAnimationClip clip) 73 | { 74 | if (_queueName == null) 75 | _queueName = "unnamed"; 76 | 77 | _queue.Add(clip); 78 | onQueueUpdated.Invoke(); 79 | } 80 | 81 | public void PlayQueue() 82 | { 83 | if (logger.triggersReceived) logger.Log(logger.triggersCategory, $"Triggered '{StorableNames.PlayQueue}' with queue '{_queueName}' containing {_queue.Count} clips."); 84 | 85 | if (_queue.Count == 0) 86 | { 87 | SuperController.LogError($"Timeline: Cannot play queue '{_queueName}', no clips in queue."); 88 | ClearQueue(); 89 | return; 90 | } 91 | 92 | onQueueStarted.Invoke(_queueName); 93 | 94 | var next = _queue[0]; 95 | _queue.RemoveAt(0); 96 | 97 | if (_queue.Count == 0) 98 | { 99 | onQueueFinished.Invoke(_queueName); 100 | ClearQueue(); 101 | } 102 | else 103 | { 104 | _processingQueue = true; 105 | } 106 | 107 | PlayClip(next, true); 108 | } 109 | 110 | public void ClearQueue() 111 | { 112 | _processingQueue = false; 113 | _queueName = null; 114 | _queueNextTimes = 1; 115 | _queue.Clear(); 116 | 117 | onQueueUpdated.Invoke(); 118 | } 119 | 120 | #endregion 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/UI/Screens/HelpScreen.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class HelpScreen : ScreenBase 4 | { 5 | public const string ScreenName = "Help"; 6 | 7 | private const string _helpText = @" 8 | Welcome to Timeline! 9 | 10 | Create advanced and dynamic curve-based animations using keyframes and triggers. 11 | 12 | There is documentation available in the wiki (accessible from the More menu), as well as video tutorials. 13 | 14 | The UI 15 | 16 | At the top you can find tabs. This is how you'll navigate between screens. 17 | 18 | On your left, you can find the list of animations. By default, there's only one: 'Anim 1'. You can also have Layers and Segments (see below) if you use them. 19 | 20 | Under, you have the keyframe navigation controls. The leftmost and rightmost buttons go to the next/previous frame, the inner buttons let you move backward and forward, and the center button 'snaps' to the closest second. 21 | 22 | There are two Play buttons. ""All"" is used to play all layers at once, and will play sequences. The second (named after the current animation) will only play the current clip. 23 | 24 | Then you have the scrubber. It shows the animation time and where you currently are. You can move the time back and forth, and zoom in/out. 25 | 26 | Underneath, you have the dope sheet. This shows you all targets (e.g. a hand control or a smile morph), and whether there's a keyframe at any point in time. You can switch to the Curves view using the top-left button. 27 | 28 | Finally, you have buttons to delete, copy and paste; they will affect the currently selected keyframes in the dope sheet. 29 | 30 | Your first animation 31 | 32 | Select the 'Add/remove targets' button in the Targets panel. 33 | 34 | Choose what you want to animate. For example, the right hand controller: select rHandControl in the Control drop down, and press Add. You'll see the target added on the dope sheet. 35 | 36 | Move the right hand to a new position, move the scrubber to 1s, and move the right hand to another position. 37 | 38 | Now, rewind (press Stop) and Play. You'll see the right hand move back and forth between the two positions. This is because the animation is looping. You can change this behavior in the Edit tab. 39 | 40 | Sequencing 41 | 42 | You can create new animations and automatically blend between them. Create another animation (Animations, Create, Create animation) and go to the Sequence tab. You can see in the Play next drop down the other animation. If you select it, it will automatically play. Check out the wiki for more information on sequencing features. 43 | 44 | You can name your animations 'prefix/something' to play a subset of animations, they will show up as 'prefix/*' in the Play next drop down. 45 | 46 | Layers and segments 47 | 48 | You can create layers in Animations, Create. Layers allow you to have multiple animations running at the same time, each affecting its own targets. For example, you could have a layer to animate breathing, and another layer to animate the hands. Each will have its own animations and sequencing. 49 | 50 | You can also have multiple animations sharing the same name across layers; they will always play and scrub together. 51 | 52 | If you need to create animations that use different targets, you can create segments. Segments are like completely independent animations, with their own layers. Only one segment can play at a time, except the shared segment. 53 | 54 | Learning 55 | 56 | Check out the wiki (link in the More menu) for more documentation and videos. There are a ton of things you can do! Now have fun! 57 | "; 58 | 59 | public override string screenId => ScreenName; 60 | 61 | public override void Init(IAtomPlugin plugin, object arg) 62 | { 63 | base.Init(plugin, arg); 64 | 65 | CreateChangeScreenButton("< Back", MoreScreen.ScreenName); 66 | 67 | InitExplanation(); 68 | } 69 | 70 | private void InitExplanation() 71 | { 72 | var textJSON = new JSONStorableString("Help", _helpText); 73 | var textUI = prefabFactory.CreateTextField(textJSON); 74 | textUI.height = 1070f; 75 | } 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/FreeControllerV3s/FreeControllerV3Ref.cs: -------------------------------------------------------------------------------- 1 | namespace VamTimeline 2 | { 3 | public class FreeControllerV3Ref : AnimatableRefBase, IAnimatableRefWithTransform 4 | { 5 | public readonly bool owned; 6 | public readonly string lastKnownAtomUid; 7 | public readonly string lastKnownControllerName; 8 | public readonly FreeControllerV3 controller; 9 | public readonly JSONStorableFloat weightJSON; 10 | public readonly JSONStorableFloat positionWeightJSON; 11 | public readonly JSONStorableFloat rotationWeightJSON; 12 | public float scaledPositionWeight = 1f; 13 | public float scaledRotationWeight = 1f; 14 | 15 | public FreeControllerV3Ref(FreeControllerV3 controller, bool owned) 16 | { 17 | this.controller = controller; 18 | if (!owned) 19 | lastKnownAtomUid = controller.containingAtom.uid; 20 | lastKnownControllerName = controller.name; 21 | this.owned = owned; 22 | var weightJSONName = owned 23 | ? $"Controller Weight {controller.name}" 24 | : $"External Controller Weight {controller.containingAtom.name} / {controller.name}"; 25 | weightJSON = new JSONStorableFloat(weightJSONName, 1f, val => 26 | { 27 | val = val.ExponentialScale(0.1f, 1f); 28 | scaledPositionWeight = val; 29 | scaledRotationWeight = val; 30 | if (positionWeightJSON != null) positionWeightJSON.valNoCallback = val; 31 | if (rotationWeightJSON != null) rotationWeightJSON.valNoCallback = val; 32 | }, 0f, 1f) 33 | { 34 | isStorable = false 35 | }; 36 | positionWeightJSON = new JSONStorableFloat(weightJSONName + " (Position)", 1f, val => 37 | { 38 | val = val.ExponentialScale(0.1f, 1f); 39 | scaledPositionWeight = val; 40 | weightJSON.valNoCallback = val; 41 | }, 0f, 1f) 42 | { 43 | isStorable = false 44 | }; 45 | rotationWeightJSON = new JSONStorableFloat(weightJSONName + " (Rotation)", 1f, val => 46 | { 47 | 48 | val = val.ExponentialScale(0.1f, 1f); 49 | scaledRotationWeight = val; 50 | }, 0f, 1f) 51 | { 52 | isStorable = false 53 | }; 54 | } 55 | 56 | public bool selectedPosition { get; set; } 57 | public bool selectedRotation { get; set; } 58 | 59 | public override string name 60 | { 61 | get 62 | { 63 | if (!owned && controller == null) 64 | return "[Missing]"; 65 | 66 | return controller.name; 67 | } 68 | } 69 | 70 | public override object groupKey => controller != null ? (object)controller.containingAtom : 0; 71 | 72 | public override string groupLabel => owned 73 | ? "Controls" 74 | : $"{(controller != null ? controller.containingAtom.name : lastKnownAtomUid)} controls"; 75 | 76 | #warning Mark with or without rotation 77 | public override string GetShortName() 78 | { 79 | if (!owned && controller == null) 80 | return $"[Missing: {lastKnownControllerName}]"; 81 | 82 | return controller.name.EndsWith("Control") 83 | ? controller.name.Substring(0, controller.name.Length - "Control".Length) 84 | : controller.name; 85 | } 86 | 87 | public override string GetFullName() 88 | { 89 | if (!owned) 90 | { 91 | if (controller == null) 92 | return $"[Missing: {lastKnownAtomUid} {lastKnownControllerName}]"; 93 | return $"{controller.containingAtom.name} {controller.name}"; 94 | } 95 | 96 | return controller.name; 97 | } 98 | 99 | public bool Targets(FreeControllerV3 otherController) 100 | { 101 | return controller == otherController; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Unit/AtomAnimations/Targets/FreeControllerAnimationTargetTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace VamTimeline 7 | { 8 | public class FreeControllerAnimationTargetTests : ITestClass 9 | { 10 | public IEnumerable GetTests() 11 | { 12 | yield return new Test(nameof(AddEdgeFramesIfMissing_SameLengthStaysUntouched), AddEdgeFramesIfMissing_SameLengthStaysUntouched); 13 | yield return new Test(nameof(AddEdgeFramesIfMissing_WithTwoKeyframes_Moves), AddEdgeFramesIfMissing_WithTwoKeyframes_Moves); 14 | yield return new Test(nameof(AddEdgeFramesIfMissing_WithThreeKeyframes_Adds), AddEdgeFramesIfMissing_WithThreeKeyframes_Adds); 15 | yield return new Test(nameof(AddEdgeFramesIfMissing_WithCopyPrevious_AlwaysExtends), AddEdgeFramesIfMissing_WithCopyPrevious_AlwaysExtends); 16 | } 17 | 18 | public IEnumerable AddEdgeFramesIfMissing_SameLengthStaysUntouched(TestContext context) 19 | { 20 | var target = GivenAFreeController(context); 21 | target.SetKeyframeByTime(0, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 22 | target.SetKeyframeByTime(1, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 23 | target.SetKeyframeByTime(2, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 24 | 25 | target.AddEdgeFramesIfMissing(2f); 26 | 27 | context.AssertList(target.rotation.rotX.keys.Select(k => k.curveType), 28 | new[] {CurveTypeValues.Linear, CurveTypeValues.Linear, CurveTypeValues.Linear} 29 | ); 30 | yield break; 31 | } 32 | 33 | public IEnumerable AddEdgeFramesIfMissing_WithTwoKeyframes_Moves(TestContext context) 34 | { 35 | var target = GivenAFreeController(context); 36 | target.SetKeyframeByTime(0, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 37 | target.SetKeyframeByTime(1, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 38 | 39 | target.AddEdgeFramesIfMissing(2f); 40 | 41 | context.AssertList(target.rotation.rotX.keys.Select(k => k.curveType), 42 | new[] {CurveTypeValues.Linear, CurveTypeValues.Linear} 43 | ); 44 | yield break; 45 | } 46 | 47 | public IEnumerable AddEdgeFramesIfMissing_WithThreeKeyframes_Adds(TestContext context) 48 | { 49 | var target = GivenAFreeController(context); 50 | target.SetKeyframeByTime(0, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 51 | target.SetKeyframeByTime(1, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 52 | target.SetKeyframeByTime(2, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 53 | 54 | target.AddEdgeFramesIfMissing(3f); 55 | 56 | context.AssertList(target.rotation.rotX.keys.Select(k => k.curveType), 57 | new[] {CurveTypeValues.Linear, CurveTypeValues.Linear, CurveTypeValues.Linear, CurveTypeValues.Linear} 58 | ); 59 | yield break; 60 | } 61 | 62 | public IEnumerable AddEdgeFramesIfMissing_WithCopyPrevious_AlwaysExtends(TestContext context) 63 | { 64 | var target = GivenAFreeController(context); 65 | target.SetKeyframeByTime(0, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 66 | target.SetKeyframeByTime(1, Vector3.zero, Quaternion.identity, CurveTypeValues.Linear); 67 | target.SetKeyframeByTime(2, Vector3.zero, Quaternion.identity, CurveTypeValues.CopyPrevious); 68 | 69 | target.AddEdgeFramesIfMissing(3f); 70 | 71 | context.AssertList(target.rotation.rotX.keys.Select(k => k.curveType), 72 | new[] {CurveTypeValues.Linear, CurveTypeValues.Linear, CurveTypeValues.CopyPrevious} 73 | ); 74 | yield break; 75 | } 76 | 77 | private static FreeControllerV3AnimationTarget GivenAFreeController(TestContext context) 78 | { 79 | var animatable = new TargetsHelper(context).GivenFreeController(); 80 | var target = new FreeControllerV3AnimationTarget(animatable, true, true); 81 | return target; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/UI/Components/AnimatableFrames/FreeControllerV2AnimationTargetFrameComponent.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class FreeControllerV2AnimationTargetFrameComponent : AnimationTargetFrameComponentBase 7 | { 8 | protected override bool enableValueText => true; 9 | protected override bool enableLabel => true; 10 | 11 | protected override float expandSize => 140f; 12 | 13 | protected override void CreateCustom() 14 | { 15 | } 16 | 17 | protected override void CreateExpandPanel(RectTransform container) 18 | { 19 | var group = container.gameObject.AddComponent(); 20 | group.spacing = 4f; 21 | group.padding = new RectOffset(8, 8, 8, 8); 22 | group.childAlignment = TextAnchor.MiddleCenter; 23 | 24 | var row1 = new GameObject(); 25 | row1.transform.SetParent(group.transform, false); 26 | row1.AddComponent().preferredHeight = 70f; 27 | row1.AddComponent(); 28 | 29 | CreateExpandButton(row1.transform, "Select", () => target.SelectInVam()); 30 | 31 | CreateExpandButton(row1.transform, "Parenting & more", () => 32 | { 33 | plugin.ChangeScreen(ControllerTargetSettingsScreen.ScreenName, target); 34 | }); 35 | 36 | var row2 = new GameObject(); 37 | row2.transform.SetParent(group.transform, false); 38 | row2.AddComponent(); 39 | 40 | if (target.targetsPosition) 41 | { 42 | var posJSON = new JSONStorableBool("Pos. enabled", target.controlPosition, val => target.controlPosition = val); 43 | CreateExpandToggle(row2.transform, posJSON); 44 | } 45 | if (target.targetsRotation) 46 | { 47 | var rotJSON = new JSONStorableBool("Rot. enabled", target.controlRotation, val => target.controlRotation = val); 48 | CreateExpandToggle(row2.transform, rotJSON); 49 | } 50 | } 51 | 52 | public override void SetTime(float time, bool stopped) 53 | { 54 | base.SetTime(time, stopped); 55 | 56 | if (stopped) 57 | { 58 | if (!target.animatableRef.owned && target.animatableRef.controller == null) 59 | { 60 | valueText.text = "[Missing]"; 61 | } 62 | else 63 | { 64 | var pos = target.animatableRef.controller.transform.position; 65 | valueText.text = $"x: {pos.x:0.000} y: {pos.y:0.000} z: {pos.z:0.000}"; 66 | } 67 | } 68 | } 69 | 70 | protected override void ToggleKeyframeImpl(float time, bool on, bool mustBeOn) 71 | { 72 | if (on) 73 | { 74 | if (plugin.animationEditContext.autoKeyframeAllControllers) 75 | { 76 | foreach (var target1 in clip.targetControllers) 77 | SetControllerKeyframe(time, target1); 78 | } 79 | else 80 | { 81 | SetControllerKeyframe(time, target); 82 | } 83 | } 84 | else 85 | { 86 | if (plugin.animationEditContext.autoKeyframeAllControllers) 87 | { 88 | foreach (var target1 in clip.targetControllers) 89 | target1.DeleteFrame(time); 90 | } 91 | else 92 | { 93 | target.DeleteFrame(time); 94 | } 95 | } 96 | } 97 | 98 | private void SetControllerKeyframe(float time, FreeControllerV3AnimationTarget target) 99 | { 100 | var key = plugin.animationEditContext.SetKeyframeToCurrentTransform(target, time); 101 | var keyframe = target.GetLeadCurve().keys[key]; 102 | if (keyframe.curveType == CurveTypeValues.CopyPrevious) 103 | target.ChangeCurveByTime(time, CurveTypeValues.SmoothLocal); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/CurvesBase/CurveAnimationTargetBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace VamTimeline 7 | { 8 | public abstract class CurveAnimationTargetBase : AnimationTargetBase where TAnimatableRef : AnimatableRefBase 9 | { 10 | protected CurveAnimationTargetBase(TAnimatableRef animatableRef) 11 | : base(animatableRef) 12 | { 13 | } 14 | 15 | public abstract BezierAnimationCurve GetLeadCurve(); 16 | public abstract IEnumerable GetCurves(); 17 | 18 | protected void Validate(BezierAnimationCurve curve, float animationLength) 19 | { 20 | if (animationLength <= 0) 21 | { 22 | SuperController.LogError($"Target {name} has an invalid animation length of {animationLength}"); 23 | return; 24 | } 25 | if (curve.length < 2) 26 | { 27 | SuperController.LogError($"Target {name} has {curve.length} frames"); 28 | return; 29 | } 30 | if (curve.GetFirstFrame().time != 0) 31 | { 32 | SuperController.LogError($"Target {name} has no start frame. Frames: {string.Join(", ", curve.keys.Select(k => k.time.ToString(CultureInfo.InvariantCulture)).ToArray())}"); 33 | return; 34 | } 35 | if (curve.duration > animationLength + 0.0001f) 36 | { 37 | SuperController.LogError($"Target {name} has duration of {curve.duration:0.0000} but the animation should be {animationLength:0.0000}. Auto-repairing extraneous keys."); 38 | foreach (var c in GetCurves()) 39 | while (c.GetKeyframeByKey(c.length - 1).time > animationLength && c.length > 2) 40 | c.RemoveKey(c.length - 1); 41 | } 42 | if (curve.duration != animationLength) 43 | { 44 | if(Mathf.Abs(curve.duration - animationLength) > 0.0009f) 45 | SuperController.LogError($"Target {name} ends with frame {curve.duration:0.0000} instead of expected {animationLength:0.0000}. Auto-repairing last frame."); 46 | foreach (var c in GetCurves()) 47 | { 48 | var keyframe = c.GetLastFrame(); 49 | if (keyframe.time == animationLength) continue; 50 | keyframe.time = animationLength; 51 | c.SetLastFrame(keyframe); 52 | } 53 | } 54 | } 55 | 56 | public void ChangeCurveByTime(float time, int curveType, bool makeDirty = true) 57 | { 58 | var key = GetLeadCurve().KeyframeBinarySearch(time); 59 | ChangeCurveByKey(key, curveType, makeDirty); 60 | } 61 | 62 | public void ChangeCurveByKey(int key, int curveType, bool makeDirty = true) 63 | { 64 | if (key == -1) return; 65 | foreach (var curve in GetCurves()) 66 | { 67 | var keyframe = curve.GetKeyframeByKey(key); 68 | keyframe.curveType = curveType; 69 | curve.SetKeyframeByKey(key, keyframe); 70 | if (curve.loop && key == 0) 71 | { 72 | var last = curve.GetLastFrame(); 73 | last.curveType = curveType; 74 | curve.SetLastFrame(last); 75 | } 76 | } 77 | 78 | if (makeDirty) dirty = true; 79 | } 80 | 81 | protected int SelectCurveType(float time, int curveType) 82 | { 83 | if (curveType != CurveTypeValues.Undefined) 84 | return curveType; 85 | var curve = GetLeadCurve(); 86 | if (curve.keys.Count == 0) 87 | return CurveTypeValues.SmoothLocal; 88 | var key = curve.KeyframeBinarySearch(time, true); 89 | if (key == -1) 90 | return CurveTypeValues.SmoothLocal; 91 | var keyframe = curve.keys[key]; 92 | if (keyframe.curveType != CurveTypeValues.CopyPrevious) 93 | return keyframe.curveType; 94 | return CurveTypeValues.SmoothLocal; 95 | } 96 | 97 | public int GetKeyframeCurveTypeByTime(float time) 98 | { 99 | return GetLeadCurve().GetKeyframeAt(time).curveType; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/UnitySpecific.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | #pragma warning disable IDE1006 4 | namespace VamTimeline 5 | { 6 | /// 7 | /// Converted from https://github.com/unity3d-jp/MeshSync/blob/dev/Plugin%7E/Src/mscore/msUnitySpecific.cpp 8 | /// 9 | public static class UnitySpecific 10 | { 11 | // const float kDefaultWeight = 1.0f / 3.0f; 12 | // const float kCurveTimeEpsilon = 0.00001f; 13 | 14 | private static Quaternion GetValue(BezierAnimationCurve x, BezierAnimationCurve y, BezierAnimationCurve z, BezierAnimationCurve w, int key) 15 | { 16 | return new Quaternion(x.GetKeyframeByKey(key).value, y.GetKeyframeByKey(key).value, z.GetKeyframeByKey(key).value, w.GetKeyframeByKey(key).value); 17 | } 18 | 19 | private static void SetValue(BezierAnimationCurve x, BezierAnimationCurve y, BezierAnimationCurve z, BezierAnimationCurve w, int key, Quaternion q) 20 | { 21 | SetValue(x, key, q.x); 22 | SetValue(y, key, q.y); 23 | SetValue(z, key, q.z); 24 | SetValue(w, key, q.w); 25 | } 26 | 27 | private static void SetValue(BezierAnimationCurve curve, int key, float value) 28 | { 29 | var keyframe = curve.GetKeyframeByKey(key); 30 | keyframe.value = value; 31 | curve.SetKeyframeByKey(key, keyframe); 32 | } 33 | 34 | public static void EnsureQuaternionContinuityAndRecalculateSlope(BezierAnimationCurve x, BezierAnimationCurve y, BezierAnimationCurve z, BezierAnimationCurve w, Quaternion last) 35 | { 36 | var keyCount = x.length; 37 | if (keyCount < 2) return; 38 | for (var i = 0; i < keyCount; i++) 39 | { 40 | var cur = GetValue(x, y, z, w, i); 41 | if (Quaternion.Dot(cur, last) < 0.0f) 42 | cur = new Quaternion(-cur.x, -cur.y, -cur.z, -cur.w); 43 | last = cur; 44 | SetValue(x, y, z, w, i, cur); 45 | } 46 | 47 | // for (int i = 0; i < keyCount; i++) 48 | // { 49 | // RecalculateSplineSlopeT(x, i); 50 | // RecalculateSplineSlopeT(y, i); 51 | // RecalculateSplineSlopeT(z, i); 52 | // RecalculateSplineSlopeT(w, i); 53 | // } 54 | } 55 | 56 | // private static void RecalculateSplineSlopeT(BezierAnimationCurve curve, int key, float b = 0.0f) 57 | // { 58 | // if (curve.length < 2) 59 | // return; 60 | 61 | // var keyframe = curve.GetKeyframe(key); 62 | // if (key == 0) 63 | // { 64 | // float dx = curve.GetKeyframe(1).time - curve.GetKeyframe(0).time; 65 | // float dy = curve.GetKeyframe(1).value - curve.GetKeyframe(0).value; 66 | // float m = dy / dx; 67 | // keyframe.inTangent = m; 68 | // keyframe.outTangent = m; 69 | // keyframe.outWeight = kDefaultWeight; 70 | // } 71 | // else if (key == curve.length - 1) 72 | // { 73 | // float dx = keyframe.time - curve.GetKeyframe(key - 1).time; 74 | // float dy = keyframe.value - curve.GetKeyframe(key - 1).value; 75 | // float m = dy / dx; 76 | // keyframe.inTangent = m; 77 | // keyframe.outTangent = m; 78 | // keyframe.inWeight = kDefaultWeight; 79 | // } 80 | // else 81 | // { 82 | // float dx1 = keyframe.time - curve.GetKeyframe(key - 1).time; 83 | // float dy1 = keyframe.value - curve.GetKeyframe(key - 1).value; 84 | 85 | // float dx2 = curve.GetKeyframe(key + 1).time - keyframe.time; 86 | // float dy2 = curve.GetKeyframe(key + 1).value - keyframe.value; 87 | 88 | // float m1 = SafeDiv(dy1, dx1); 89 | // float m2 = SafeDiv(dy2, dx2); 90 | 91 | // float m = (1.0f + b) * 0.5f * m1 + (1.0f - b) * 0.5f * m2; 92 | // keyframe.inTangent = m; 93 | // keyframe.outTangent = m; 94 | // keyframe.inWeight = kDefaultWeight; 95 | // keyframe.outWeight = kDefaultWeight; 96 | // } 97 | 98 | // curve.MoveKey(key, keyframe); 99 | // } 100 | 101 | // private static float SafeDiv(float y, float x) 102 | // { 103 | // if (Mathf.Abs(x) > kCurveTimeEpsilon) 104 | // return y / x; 105 | // else 106 | // return 0; 107 | // } 108 | } 109 | } 110 | #pragma warning restore IDE1006 111 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animatables/AnimatablesRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine.Events; 5 | 6 | namespace VamTimeline 7 | { 8 | public class AnimatablesRegistry : IDisposable 9 | { 10 | private readonly List _storableFloats = new List(); 11 | private readonly List _controllers = new List(); 12 | private readonly List _triggers = new List(); 13 | 14 | public readonly UnityEvent onTargetsSelectionChanged = new UnityEvent(); 15 | public readonly UnityEvent onControllersListChanged = new UnityEvent(); 16 | public bool locked; 17 | 18 | public IList storableFloats => _storableFloats; 19 | 20 | public JSONStorableFloatRef GetOrCreateStorableFloat(Atom atom, string storableId, string floatParamName, bool owned, float? assignMinValueOnBound = null, float? assignMaxValueOnBound = null) 21 | { 22 | var t = _storableFloats.FirstOrDefault(x => x.Targets(atom, storableId, floatParamName)); 23 | if (t != null) return t; 24 | t = new JSONStorableFloatRef(atom, storableId, floatParamName, owned, assignMinValueOnBound, assignMaxValueOnBound); 25 | _storableFloats.Add(t); 26 | RegisterAnimatableRef(t); 27 | return t; 28 | } 29 | 30 | public JSONStorableFloatRef GetOrCreateStorableFloat(JSONStorable storable, JSONStorableFloat floatParam, bool owned) 31 | { 32 | var t = _storableFloats.FirstOrDefault(x => x.Targets(storable, floatParam)); 33 | if (t != null) return t; 34 | t = new JSONStorableFloatRef(storable, floatParam, owned); 35 | _storableFloats.Add(t); 36 | RegisterAnimatableRef(t); 37 | return t; 38 | } 39 | 40 | public void RemoveStorableFloat(JSONStorableFloatRef t) 41 | { 42 | _storableFloats.Remove(t); 43 | UnregisterAnimatableRef(t); 44 | } 45 | 46 | public IList controllers => _controllers; 47 | 48 | public FreeControllerV3Ref GetOrCreateController(FreeControllerV3 controller, bool owned) 49 | { 50 | var t = _controllers.FirstOrDefault(x => x.Targets(controller)); 51 | if (t != null) return t; 52 | t = new FreeControllerV3Ref(controller, owned); 53 | _controllers.Add(t); 54 | onControllersListChanged.Invoke(); 55 | RegisterAnimatableRef(t); 56 | return t; 57 | } 58 | 59 | public void RemoveController(FreeControllerV3Ref t) 60 | { 61 | _controllers.Remove(t); 62 | onControllersListChanged.Invoke(); 63 | UnregisterAnimatableRef(t); 64 | } 65 | 66 | public IList triggers => _triggers; 67 | 68 | public TriggersTrackRef GetOrCreateTriggerTrack(int animationLayerQualifiedId, string triggerTrackName) 69 | { 70 | var t = _triggers.FirstOrDefault(x => x.Targets(animationLayerQualifiedId, triggerTrackName)); 71 | if (t != null) return t; 72 | t = new TriggersTrackRef(animationLayerQualifiedId, triggerTrackName); 73 | _triggers.Add(t); 74 | RegisterAnimatableRef(t); 75 | return t; 76 | } 77 | 78 | public void RemoveTriggerTrack(TriggersTrackRef t) 79 | { 80 | _triggers.Remove(t); 81 | UnregisterAnimatableRef(t); 82 | } 83 | 84 | private void RegisterAnimatableRef(AnimatableRefBase animatableRef) 85 | { 86 | animatableRef.onSelectedChanged.AddListener(OnSelectedChanged); 87 | } 88 | 89 | private void UnregisterAnimatableRef(AnimatableRefBase animatableRef) 90 | { 91 | animatableRef.onSelectedChanged.RemoveListener(OnSelectedChanged); 92 | } 93 | 94 | private void OnSelectedChanged() 95 | { 96 | onTargetsSelectionChanged.Invoke(); 97 | } 98 | 99 | public void Dispose() 100 | { 101 | foreach (var t in _storableFloats) 102 | t.onSelectedChanged.RemoveAllListeners(); 103 | 104 | foreach (var t in _controllers) 105 | t.onSelectedChanged.RemoveAllListeners(); 106 | 107 | foreach (var t in _triggers) 108 | t.onSelectedChanged.RemoveAllListeners(); 109 | } 110 | 111 | public void RemoveAllListeners() 112 | { 113 | onTargetsSelectionChanged.RemoveAllListeners(); 114 | onControllersListChanged.RemoveAllListeners(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/VamOverlaysFadeManager.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Linq; 3 | using SimpleJSON; 4 | 5 | namespace VamTimeline 6 | { 7 | public interface IFadeManager 8 | { 9 | float blackTime { get; set; } 10 | float halfBlackTime { get; } 11 | bool black { get; } 12 | float fadeInTime { get; } 13 | float fadeOutTime { get; } 14 | 15 | string GetAtomUid(); 16 | bool TryConnectNow(); 17 | JSONNode GetJSON(); 18 | void SyncFadeTime(); 19 | void FadeIn(); 20 | void FadeOut(); 21 | void FadeOutInstant(); 22 | bool ShowText(string text); 23 | } 24 | 25 | public class VamOverlaysFadeManager : IFadeManager 26 | { 27 | public float blackTime 28 | { 29 | get { return _blackTime; } 30 | set 31 | { 32 | _blackTime = value; 33 | halfBlackTime = blackTime / 2f; 34 | } 35 | } 36 | public float halfBlackTime { get; private set; } 37 | public bool black { get; private set; } 38 | 39 | public float fadeInTime { get; private set; } 40 | public float fadeOutTime { get; private set; } 41 | 42 | private float _blackTime; 43 | private JSONStorable _overlays; 44 | private string _atomUid; 45 | private Atom _atom; 46 | 47 | private JSONStorableAction _fadeIn; 48 | private JSONStorableAction _fadeOut; 49 | private JSONStorableAction _fadeOutInstant; 50 | private JSONStorableFloat _fadeInTime; 51 | private JSONStorableFloat _fadeOutTime; 52 | private JSONStorableString _showText; 53 | private JSONStorableAction _hideText; 54 | 55 | public static IFadeManager FromAtomUid(string atomUid, float blackTime) 56 | { 57 | return new VamOverlaysFadeManager { _atomUid = atomUid, blackTime = blackTime}; 58 | } 59 | 60 | public void SyncFadeTime() 61 | { 62 | fadeInTime = _fadeInTime?.val ?? 1f; 63 | fadeOutTime = _fadeOutTime?.val ?? 1f; 64 | } 65 | 66 | public void FadeIn() 67 | { 68 | black = false; 69 | if (TryConnectNow()) 70 | _fadeIn.actionCallback(); 71 | } 72 | 73 | public void FadeOut() 74 | { 75 | if (TryConnectNow()) 76 | { 77 | black = true; 78 | _fadeOut.actionCallback(); 79 | } 80 | } 81 | 82 | public void FadeOutInstant() 83 | { 84 | if (TryConnectNow() && _fadeOutInstant != null) 85 | { 86 | black = true; 87 | _fadeOutInstant.actionCallback(); 88 | } 89 | } 90 | 91 | public bool ShowText(string text) 92 | { 93 | if (!TryConnectNow()) 94 | return false; 95 | 96 | if (text != null) 97 | { 98 | if (_showText == null) return false; 99 | _showText.val = text; 100 | } 101 | else 102 | { 103 | if (_hideText == null) return false; 104 | _hideText.actionCallback.Invoke(); 105 | } 106 | 107 | return true; 108 | } 109 | 110 | public string GetAtomUid() 111 | { 112 | return _atom != null ? _atom.uid : _atomUid; 113 | } 114 | 115 | public bool TryConnectNow() 116 | { 117 | if (_overlays != null) return true; 118 | _atom = SuperController.singleton.GetAtomByUid(_atomUid); 119 | if (_atom == null) return false; 120 | _overlays = _atom.GetStorableIDs().Select(_atom.GetStorableByID).FirstOrDefault(s => s.IsAction("Start Fade In")); 121 | if (_overlays == null) return false; 122 | _fadeIn = _overlays.GetAction("Start Fade In"); 123 | _fadeOut = _overlays.GetAction("Start Fade Out"); 124 | _fadeOutInstant = _overlays.GetAction("Fade Out Instant"); 125 | _fadeInTime = _overlays.GetFloatJSONParam("Fade in time"); 126 | _fadeOutTime = _overlays.GetFloatJSONParam("Fade out time"); 127 | _showText = _overlays.GetStringJSONParam("Set and show subtitles instant"); 128 | _hideText = _overlays.GetAction("Hide subtitles instant"); 129 | if (_fadeIn != null && _fadeOut != null) return true; 130 | _overlays = null; 131 | return false; 132 | } 133 | 134 | public JSONNode GetJSON() 135 | { 136 | return new JSONClass 137 | { 138 | ["Atom"] = GetAtomUid(), 139 | ["BlackTime"] = blackTime.ToString(CultureInfo.InvariantCulture) 140 | }; 141 | } 142 | 143 | public static IFadeManager FromJSON(JSONClass jc) 144 | { 145 | var atomUid = jc["Atom"].Value; 146 | if (atomUid == null) return null; 147 | return FromAtomUid(atomUid, jc["BlackTime"].AsFloat); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/AtomAnimations/BezierCurves/Smoothing/BezierAnimationCurveSmoothingNonLooping.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace VamTimeline 5 | { 6 | public class BezierAnimationCurveSmoothingNonLooping : BezierAnimationCurveSmoothingBase, IBezierAnimationCurveSmoothing 7 | { 8 | public bool looping => false; 9 | 10 | private float[] _p; 11 | private float[] _d; 12 | 13 | public void AutoComputeControlPoints(List keys) 14 | { 15 | // Original implementation: https://www.particleincell.com/wp-content/uploads/2012/06/bezier-spline.js 16 | // Based on: https://www.particleincell.com/2012/bezier-splines/ 17 | // Using improvements on near keyframes: http://www.jacos.nl/jacos_html/spline/ 18 | var n = keys.Count - 1; 19 | // ComputeTimeAndDistance(keys); 20 | InitializeArrays(n); 21 | Weighting(keys, n); 22 | InternalSegments(keys, n); 23 | ThomasAlgorithm(); 24 | Rearrange(n); 25 | AssignComputedControlPointsToKeyframes(keys, n); 26 | } 27 | 28 | [MethodImpl(256)] 29 | private void InitializeArrays(int n) 30 | { 31 | if (_w == null || _w.Length < n + 1) 32 | { 33 | _w = new float[n + 1]; 34 | _p1 = new float[n + 1]; 35 | _p2 = new float[n]; 36 | // rhs vector 37 | // TODO: *2 only for non-looping? 38 | _a = new float[n * 2]; 39 | _b = new float[n * 2]; 40 | _c = new float[n * 2]; 41 | _d = new float[n * 2]; 42 | _r = new float[n * 2]; 43 | _p = new float[n * 2]; 44 | } 45 | } 46 | 47 | [MethodImpl(256)] 48 | private void Weighting(List keys, int n) 49 | { 50 | for (var i = 0; i < n; i++) 51 | { 52 | _w[i] = Weighting(keys[i+1], keys[i]); 53 | } 54 | _w[n] = _w[n - 1]; 55 | } 56 | 57 | [MethodImpl(256)] 58 | private void InternalSegments(List keys, int n) 59 | { 60 | // left most segment 61 | var idx = 0; 62 | _a[idx] = 0; // outside the matrix 63 | _b[idx] = 2; 64 | _c[idx] = -1; 65 | _d[idx] = 0; 66 | _r[idx] = keys[0].value + 0;// add curvature at K0 67 | 68 | // internal segments 69 | for (var i = 1; i < n; i++) 70 | { 71 | idx = 2 * i - 1; 72 | _a[idx] = 1 * _w[i] * _w[i]; 73 | _b[idx] = -2 * _w[i] * _w[i]; 74 | _c[idx] = 2 * _w[i - 1] * _w[i - 1]; 75 | _d[idx] = -1 * _w[i - 1] * _w[i - 1]; 76 | _r[idx] = keys[i].value * (-_w[i] * _w[i] + _w[i - 1] * _w[i - 1]); 77 | 78 | idx = 2 * i; 79 | _a[idx] = _w[i]; 80 | _b[idx] = _w[i - 1]; 81 | _c[idx] = 0; 82 | _d[idx] = 0; // note: d[2n-2] is already outside the matrix 83 | _r[idx] = (_w[i - 1] + _w[i]) * keys[i].value; 84 | 85 | } 86 | 87 | // right segment 88 | idx = 2 * n - 1; 89 | _a[idx] = -1; 90 | _b[idx] = 2; 91 | _r[idx] = keys[n].value; // curvature at last point 92 | _c[idx] = 0; // outside the matrix 93 | _d[2 * n - 2] = 0; // outside the matrix 94 | _d[idx] = 0; // outside the matrix 95 | } 96 | 97 | [MethodImpl(256)] 98 | private void ThomasAlgorithm() 99 | { 100 | var n = _r.Length; 101 | 102 | // the following array elements are not in the original matrix, so they should not have an effect 103 | _a[0] = 0; // outside the matrix 104 | _c[n - 1] = 0; // outside the matrix 105 | _d[n - 2] = 0; // outside the matrix 106 | _d[n - 1] = 0; // outside the matrix 107 | 108 | /* solves Ax=b with the Thomas algorithm (from Wikipedia) */ 109 | /* adapted for a 4-diagonal matrix. only the a[i] are under the diagonal, so the Gaussian elimination is very similar */ 110 | for (var i = 1; i < n; i++) 111 | { 112 | var m = _a[i] / _b[i - 1]; 113 | _b[i] = _b[i] - m * _c[i - 1]; 114 | _c[i] = _c[i] - m * _d[i - 1]; 115 | _r[i] = _r[i] - m * _r[i - 1]; 116 | } 117 | 118 | _p[n - 1] = _r[n - 1] / _b[n - 1]; 119 | _p[n - 2] = (_r[n - 2] - _c[n - 2] * _p[n - 1]) / _b[n - 2]; 120 | for (var i = n - 3; i >= 0; --i) 121 | { 122 | _p[i] = (_r[i] - _c[i] * _p[i + 1] - _d[i] * _p[i + 2]) / _b[i]; 123 | } 124 | } 125 | 126 | [MethodImpl(256)] 127 | private void Rearrange(int n) 128 | { 129 | for (var i = 0; i < n; i++) 130 | { 131 | _p1[i] = _p[2 * i]; 132 | _p2[i] = _p[2 * i + 1]; 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/UI/Components/CurveTypePopup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | namespace VamTimeline 6 | { 7 | public class CurveTypePopup : MonoBehaviour 8 | { 9 | private const string _noKeyframeCurveType = "(No Keyframe)"; 10 | 11 | private readonly HashSet _curveTypes = new HashSet(); 12 | 13 | public static CurveTypePopup Create(VamPrefabFactory prefabFactory) 14 | { 15 | var curveTypeJSON = new JSONStorableStringChooser("Change curve", CurveTypeValues.choicesList, "", "Curve type"); 16 | var curveTypeUI = prefabFactory.CreatePopup(curveTypeJSON, false, true, 380f, true); 17 | 18 | var curveTypePopup = curveTypeUI.gameObject.AddComponent(); 19 | curveTypePopup._curveTypeJSON = curveTypeJSON; 20 | curveTypePopup.curveTypeUI = curveTypeUI; 21 | 22 | return curveTypePopup; 23 | } 24 | 25 | public UIDynamicPopup curveTypeUI; 26 | private JSONStorableStringChooser _curveTypeJSON; 27 | private AtomAnimationEditContext _animationEditContext; 28 | private bool _listening; 29 | 30 | public void Bind(AtomAnimationEditContext animationEditContext) 31 | { 32 | _animationEditContext = animationEditContext; 33 | _curveTypeJSON.setCallbackFunction = ChangeCurve; 34 | OnEnable(); 35 | } 36 | 37 | private void ChangeCurve(string val) 38 | { 39 | if (!_animationEditContext.CanEdit()) 40 | { 41 | RefreshCurrentCurveType(_animationEditContext.clipTime); 42 | return; 43 | } 44 | 45 | if (string.IsNullOrEmpty(val) || val.StartsWith("(")) 46 | { 47 | RefreshCurrentCurveType(_animationEditContext.clipTime); 48 | return; 49 | } 50 | var time = _animationEditContext.clipTime.Snap(); 51 | 52 | var curveType = CurveTypeValues.ToInt(val); 53 | 54 | foreach (var target in _animationEditContext.GetAllOrSelectedTargets().OfType()) 55 | target.ChangeCurveByTime(time, curveType); 56 | 57 | if (curveType == CurveTypeValues.CopyPrevious) 58 | _animationEditContext.Sample(); 59 | 60 | RefreshCurrentCurveType(_animationEditContext.clipTime); 61 | } 62 | 63 | private void RefreshCurrentCurveType(float currentClipTime) 64 | { 65 | if (_curveTypeJSON == null) return; 66 | 67 | var time = currentClipTime.Snap(); 68 | _curveTypes.Clear(); 69 | foreach (var target in _animationEditContext.GetAllOrSelectedTargets().OfType()) 70 | { 71 | var curveType = target.GetKeyframeCurveTypeByTime(time); 72 | if (curveType == BezierKeyframe.NullKeyframeCurveType) continue; 73 | _curveTypes.Add(CurveTypeValues.FromInt(curveType)); 74 | } 75 | 76 | switch (_curveTypes.Count) 77 | { 78 | case 0: 79 | _curveTypeJSON.valNoCallback = _noKeyframeCurveType; 80 | curveTypeUI.popup.topButton.interactable = false; 81 | break; 82 | case 1: 83 | _curveTypeJSON.valNoCallback = _curveTypes.First(); 84 | curveTypeUI.popup.topButton.interactable = true; 85 | break; 86 | default: 87 | _curveTypeJSON.valNoCallback = "(" + string.Join("/", _curveTypes.ToArray()) + ")"; 88 | curveTypeUI.popup.topButton.interactable = true; 89 | break; 90 | } 91 | } 92 | 93 | private void OnTimeChanged(AtomAnimationEditContext.TimeChangedEventArgs args) 94 | { 95 | RefreshCurrentCurveType(args.currentClipTime); 96 | } 97 | 98 | private void OnTargetsSelectionChanged() 99 | { 100 | RefreshCurrentCurveType(_animationEditContext.clipTime); 101 | } 102 | 103 | private void OnAnimationRebuilt() 104 | { 105 | RefreshCurrentCurveType(_animationEditContext.clipTime); 106 | } 107 | 108 | public void OnEnable() 109 | { 110 | if (_listening || _animationEditContext == null) return; 111 | _listening = true; 112 | _animationEditContext.onTimeChanged.AddListener(OnTimeChanged); 113 | _animationEditContext.animation.animatables.onTargetsSelectionChanged.AddListener(OnTargetsSelectionChanged); 114 | _animationEditContext.animation.onAnimationRebuilt.AddListener(OnAnimationRebuilt); 115 | OnTimeChanged(_animationEditContext.timeArgs); 116 | } 117 | 118 | public void OnDisable() 119 | { 120 | if (!_listening || _animationEditContext == null) return; 121 | _animationEditContext.onTimeChanged.RemoveListener(OnTimeChanged); 122 | _animationEditContext.animation.animatables.onTargetsSelectionChanged.RemoveListener(OnTargetsSelectionChanged); 123 | _animationEditContext.animation.onAnimationRebuilt.RemoveListener(OnAnimationRebuilt); 124 | _listening = false; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/AtomAnimations/Operations/Reduction/ControllerTargetReduceProcessor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace VamTimeline 4 | { 5 | public class ControllerTargetReduceProcessor : TargetReduceProcessorBase, ITargetReduceProcessor 6 | { 7 | ICurveAnimationTarget ITargetReduceProcessor.target => source; 8 | 9 | public ControllerTargetReduceProcessor(FreeControllerV3AnimationTarget source, ReduceSettings settings) 10 | : base(source, settings) 11 | { 12 | } 13 | 14 | public void CopyToBranch(int key, int curveType = CurveTypeValues.Undefined, float time = -1) 15 | { 16 | var sourceLead = source.GetLeadCurve(); 17 | var branchLead = branch.GetLeadCurve(); 18 | if (time < -Mathf.Epsilon) 19 | time = sourceLead.keys[key].time; 20 | branch.SetSnapshot(time, source.GetSnapshot(sourceLead.keys[key].time)); 21 | var branchKey = branchLead.KeyframeBinarySearch(time); 22 | if (branchKey == -1) return; 23 | if (curveType != CurveTypeValues.Undefined) 24 | branch.ChangeCurveByKey(branchKey, curveType, false); 25 | branch.RecomputeKey(branchKey); 26 | } 27 | 28 | public void AverageToBranch(float keyTime, int fromKey, int toKey) 29 | { 30 | var position = Vector3.zero; 31 | var rotationSum = Vector4.zero; 32 | var targetsRotation = source.targetsRotation; 33 | var firstRotation = targetsRotation ? source.GetKeyframeRotation(fromKey) : Quaternion.identity; 34 | var sourceLead = source.GetLeadCurve(); 35 | var duration = sourceLead.GetKeyframeByKey(toKey).time - sourceLead.GetKeyframeByKey(fromKey).time; 36 | for (var key = fromKey; key < toKey; key++) 37 | { 38 | var frameDuration = sourceLead.GetKeyframeByKey(key + 1).time - sourceLead.GetKeyframeByKey(key).time; 39 | var weight = frameDuration / duration; 40 | position += source.GetKeyframePosition(key) * weight; 41 | if (targetsRotation) QuaternionUtil.AverageQuaternion(ref rotationSum, source.GetKeyframeRotation(key), firstRotation, weight); 42 | } 43 | branch.SetKeyframeByTime(keyTime, position, targetsRotation ? source.GetKeyframeRotation(fromKey) : Quaternion.identity, CurveTypeValues.SmoothLocal); 44 | } 45 | 46 | public void FlattenToBranch(int sectionStart, int sectionEnd) 47 | { 48 | var avgPos = Vector3.zero; 49 | var cumulativeRotation = Vector4.zero; 50 | var firstRotation = source.GetKeyframeRotation(sectionStart); 51 | var div = 0f; 52 | for (var i = sectionStart; i <= sectionEnd; i++) 53 | { 54 | avgPos += source.GetKeyframePosition(i); 55 | QuaternionUtil.AverageQuaternion(ref cumulativeRotation, source.GetKeyframeRotation(i), firstRotation, 1f); 56 | div += 1f; 57 | } 58 | avgPos /= div; 59 | var avgRot = QuaternionUtil.FromVector(cumulativeRotation); 60 | 61 | var sourceLead = source.GetLeadCurve(); 62 | var branchStart = branch.SetKeyframeByTime(sourceLead.GetKeyframeByKey(sectionStart).time, avgPos, avgRot, CurveTypeValues.FlatLinear); 63 | var branchEnd = branch.SetKeyframeByTime(sourceLead.GetKeyframeByKey(sectionEnd).time, avgPos, avgRot, CurveTypeValues.LinearFlat); 64 | branch.RecomputeKey(branchStart); 65 | branch.RecomputeKey(branchEnd); 66 | } 67 | 68 | public bool IsStable(int key1, int key2) 69 | { 70 | var positionDiff = Vector3.Distance( 71 | source.GetKeyframePosition(key1), 72 | source.GetKeyframePosition(key2) 73 | ); 74 | if (positionDiff >= settings.minMeaningfulDistance / 10f) return false; 75 | var rotationDot = 1f - Mathf.Abs(Quaternion.Dot( 76 | source.GetKeyframeRotation(key1), 77 | source.GetKeyframeRotation(key2) 78 | )); 79 | if (rotationDot >= settings.minMeaningfulRotation / 10f) return false; 80 | return true; 81 | } 82 | 83 | public override float GetComparableNormalizedValue(int key) 84 | { 85 | var sourceLead = source.GetLeadCurve(); 86 | var time = sourceLead.keys[key].time; 87 | 88 | var positionDiff = source.targetsPosition ? Vector3.Distance( 89 | branch.EvaluatePosition(time), 90 | source.EvaluatePosition(time) 91 | ) : 0f; 92 | var rotationDot = source.targetsRotation ? 1f - Mathf.Abs(Quaternion.Dot( 93 | branch.EvaluateRotation(time), 94 | source.EvaluateRotation(time) 95 | )) : 0f; 96 | // This is an attempt to compare translations and rotations 97 | // TODO: Normalize the values, investigate how to do this with settings 98 | var normalizedPositionDistance = positionDiff / Mathf.Clamp(settings.minMeaningfulDistance, Mathf.Epsilon, Mathf.Infinity); 99 | var normalizedRotationDot = rotationDot / Mathf.Clamp(settings.minMeaningfulRotation, Mathf.Epsilon, Mathf.Infinity); 100 | var delta = normalizedPositionDistance + normalizedRotationDot; 101 | return delta; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/UI/Components/Zoom/Zoom.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.UI; 3 | 4 | namespace VamTimeline 5 | { 6 | public class Zoom : MonoBehaviour 7 | { 8 | private readonly ZoomStyle _style = new ZoomStyle(); 9 | private readonly ZoomControl _zoomControl; 10 | private readonly ZoomControlGraphics _zoomGraphics; 11 | private readonly ZoomTime _time; 12 | private readonly Text _zoomText; 13 | 14 | private AtomAnimationEditContext _animationEditContext; 15 | 16 | public Zoom() 17 | { 18 | var image = gameObject.AddComponent(); 19 | image.raycastTarget = false; 20 | 21 | var mask = gameObject.AddComponent(); 22 | mask.showMaskGraphic = false; 23 | 24 | CreateBackground(); 25 | _zoomControl = CreateZoomControl(); 26 | _zoomGraphics = CreateZoomGraphics(_zoomControl.gameObject); 27 | _zoomControl.graphics = _zoomGraphics; 28 | _time = CreateTime(); 29 | _zoomText = CreateZoomText(); 30 | } 31 | 32 | public void Bind(AtomAnimationEditContext animationEditContext) 33 | { 34 | _animationEditContext = animationEditContext; 35 | _animationEditContext.onScrubberRangeChanged.AddListener(OnScrubberRangeChanged); 36 | _zoomControl.animationEditContext = _animationEditContext; 37 | _zoomGraphics.animationEditContext = _animationEditContext; 38 | OnScrubberRangeChanged(new AtomAnimationEditContext.ScrubberRangeChangedEventArgs {scrubberRange = _animationEditContext.scrubberRange}); 39 | } 40 | 41 | private void OnDestroy() 42 | { 43 | if (_animationEditContext == null) return; 44 | _animationEditContext.onScrubberRangeChanged.RemoveListener(OnScrubberRangeChanged); 45 | } 46 | 47 | private GameObject CreateBackground() 48 | { 49 | var go = new GameObject(); 50 | go.transform.SetParent(transform, false); 51 | 52 | var rect = go.AddComponent(); 53 | rect.StretchParent(); 54 | 55 | var image = go.AddComponent(); 56 | image.color = _style.BackgroundColor; 57 | image.raycastTarget = false; 58 | 59 | return go; 60 | } 61 | 62 | private ZoomControl CreateZoomControl() 63 | { 64 | var go = new GameObject(); 65 | go.transform.SetParent(transform, false); 66 | 67 | var rect = go.AddComponent(); 68 | rect.StretchParent(); 69 | rect.offsetMin = new Vector2(0, _style.VerticalPadding); 70 | rect.offsetMax = new Vector2(0, -_style.VerticalPadding); 71 | 72 | var control = go.AddComponent(); 73 | control.style = _style; 74 | 75 | return control; 76 | } 77 | 78 | private ZoomControlGraphics CreateZoomGraphics(GameObject go) 79 | { 80 | var graphics = go.AddComponent(); 81 | graphics.raycastTarget = true; 82 | graphics.style = _style; 83 | return graphics; 84 | } 85 | 86 | private ZoomTime CreateTime() 87 | { 88 | var go = new GameObject(); 89 | go.transform.SetParent(transform, false); 90 | 91 | var rect = go.AddComponent(); 92 | rect.StretchParent(); 93 | rect.offsetMin = new Vector2(_style.Padding, _style.VerticalPadding); 94 | rect.offsetMax = new Vector2(-_style.Padding, -_style.VerticalPadding); 95 | 96 | var graphics = go.AddComponent(); 97 | graphics.raycastTarget = false; 98 | graphics.style = _style; 99 | return graphics; 100 | } 101 | 102 | private Text CreateZoomText() 103 | { 104 | var go = new GameObject(); 105 | go.transform.SetParent(transform, false); 106 | 107 | var rect = go.AddComponent(); 108 | rect.StretchParent(); 109 | 110 | var text = go.AddComponent(); 111 | text.text = "100%"; 112 | text.font = _style.Font; 113 | text.fontSize = 20; 114 | text.color = _style.FontColor; 115 | text.alignment = TextAnchor.MiddleCenter; 116 | text.raycastTarget = false; 117 | 118 | return text; 119 | } 120 | 121 | private void OnScrubberRangeChanged(AtomAnimationEditContext.ScrubberRangeChangedEventArgs args) 122 | { 123 | if (_animationEditContext == null) return; 124 | _zoomGraphics.SetVerticesDirty(); 125 | _time.animationLength = _animationEditContext.current.animationLength; 126 | _time.time = _animationEditContext.clipTime; 127 | _time.SetVerticesDirty(); 128 | _zoomText.text = args.scrubberRange.rangeDuration == _animationEditContext.current.animationLength ? "100%" : $"{args.scrubberRange.rangeBegin:0.0}s - {args.scrubberRange.rangeBegin + args.scrubberRange.rangeDuration:0.0}s"; 129 | } 130 | 131 | public void Update() 132 | { 133 | if (!UIPerformance.ShouldRun(UIPerformance.HighFrequency)) return; 134 | if (_time.time == _animationEditContext.clipTime) return; 135 | _time.time = _animationEditContext.clipTime; 136 | _time.SetVerticesDirty(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/AtomAnimations/Animations/AtomAnimation.Building.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace VamTimeline 8 | { 9 | public partial class AtomAnimation 10 | { 11 | #region Animation Rebuilding 12 | 13 | private IEnumerator RebuildDeferred() 14 | { 15 | yield return new WaitForEndOfFrame(); 16 | while (isPlaying) 17 | yield return 0; 18 | RebuildAnimationNow(); 19 | } 20 | 21 | public void RebuildAnimationNow() 22 | { 23 | if (_animationRebuildInProgress) throw new InvalidOperationException("A rebuild is already in progress. This is usually caused by by RebuildAnimation triggering dirty (internal error)."); 24 | _animationRebuildRequestPending = false; 25 | _animationRebuildInProgress = true; 26 | try 27 | { 28 | RebuildAnimationNowImpl(); 29 | } 30 | catch (Exception exc) 31 | { 32 | SuperController.LogError($"Timeline.{nameof(AtomAnimation)}.{nameof(RebuildAnimationNow)}: " + exc); 33 | } 34 | finally 35 | { 36 | _animationRebuildInProgress = false; 37 | } 38 | 39 | onAnimationRebuilt.Invoke(); 40 | } 41 | 42 | private void RebuildAnimationNowImpl() 43 | { 44 | var sw = Stopwatch.StartNew(); 45 | foreach (var layer in index.clipsGroupedByLayer) 46 | { 47 | AtomAnimationClip last = null; 48 | foreach (var clip in layer) 49 | { 50 | clip.Validate(); 51 | clip.Rebuild(last); 52 | last = clip; 53 | } 54 | } 55 | foreach (var clip in clips) 56 | { 57 | RebuildTransition(clip); 58 | } 59 | foreach (var clip in clips) 60 | { 61 | if (!clip.IsDirty()) continue; 62 | 63 | foreach (var target in clip.GetAllTargets()) 64 | { 65 | target.dirty = false; 66 | target.onAnimationKeyframesRebuilt.Invoke(); 67 | } 68 | 69 | clip.onAnimationKeyframesRebuilt.Invoke(); 70 | } 71 | if (sw.ElapsedMilliseconds > 1000) 72 | { 73 | SuperController.LogError($"Timeline.{nameof(RebuildAnimationNowImpl)}: Suspiciously long animation rebuild ({sw.Elapsed})"); 74 | } 75 | } 76 | 77 | private void RebuildTransition(AtomAnimationClip clip) 78 | { 79 | if (clip.autoTransitionPrevious) 80 | { 81 | var previous = index.ByName(clip.animationSegmentId, clip.nextAnimationNameId).FirstOrDefault(); 82 | if (previous != null && (previous.IsDirty() || clip.IsDirty())) 83 | { 84 | CopySourceFrameToClip(previous, previous.animationLength, clip, 0f); 85 | } 86 | } 87 | if (clip.autoTransitionNext) 88 | { 89 | var next = GetClip(clip.animationSegment, clip.animationLayer, clip.nextAnimationName); 90 | if (next != null && (next.IsDirty() || clip.IsDirty())) 91 | { 92 | CopySourceFrameToClip(next, 0f, clip, clip.animationLength); 93 | } 94 | } 95 | } 96 | 97 | private static void CopySourceFrameToClip(AtomAnimationClip source, float sourceTime, AtomAnimationClip clip, float clipTime) 98 | { 99 | foreach (var sourceTarget in source.targetControllers) 100 | { 101 | if (!sourceTarget.EnsureParentAvailable()) continue; 102 | var currentTarget = clip.targetControllers.FirstOrDefault(t => t.TargetsSameAs(sourceTarget)); 103 | if (currentTarget == null) continue; 104 | if (!currentTarget.EnsureParentAvailable()) continue; 105 | // TODO: If there's a parent for position but not rotation or vice versa there will be problems 106 | // ReSharper disable Unity.NoNullCoalescing 107 | var sourceParent = sourceTarget.GetPositionParentRB()?.transform ?? sourceTarget.animatableRef.controller.control.parent; 108 | var currentParent = currentTarget.GetPositionParentRB()?.transform ?? currentTarget.animatableRef.controller.control.parent; 109 | // ReSharper restore Unity.NoNullCoalescing 110 | if (sourceParent == currentParent) 111 | { 112 | currentTarget.SetCurveSnapshot(clipTime, sourceTarget.GetCurveSnapshot(sourceTime), false); 113 | currentTarget.ChangeCurveByTime(clipTime, CurveTypeValues.Linear, false); 114 | } 115 | else 116 | { 117 | var position = sourceParent.TransformPoint(sourceTarget.EvaluatePosition(sourceTime)); 118 | var rotation = Quaternion.Inverse(sourceParent.rotation) * sourceTarget.EvaluateRotation(sourceTime); 119 | currentTarget.SetKeyframeByTime(clipTime, currentParent.TransformPoint(position), Quaternion.Inverse(currentParent.rotation) * rotation, CurveTypeValues.Linear, false); 120 | } 121 | } 122 | foreach (var sourceTarget in source.targetFloatParams) 123 | { 124 | var currentTarget = clip.targetFloatParams.FirstOrDefault(t => t.TargetsSameAs(sourceTarget)); 125 | if (currentTarget == null) continue; 126 | currentTarget.value.SetKeySnapshot(clipTime, sourceTarget.value.GetKeyframeAt(sourceTime)); 127 | currentTarget.ChangeCurveByTime(clipTime, CurveTypeValues.Linear, false); 128 | } 129 | } 130 | 131 | #endregion 132 | } 133 | } 134 | --------------------------------------------------------------------------------