265 lines
13 KiB
C#
265 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using AibisDream.FrameAnimation.Editor;
|
|
using NUnit.Framework;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace AibisDream.FrameAnimation.Tests.EditMode
|
|
{
|
|
public sealed class FrameAnimationPreviewTests
|
|
{
|
|
private readonly List<UnityEngine.Object> objects = new List<UnityEngine.Object>();
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
foreach (var item in objects.Where(item => item != null))
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(item);
|
|
}
|
|
objects.Clear();
|
|
}
|
|
|
|
[Test]
|
|
public void SessionSnapshotAndSeek_UseTheRuntimeEvaluator()
|
|
{
|
|
var clip = Clip("Clip", 2f, FrameClipEndBehavior.HoldLastFrame, 100, 200, 300);
|
|
var graph = Graph(new[] { clip }, Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>(),
|
|
Array.Empty<AnimationFlow>());
|
|
Assert.That(FrameAnimationResolver.TryResolve(graph, clip.Id, default, out var plan, out _), Is.True);
|
|
var session = new FrameAnimationPlaybackSession(plan);
|
|
session.Start(_ => { });
|
|
|
|
session.Seek(0.075d, _ => { });
|
|
|
|
Assert.That(session.Snapshot.StepIndex, Is.EqualTo(0));
|
|
Assert.That(session.Snapshot.FrameIndex, Is.EqualTo(1));
|
|
Assert.That(session.Snapshot.FrameElapsedSeconds, Is.EqualTo(0.05d).Within(0.000001d));
|
|
Assert.That(session.Snapshot.StepSpeed, Is.EqualTo(2f));
|
|
}
|
|
|
|
[Test]
|
|
public void NodeResolver_AppliesNodeOverridesWithoutChangingClip()
|
|
{
|
|
var clip = Clip("Clip", 0.5f, FrameClipEndBehavior.HoldLastFrame, 100);
|
|
var node = new AnimationNode(clip.Id, speedOverride: 3f,
|
|
endBehaviorOverride: FrameClipEndBehavior.HideTarget);
|
|
var graph = Graph(new[] { clip }, new[] { node }, Array.Empty<AnimationEdge>(),
|
|
Array.Empty<AnimationFlow>());
|
|
|
|
Assert.That(FrameAnimationResolver.TryResolveNode(graph, node, out var plan, out var error),
|
|
Is.True, error.ToString());
|
|
Assert.That(plan.Steps.Single().PlaybackSpeed, Is.EqualTo(3f));
|
|
Assert.That(plan.Steps.Single().TerminalEndBehavior, Is.EqualTo(FrameClipEndBehavior.HideTarget));
|
|
Assert.That(plan.Steps.Single().NodeId, Is.EqualTo(node.InternalId));
|
|
Assert.That(clip.Speed, Is.EqualTo(0.5f));
|
|
}
|
|
|
|
[Test]
|
|
public void Timeline_FlattensFlowUsingEffectiveStepDurations()
|
|
{
|
|
var first = Clip("First", 1f, FrameClipEndBehavior.HoldLastFrame, 100, 100);
|
|
var second = Clip("Second", 1f, FrameClipEndBehavior.HoldLastFrame, 300);
|
|
var firstNode = new AnimationNode(first.Id, speedOverride: 2f);
|
|
var secondNode = new AnimationNode(second.Id, speedOverride: 0.5f);
|
|
var edge = new AnimationEdge(firstNode.InternalId, secondNode.InternalId);
|
|
var flow = new AnimationFlow("Flow", "Flow", firstNode.InternalId);
|
|
var graph = Graph(new[] { first, second }, new[] { firstNode, secondNode }, new[] { edge }, new[] { flow });
|
|
Assert.That(FrameAnimationResolver.TryResolve(graph, flow.Id, default, out var plan, out _), Is.True);
|
|
|
|
var timeline = new FrameAnimationPreviewTimeline(plan);
|
|
|
|
Assert.That(timeline.Segments.Count, Is.EqualTo(3));
|
|
Assert.That(timeline.Segments[2].NodeId, Is.EqualTo(secondNode.InternalId));
|
|
Assert.That(timeline.Segments[2].StartSeconds, Is.EqualTo(0.1d).Within(0.000001d));
|
|
Assert.That(timeline.DisplayDurationSeconds, Is.EqualTo(0.7d).Within(0.000001d));
|
|
Assert.That(timeline.IsInfinite, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void Timeline_LoopUsesOneCycle_AndZeroSpeedStopsReachability()
|
|
{
|
|
var loopClip = Clip("Loop", 1f, FrameClipEndBehavior.Loop, 100, 200);
|
|
var loopNode = new AnimationNode(loopClip.Id, endBehaviorOverride: FrameClipEndBehavior.Loop);
|
|
var loopFlow = new AnimationFlow("LoopFlow", "Loop", loopNode.InternalId);
|
|
var loopGraph = Graph(new[] { loopClip }, new[] { loopNode }, Array.Empty<AnimationEdge>(), new[] { loopFlow });
|
|
FrameAnimationResolver.TryResolve(loopGraph, loopFlow.Id, default, out var loopPlan, out _);
|
|
var loopTimeline = new FrameAnimationPreviewTimeline(loopPlan);
|
|
|
|
Assert.That(loopTimeline.IsInfinite, Is.True);
|
|
Assert.That(loopTimeline.Segments.Count, Is.EqualTo(2));
|
|
Assert.That(loopTimeline.DisplayDurationSeconds, Is.EqualTo(0.3d).Within(0.000001d));
|
|
|
|
var blockedClip = Clip("Blocked", 1f, FrameClipEndBehavior.HoldLastFrame, 100, 100);
|
|
var blockedNode = new AnimationNode(blockedClip.Id, speedOverride: 0f);
|
|
var blockedGraph = Graph(new[] { blockedClip }, new[] { blockedNode }, Array.Empty<AnimationEdge>(),
|
|
Array.Empty<AnimationFlow>());
|
|
FrameAnimationResolver.TryResolveNode(blockedGraph, blockedNode, out var blockedPlan, out _);
|
|
var blockedTimeline = new FrameAnimationPreviewTimeline(blockedPlan);
|
|
|
|
Assert.That(blockedTimeline.IsBlocked, Is.True);
|
|
Assert.That(blockedTimeline.IsInfinite, Is.True);
|
|
Assert.That(blockedTimeline.Segments.Count, Is.EqualTo(1));
|
|
Assert.That(blockedTimeline.Segments[0].IsBlocked, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void Coordinator_FlowPreviewCrossesNodesAndRestoresAutomaticPolicyOnStop()
|
|
{
|
|
var first = Clip("First", 1f, FrameClipEndBehavior.HoldLastFrame, 100);
|
|
var second = Clip("Second", 1f, FrameClipEndBehavior.Loop, 100);
|
|
var firstNode = new AnimationNode(first.Id);
|
|
var secondNode = new AnimationNode(second.Id, endBehaviorOverride: FrameClipEndBehavior.Loop);
|
|
var edge = new AnimationEdge(firstNode.InternalId, secondNode.InternalId);
|
|
var flow = new AnimationFlow("Flow", "Flow", firstNode.InternalId);
|
|
var graph = Graph(new[] { first, second }, new[] { firstNode, secondNode }, new[] { edge }, new[] { flow });
|
|
var coordinator = new FrameAnimationPreviewCoordinator();
|
|
coordinator.SetGraph(graph);
|
|
coordinator.SetTarget(FrameAnimationPreviewTargetKind.Flow, flow);
|
|
|
|
coordinator.Play();
|
|
coordinator.Tick(0.1d, graph.Nodes);
|
|
|
|
Assert.That(coordinator.IsFlowPreview, Is.True);
|
|
Assert.That(coordinator.Snapshot.NodeId, Is.EqualTo(secondNode.InternalId));
|
|
Assert.That(coordinator.GetNodeDisplay(firstNode).FlowState, Is.EqualTo(FrameAnimationFlowElementState.Passed));
|
|
Assert.That(coordinator.GetNodeDisplay(secondNode).FlowState, Is.EqualTo(FrameAnimationFlowElementState.Current));
|
|
|
|
coordinator.Stop();
|
|
Assert.That(coordinator.IsFlowPreview, Is.False);
|
|
Assert.That(coordinator.NodePolicy, Is.EqualTo(NodePreviewPolicy.SelectedOnly));
|
|
}
|
|
|
|
[Test]
|
|
public void Coordinator_FlowSeekAndStepCrossNodeBoundaries()
|
|
{
|
|
var first = Clip("First", 1f, FrameClipEndBehavior.HoldLastFrame, 100);
|
|
var second = Clip("Second", 1f, FrameClipEndBehavior.HoldLastFrame, 200, 200);
|
|
var firstNode = new AnimationNode(first.Id);
|
|
var secondNode = new AnimationNode(second.Id);
|
|
var flow = new AnimationFlow("Flow", "Flow", firstNode.InternalId);
|
|
var graph = Graph(new[] { first, second }, new[] { firstNode, secondNode },
|
|
new[] { new AnimationEdge(firstNode.InternalId, secondNode.InternalId) }, new[] { flow });
|
|
var coordinator = new FrameAnimationPreviewCoordinator();
|
|
coordinator.SetGraph(graph);
|
|
coordinator.SetTarget(FrameAnimationPreviewTargetKind.Flow, flow);
|
|
|
|
coordinator.Seek(0.1d);
|
|
Assert.That(coordinator.Snapshot.NodeId, Is.EqualTo(secondNode.InternalId));
|
|
Assert.That(coordinator.Snapshot.FrameIndex, Is.EqualTo(0));
|
|
coordinator.Step(1);
|
|
Assert.That(coordinator.Snapshot.NodeId, Is.EqualTo(secondNode.InternalId));
|
|
Assert.That(coordinator.Snapshot.FrameIndex, Is.EqualTo(1));
|
|
coordinator.Step(-1);
|
|
Assert.That(coordinator.Snapshot.FrameIndex, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void GraphViewPreview_RendersOnePreviewPerNodeWithoutMutatingGraph()
|
|
{
|
|
var clip = Clip("Clip", 1f, FrameClipEndBehavior.Loop, 100);
|
|
var first = new AnimationNode(clip.Id);
|
|
var second = new AnimationNode(clip.Id);
|
|
var graph = Graph(new[] { clip }, new[] { first, second }, Array.Empty<AnimationEdge>(),
|
|
Array.Empty<AnimationFlow>());
|
|
var before = JsonUtility.ToJson(graph);
|
|
var view = new FrameAnimationGraphView();
|
|
view.Bind(graph, Array.Empty<FrameAnimationEditorIssue>(), string.Empty);
|
|
var coordinator = new FrameAnimationPreviewCoordinator();
|
|
coordinator.SetGraph(graph);
|
|
coordinator.SetNodePolicy(NodePreviewPolicy.AllVisible);
|
|
|
|
view.ApplyPreview(coordinator);
|
|
|
|
Assert.That(view.Query<FrameAnimationPreviewElement>().ToList().Count, Is.EqualTo(2));
|
|
Assert.That(JsonUtility.ToJson(graph), Is.EqualTo(before));
|
|
}
|
|
|
|
[Test]
|
|
public void PreviewSample_IsIsolatedAndStructurallyPlayable()
|
|
{
|
|
var sample = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(
|
|
"Assets/GameContent/Test/FrameAnimation/Preview/PreviewSampleGraph.asset");
|
|
|
|
Assert.That(sample, Is.Not.Null);
|
|
Assert.That(sample.Clips.Count, Is.EqualTo(3));
|
|
Assert.That(sample.Flows.Count, Is.EqualTo(5));
|
|
Assert.That(sample.Clips.Any(item => item.Frames.Any(frame => frame.Sprite == null)), Is.True);
|
|
foreach (var flow in sample.Flows)
|
|
{
|
|
Assert.That(FrameAnimationResolver.TryResolve(sample, flow.Id, default, out _, out var error),
|
|
Is.True, error.ToString());
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Workbench_CreateGUIBuildsSharedAndClipPreviewElements()
|
|
{
|
|
var sample = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(
|
|
"Assets/GameContent/Test/FrameAnimation/Preview/PreviewSampleGraph.asset");
|
|
var window = ScriptableObject.CreateInstance<FrameAnimationGraphEditorWindow>();
|
|
try
|
|
{
|
|
typeof(FrameAnimationGraphEditorWindow).GetField("graph",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(window, sample);
|
|
typeof(FrameAnimationGraphEditorWindow).GetMethod("CreateGUI",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window, null);
|
|
|
|
Assert.That(window.rootVisualElement.Query<FrameAnimationPreviewElement>().ToList().Count,
|
|
Is.GreaterThanOrEqualTo(1));
|
|
Assert.That(window.rootVisualElement.Query<Slider>().ToList().Count,
|
|
Is.GreaterThanOrEqualTo(2));
|
|
Assert.That(window.rootVisualElement.Q<Button>("graph-properties-button"), Is.Not.Null);
|
|
Assert.That(window.rootVisualElement.Q<Button>("graph-breadcrumb-button"), Is.Not.Null);
|
|
|
|
var clip = sample.Clips.First(item => item != null);
|
|
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetSelection",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window, new object[]
|
|
{
|
|
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
|
|
false
|
|
});
|
|
typeof(FrameAnimationGraphEditorWindow).GetMethod("SelectGraphProperties",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window, null);
|
|
var selected = (FrameAnimationEditorSelection)typeof(FrameAnimationGraphEditorWindow)
|
|
.GetField("selection", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(window);
|
|
Assert.That(selected.Kind, Is.EqualTo(FrameAnimationEditorSelectionKind.Graph));
|
|
Assert.That(selected.Value, Is.SameAs(sample));
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(window);
|
|
}
|
|
}
|
|
|
|
private FrameClip Clip(string id, float speed, FrameClipEndBehavior end, params int[] durations)
|
|
{
|
|
var clip = Track(ScriptableObject.CreateInstance<FrameClip>());
|
|
clip.Configure(id, id, durations.Select((duration, index) =>
|
|
new FrameAnimationFrame(null, duration, $"frame_{index}", index)), speed, end);
|
|
return clip;
|
|
}
|
|
|
|
private FrameAnimationGraph Graph(
|
|
IEnumerable<FrameClip> clips,
|
|
IEnumerable<AnimationNode> nodes,
|
|
IEnumerable<AnimationEdge> edges,
|
|
IEnumerable<AnimationFlow> flows)
|
|
{
|
|
var graph = Track(ScriptableObject.CreateInstance<FrameAnimationGraph>());
|
|
graph.Configure("Graph", "Graph", clips, nodes, edges, flows, string.Empty);
|
|
return graph;
|
|
}
|
|
|
|
private T Track<T>(T value) where T : UnityEngine.Object
|
|
{
|
|
objects.Add(value);
|
|
return value;
|
|
}
|
|
}
|
|
}
|