Files
aibis-dream/Assets/Tests/FrameAnimation/EditMode/FrameAnimationPreviewTests.cs
T

434 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using AibisDream.FrameAnimation.Editor;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.UIElements;
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 FlowColorPalette_IsOpaqueAndReadableForExtremeColors()
{
var dark = FrameAnimationFlowColorUtility.CreatePalette(new Color(0.005f, 0.01f, 0.02f, 0f));
var light = FrameAnimationFlowColorUtility.CreatePalette(new Color(1f, 0.98f, 0.9f, 0.1f));
Assert.That(dark.Raw.a, Is.EqualTo(1f));
Assert.That(dark.Stroke.maxColorComponent, Is.GreaterThan(dark.Raw.maxColorComponent));
Assert.That(light.Raw.a, Is.EqualTo(1f));
Assert.That(FrameAnimationFlowColorUtility.Luminance(light.Text),
Is.LessThan(FrameAnimationFlowColorUtility.Luminance(light.Header)));
}
[Test]
public void GraphView_FocusedFlowUsesItsColorAndShowAllKeepsNeutralEdges()
{
var clip = Clip("Clip", 1f, FrameClipEndBehavior.HoldLastFrame, 100);
var first = new AnimationNode(clip.Id, internalId: "first");
var second = new AnimationNode(clip.Id, internalId: "second");
var edge = new AnimationEdge(first.InternalId, second.InternalId, internalId: "edge");
var flow = new AnimationFlow("Flow", "Flow", first.InternalId);
var graph = Graph(new[] { clip }, new[] { first, second }, new[] { edge }, new[] { flow });
var stored = new Color(0.12f, 0.72f, 0.38f, 0.15f);
graph.EditorData.GetOrCreateFlowData(flow.Id, stored);
var palette = FrameAnimationFlowColorUtility.CreatePalette(stored);
var view = new FrameAnimationGraphView();
view.Bind(graph, Array.Empty<FrameAnimationEditorIssue>(), string.Empty);
var edgeView = view.Query<FrameAnimationEdgeView>().First();
Assert.That(edgeView.edgeControl.inputColor.r, Is.EqualTo(119f / 255f).Within(0.001f));
Assert.That(view.Query<Label>(className: "fa-flow-badge").ToList().Count, Is.EqualTo(2));
view.SetFocusedFlow(flow.Id);
Assert.That(edgeView.edgeControl.inputColor.r, Is.EqualTo(palette.Stroke.r).Within(0.003f));
Assert.That(edgeView.edgeControl.inputColor.g, Is.EqualTo(palette.Stroke.g).Within(0.003f));
Assert.That(view.Query<FrameAnimationClipNodeView>(className: "fa-node--focused").ToList().Count,
Is.EqualTo(2));
}
[Test]
public void GraphView_SharedPathSwitchesToNewFocusedFlowColor()
{
var clip = Clip("Clip", 1f, FrameClipEndBehavior.HoldLastFrame, 100);
var first = new AnimationNode(clip.Id, internalId: "first");
var second = new AnimationNode(clip.Id, internalId: "second");
var edge = new AnimationEdge(first.InternalId, second.InternalId, internalId: "edge");
var redFlow = new AnimationFlow("Red", "Red", first.InternalId);
var blueFlow = new AnimationFlow("Blue", "Blue", first.InternalId);
var graph = Graph(new[] { clip }, new[] { first, second }, new[] { edge }, new[] { redFlow, blueFlow });
graph.EditorData.GetOrCreateFlowData(redFlow.Id, Color.red);
graph.EditorData.GetOrCreateFlowData(blueFlow.Id, Color.blue);
var view = new FrameAnimationGraphView();
view.Bind(graph, Array.Empty<FrameAnimationEditorIssue>(), redFlow.Id);
var edgeView = view.Query<FrameAnimationEdgeView>().First();
var red = edgeView.edgeControl.inputColor;
view.SetFocusedFlow(blueFlow.Id);
var blue = edgeView.edgeControl.inputColor;
Assert.That(red.r, Is.GreaterThan(red.b));
Assert.That(blue.b, Is.GreaterThan(blue.r));
}
[Test]
public void GraphView_EdgeValidationColorOverridesFlowAndPreviewColor()
{
var clip = Clip("Clip", 1f, FrameClipEndBehavior.HoldLastFrame, 100);
var first = new AnimationNode(clip.Id, internalId: "first");
var second = new AnimationNode(clip.Id, internalId: "second");
var edge = new AnimationEdge(first.InternalId, second.InternalId, internalId: "edge");
var flow = new AnimationFlow("Flow", "Flow", first.InternalId);
var graph = Graph(new[] { clip }, new[] { first, second }, new[] { edge }, new[] { flow });
graph.EditorData.GetOrCreateFlowData(flow.Id, Color.green);
var issue = new FrameAnimationEditorIssue(FrameAnimationValidationSeverity.Error, "BrokenEdge",
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, edge), "Broken", string.Empty);
var view = new FrameAnimationGraphView();
view.Bind(graph, new[] { issue }, flow.Id);
var edgeView = view.Query<FrameAnimationEdgeView>().First();
edgeView.SetPreviewState(FrameAnimationFlowElementState.Current);
Assert.That(edgeView.edgeControl.inputColor.r, Is.EqualTo(223f / 255f).Within(0.003f));
Assert.That(edgeView.edgeControl.inputColor.g, Is.EqualTo(106f / 255f).Within(0.003f));
}
[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_CreateGUIBuildsTabsAndSelectionPreviewForClipNodeAndFlow()
{
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);
Assert.That(window.rootVisualElement.Q<VisualElement>("canvas-toolbar").Query<Slider>().ToList(),
Is.Empty);
Assert.That(window.rootVisualElement.Query<IMGUIContainer>().ToList(), Is.Empty);
Assert.That(window.rootVisualElement.Q<VisualElement>("fa-workbench"), Is.Not.Null);
var clipsTab = window.rootVisualElement.Q<Button>("resource-tab-clips");
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetResourceTab",
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window,
new[] { clipsTab.userData, (object)false });
Assert.That(clipsTab.ClassListContains("fa-tab--active"), Is.True);
var importDiffTab = window.rootVisualElement.Q<Button>("bottom-tab-import-diff");
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetBottomTab",
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window,
new[] { importDiffTab.userData });
Assert.That(importDiffTab.ClassListContains("fa-tab--active"), Is.True);
var transport = window.rootVisualElement.Q<Toolbar>("preview-transport");
Assert.That(transport, Is.Not.Null);
Assert.That(transport.Q<Button>("preview-restart"), Is.Not.Null);
Assert.That(transport.Q<Button>("preview-previous-frame"), Is.Not.Null);
Assert.That(transport.Q<Button>("preview-play-pause"), Is.Not.Null);
Assert.That(transport.Q<Button>("preview-next-frame"), Is.Not.Null);
Assert.That(transport.Q<Button>("preview-stop"), Is.Not.Null);
Assert.That(transport.Q<Button>("preview-stop").Q<Image>().image, Is.Not.Null);
Assert.That(transport.Q<Slider>("preview-timeline"), Is.Not.Null);
Assert.That(transport.Q<Label>("preview-time"), Is.Not.Null);
Assert.That(transport.Q<Button>("preview-play-pause").enabledSelf, Is.False);
var flowsTab = window.rootVisualElement.Q<Button>("resource-tab-flows");
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetResourceTab",
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window,
new[] { flowsTab.userData, (object)true });
Assert.That(flowsTab.ClassListContains("fa-tab--active"), Is.True);
Assert.That(window.rootVisualElement.Q<Button>("resource-tab-clips")
.ClassListContains("fa-tab--active"), Is.False);
var validationTab = window.rootVisualElement.Q<Button>("bottom-tab-validation");
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetBottomTab",
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window,
new[] { validationTab.userData });
Assert.That(validationTab.ClassListContains("fa-tab--active"), Is.True);
Assert.That(window.rootVisualElement.Q<Button>("bottom-tab-import-diff")
.ClassListContains("fa-tab--active"), Is.False);
var clip = sample.Clips.First(item => item != null);
var setSelection = typeof(FrameAnimationGraphEditorWindow).GetMethod("SetSelection",
BindingFlags.Instance | BindingFlags.NonPublic);
setSelection?.Invoke(window, new object[]
{
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Clip, clip),
false
});
AssertSelectionPreview(window, "Clip Preview");
var playButton = transport.Q<Button>("preview-play-pause");
Assert.That(playButton.enabledSelf, Is.True);
Assert.That(playButton.text, Is.Empty);
Assert.That(playButton.tooltip, Is.EqualTo("Play"));
Assert.That(playButton.Q<Image>(), Is.Not.Null);
Assert.That(playButton.Q<Image>().image, Is.Not.Null);
typeof(FrameAnimationGraphEditorWindow).GetMethod("PreviewPlayPause",
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window, null);
Assert.That(playButton.text, Is.Empty);
Assert.That(playButton.tooltip, Is.EqualTo("Pause"));
Assert.That(playButton.Q<Image>().image, Is.Not.Null);
var node = sample.Nodes.First(item => item != null);
setSelection?.Invoke(window, new object[]
{
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node),
false
});
AssertSelectionPreview(window, "Node Preview");
var flow = sample.Flows.First(item => item != null);
setSelection?.Invoke(window, new object[]
{
new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow),
false
});
AssertSelectionPreview(window, "Flow Preview");
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 static void AssertSelectionPreview(FrameAnimationGraphEditorWindow window, string expectedTitle)
{
var panel = window.rootVisualElement.Q<VisualElement>("selection-preview-panel");
Assert.That(panel, Is.Not.Null);
Assert.That(panel.style.display.value, Is.EqualTo(DisplayStyle.Flex));
Assert.That(window.rootVisualElement.Q<Label>("selection-preview-title").text,
Is.EqualTo(expectedTitle));
}
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;
}
}
}