using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace AibisDream.FrameAnimation.Editor { internal enum FrameAnimationPreviewTargetKind { None, Clip, Node, Flow } internal enum FrameAnimationPreviewState { Stopped, Playing, Paused, Failed } internal enum NodePreviewPolicy { Static, SelectedOnly, AllVisible } internal enum FrameAnimationPreviewBackground { Checkerboard, Dark, Light } internal enum FrameAnimationPreviewZoomMode { Fit, One, Two, Three, Four, Eight, Manual } internal enum FrameAnimationFlowElementState { None, Upcoming, Current, Passed } internal readonly struct FrameAnimationPreviewSegment { public int StepIndex { get; } public int FrameIndex { get; } public FrameClip Clip { get; } public string NodeId { get; } public double StartSeconds { get; } public double DurationSeconds { get; } public bool IsBlocked { get; } public FrameAnimationPreviewSegment( int stepIndex, int frameIndex, ResolvedPlaybackStep step, double startSeconds, double durationSeconds, bool isBlocked) { StepIndex = stepIndex; FrameIndex = frameIndex; Clip = step.Clip; NodeId = step.NodeId; StartSeconds = startSeconds; DurationSeconds = durationSeconds; IsBlocked = isBlocked; } } internal sealed class FrameAnimationPreviewTimeline { private const double Epsilon = 0.000000001d; private readonly List segments = new List(); public IReadOnlyList Segments => segments; public double DisplayDurationSeconds { get; } public bool IsInfinite { get; } public bool IsBlocked { get; } public int BlockedSegmentIndex { get; } = -1; public FrameAnimationPreviewTimeline(ResolvedPlaybackPlan plan) { if (plan?.Steps == null || plan.Steps.Count == 0) { return; } var cursor = 0d; var blockedIndex = -1; for (var stepIndex = 0; stepIndex < plan.Steps.Count; stepIndex++) { var step = plan.Steps[stepIndex]; if (step?.Clip == null) { continue; } for (var frameIndex = 0; frameIndex < step.Clip.FrameCount; frameIndex++) { var frame = step.Clip.Frames[frameIndex]; if (step.PlaybackSpeed == 0f) { blockedIndex = segments.Count; segments.Add(new FrameAnimationPreviewSegment( stepIndex, frameIndex, step, cursor, 0d, true)); break; } var duration = frame.DurationMs / 1000d / step.PlaybackSpeed; segments.Add(new FrameAnimationPreviewSegment( stepIndex, frameIndex, step, cursor, duration, false)); cursor += duration; } if (blockedIndex >= 0) { break; } } DisplayDurationSeconds = cursor; BlockedSegmentIndex = blockedIndex; IsBlocked = blockedIndex >= 0; IsInfinite = IsBlocked || plan.Steps[plan.Steps.Count - 1].TerminalEndBehavior == FrameClipEndBehavior.Loop; } public int FindSegmentIndex(double positionSeconds) { if (segments.Count == 0) { return -1; } var position = Math.Max(0d, positionSeconds); for (var index = 0; index < segments.Count; index++) { var segment = segments[index]; if (segment.IsBlocked || position < segment.StartSeconds + segment.DurationSeconds - Epsilon) { return index; } } return segments.Count - 1; } public double GetSeekSeconds(int segmentIndex) { return segmentIndex >= 0 && segmentIndex < segments.Count ? segments[segmentIndex].StartSeconds : 0d; } public double GetPosition(FrameAnimationPlaybackSnapshot snapshot) { if (!snapshot.HasStarted || segments.Count == 0) { return 0d; } var segment = segments.FirstOrDefault(item => item.StepIndex == snapshot.StepIndex && item.FrameIndex == snapshot.FrameIndex); if (segment.Clip == null) { return snapshot.IsCompleted ? DisplayDurationSeconds : 0d; } if (snapshot.IsCompleted && !IsInfinite) { return DisplayDurationSeconds; } var elapsed = snapshot.StepSpeed > 0f ? snapshot.FrameElapsedSeconds / snapshot.StepSpeed : 0d; return Math.Min(DisplayDurationSeconds, segment.StartSeconds + elapsed); } } internal readonly struct FrameAnimationPreviewDisplay { public Sprite Sprite { get; } public bool IsEmpty { get; } public bool IsActive { get; } public FrameAnimationFlowElementState FlowState { get; } public FrameAnimationPreviewDisplay( Sprite sprite, bool isEmpty, bool isActive, FrameAnimationFlowElementState flowState) { Sprite = sprite; IsEmpty = isEmpty; IsActive = isActive; FlowState = flowState; } } internal sealed class FrameAnimationPreviewCoordinator { private sealed class NodeRun { public AnimationNode Node; public FrameAnimationPlaybackSession Session; public Sprite Sprite; } private readonly Dictionary automaticRuns = new Dictionary(); private FrameAnimationGraph graph; private object target; private ResolvedPlaybackPlan plan; private FrameAnimationPlaybackSession session; private Sprite currentSprite; private bool terminalTransparent; private bool suppressSelectedNode; private bool flowPresentationActive; public event Action Changed; public FrameAnimationPreviewTargetKind TargetKind { get; private set; } public FrameAnimationPreviewState State { get; private set; } = FrameAnimationPreviewState.Stopped; public NodePreviewPolicy NodePolicy { get; set; } = NodePreviewPolicy.SelectedOnly; public FrameAnimationPreviewBackground Background { get; set; } = FrameAnimationPreviewBackground.Checkerboard; public FrameAnimationPreviewZoomMode ZoomMode { get; set; } = FrameAnimationPreviewZoomMode.Fit; public float ManualZoom { get; set; } = 1f; public float PreviewSpeed { get; set; } = 1f; public FrameAnimationPreviewTimeline Timeline { get; private set; } public FrameAnimationPlaybackError Error { get; private set; } = FrameAnimationPlaybackError.None; public double PositionSeconds { get; private set; } public string TransitionFromNodeId { get; private set; } = string.Empty; public string TransitionToNodeId { get; private set; } = string.Empty; public object Target => target; public bool IsFlowPreview => TargetKind == FrameAnimationPreviewTargetKind.Flow && flowPresentationActive && State != FrameAnimationPreviewState.Failed; public FrameAnimationPlaybackSnapshot Snapshot => session != null ? session.Snapshot : default; public Sprite CurrentSprite => terminalTransparent ? null : currentSprite; public void SetGraph(FrameAnimationGraph value) { if (ReferenceEquals(graph, value)) { return; } graph = value; ClearTarget(); automaticRuns.Clear(); } public void SetTarget(FrameAnimationPreviewTargetKind kind, object value) { if (TargetKind == kind && ReferenceEquals(target, value)) { return; } TargetKind = kind; target = value; suppressSelectedNode = false; BuildTarget(false, 0d); if (kind == FrameAnimationPreviewTargetKind.Node && NodePolicy != NodePreviewPolicy.Static && session != null) { State = FrameAnimationPreviewState.Playing; } Changed?.Invoke(); } public void ClearTarget() { TargetKind = FrameAnimationPreviewTargetKind.None; target = null; plan = null; session = null; Timeline = null; currentSprite = null; terminalTransparent = false; Error = FrameAnimationPlaybackError.None; PositionSeconds = 0d; State = FrameAnimationPreviewState.Stopped; flowPresentationActive = false; TransitionFromNodeId = string.Empty; TransitionToNodeId = string.Empty; Changed?.Invoke(); } public void RefreshTarget(bool topologyChanged) { if (TargetKind == FrameAnimationPreviewTargetKind.None) { return; } if (topologyChanged && TargetKind == FrameAnimationPreviewTargetKind.Flow) { Stop(); BuildTarget(false, 0d); return; } var wasPlaying = State == FrameAnimationPreviewState.Playing; var position = PositionSeconds; BuildTarget(position > 0d, position); if (wasPlaying && session != null) { State = FrameAnimationPreviewState.Playing; flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow; } Changed?.Invoke(); } public void SetNodePolicy(NodePreviewPolicy value) { NodePolicy = value; automaticRuns.Clear(); suppressSelectedNode = false; if (TargetKind == FrameAnimationPreviewTargetKind.Node) { BuildTarget(false, 0d); if (session != null && value != NodePreviewPolicy.Static) { State = FrameAnimationPreviewState.Playing; } } Changed?.Invoke(); } public void Play() { if (session == null) { BuildTarget(false, 0d); } if (session == null) { return; } if (session.Snapshot.IsCompleted) { session.Restart(ApplySprite); PositionSeconds = 0d; } terminalTransparent = false; suppressSelectedNode = false; flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow; State = FrameAnimationPreviewState.Playing; Changed?.Invoke(); } public void Pause() { if (session == null || State != FrameAnimationPreviewState.Playing) return; State = FrameAnimationPreviewState.Paused; Changed?.Invoke(); } public void Stop() { if (session != null) { session.Restart(ApplySprite); } State = FrameAnimationPreviewState.Stopped; PositionSeconds = 0d; terminalTransparent = false; suppressSelectedNode = TargetKind == FrameAnimationPreviewTargetKind.Node; flowPresentationActive = false; TransitionFromNodeId = string.Empty; TransitionToNodeId = string.Empty; Changed?.Invoke(); } public void Restart() { if (session == null) { BuildTarget(false, 0d); } session?.Restart(ApplySprite); PositionSeconds = 0d; terminalTransparent = false; suppressSelectedNode = false; flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow; if (session != null) State = FrameAnimationPreviewState.Playing; Changed?.Invoke(); } public void Step(int direction) { if (session == null || Timeline == null || Timeline.Segments.Count == 0) return; var current = Timeline.FindSegmentIndex(PositionSeconds); var next = Mathf.Clamp(current + Math.Sign(direction), 0, Timeline.Segments.Count - 1); Seek(Timeline.GetSeekSeconds(next)); } public void Seek(double seconds) { if (session == null || Timeline == null) return; var maximum = Timeline.DisplayDurationSeconds; var targetSeconds = Math.Max(0d, Math.Min(maximum, seconds)); if (Timeline.IsInfinite && maximum > 0d && targetSeconds >= maximum) { targetSeconds = Math.Max(0d, maximum - 0.000001d); } terminalTransparent = false; flowPresentationActive = TargetKind == FrameAnimationPreviewTargetKind.Flow; var result = session.Seek(targetSeconds, ApplySprite); ApplyEvaluation(result); PositionSeconds = targetSeconds; if (State != FrameAnimationPreviewState.Failed) { State = FrameAnimationPreviewState.Paused; } Changed?.Invoke(); } public bool Tick(double deltaSeconds, IReadOnlyCollection visibleNodes) { var changed = false; TransitionFromNodeId = string.Empty; TransitionToNodeId = string.Empty; if (State == FrameAnimationPreviewState.Playing && session != null) { var before = session.Snapshot; var result = session.Evaluate(deltaSeconds, PreviewSpeed, ApplySprite); var after = session.Snapshot; if (before.StepIndex != after.StepIndex) { TransitionFromNodeId = before.NodeId; TransitionToNodeId = after.NodeId; } ApplyEvaluation(result); PositionSeconds = Timeline?.GetPosition(after) ?? 0d; if (result.IsCompleted && TargetKind == FrameAnimationPreviewTargetKind.Node && NodePolicy != NodePreviewPolicy.Static && !suppressSelectedNode && session != null) { session.Restart(ApplySprite); State = FrameAnimationPreviewState.Playing; PositionSeconds = 0d; terminalTransparent = false; } changed = true; } if (!IsFlowPreview) { changed |= TickAutomaticNodes(deltaSeconds, visibleNodes); } else if (automaticRuns.Count > 0) { automaticRuns.Clear(); changed = true; } if (changed) Changed?.Invoke(); return changed; } public FrameAnimationPreviewDisplay GetNodeDisplay(AnimationNode node) { if (node == null) { return default; } if (IsFlowPreview && plan != null) { var current = session?.Snapshot.StepIndex ?? 0; var stepIndex = IndexOfNode(plan, node.InternalId); var flowState = stepIndex < 0 ? FrameAnimationFlowElementState.None : stepIndex < current ? FrameAnimationFlowElementState.Passed : stepIndex == current ? FrameAnimationFlowElementState.Current : FrameAnimationFlowElementState.Upcoming; if (flowState == FrameAnimationFlowElementState.Current) { return new FrameAnimationPreviewDisplay(CurrentSprite, CurrentSprite == null, true, flowState); } return Representative(node, flowState); } if (TargetKind == FrameAnimationPreviewTargetKind.Node && ReferenceEquals(target, node) && session != null) { if (NodePolicy == NodePreviewPolicy.Static && State == FrameAnimationPreviewState.Stopped && !suppressSelectedNode && !session.Snapshot.IsCompleted) { return Representative(node, FrameAnimationFlowElementState.None); } return new FrameAnimationPreviewDisplay(CurrentSprite, CurrentSprite == null, State == FrameAnimationPreviewState.Playing, FrameAnimationFlowElementState.None); } if (automaticRuns.TryGetValue(node.InternalId, out var run)) { return new FrameAnimationPreviewDisplay(run.Sprite, run.Sprite == null, true, FrameAnimationFlowElementState.None); } return Representative(node, FrameAnimationFlowElementState.None); } public FrameAnimationFlowElementState GetEdgeState(AnimationEdge edge) { if (!IsFlowPreview || plan == null || edge == null) return FrameAnimationFlowElementState.None; if (edge.FromNodeId == TransitionFromNodeId && edge.ToNodeId == TransitionToNodeId) { return FrameAnimationFlowElementState.Current; } var from = IndexOfNode(plan, edge.FromNodeId); var to = IndexOfNode(plan, edge.ToNodeId); if (from < 0 || to != from + 1) return FrameAnimationFlowElementState.None; var current = session?.Snapshot.StepIndex ?? 0; return to <= current ? FrameAnimationFlowElementState.Passed : FrameAnimationFlowElementState.Upcoming; } private bool TickAutomaticNodes(double deltaSeconds, IReadOnlyCollection visibleNodes) { IEnumerable desired = Array.Empty(); if (NodePolicy == NodePreviewPolicy.SelectedOnly && TargetKind == FrameAnimationPreviewTargetKind.Node && target is AnimationNode selected && !suppressSelectedNode) { desired = new[] { selected }; } else if (NodePolicy == NodePreviewPolicy.AllVisible) { desired = visibleNodes ?? Array.Empty(); } var desiredNodes = desired.Where(node => node != null && !(TargetKind == FrameAnimationPreviewTargetKind.Node && ReferenceEquals(target, node))) .GroupBy(node => node.InternalId).Select(group => group.First()).ToArray(); var desiredIds = new HashSet(desiredNodes.Select(node => node.InternalId)); foreach (var id in automaticRuns.Keys.Where(id => !desiredIds.Contains(id)).ToArray()) { automaticRuns.Remove(id); } foreach (var node in desiredNodes) { if (!automaticRuns.TryGetValue(node.InternalId, out var run)) { if (!FrameAnimationResolver.TryResolveNode(graph, node, out var nodePlan, out _)) continue; run = new NodeRun { Node = node, Session = new FrameAnimationPlaybackSession(nodePlan) }; run.Session.Start(sprite => run.Sprite = sprite); automaticRuns[node.InternalId] = run; } var evaluation = run.Session.Evaluate(deltaSeconds, PreviewSpeed, sprite => run.Sprite = sprite); if (evaluation.IsCompleted) { run.Session.Restart(sprite => run.Sprite = sprite); } else if (evaluation.IsFailed) { automaticRuns.Remove(node.InternalId); } } return desiredNodes.Length > 0 || automaticRuns.Count > 0; } private void BuildTarget(bool seek, double position) { plan = null; session = null; Timeline = null; currentSprite = null; terminalTransparent = false; PositionSeconds = 0d; Error = FrameAnimationPlaybackError.None; State = FrameAnimationPreviewState.Stopped; flowPresentationActive = false; var resolved = TargetKind switch { FrameAnimationPreviewTargetKind.Clip when target is FrameClip clip => TryResolvePreviewClip(clip, out plan, out var clipError) ? FrameAnimationPlaybackError.None : clipError, FrameAnimationPreviewTargetKind.Node when target is AnimationNode node => FrameAnimationResolver.TryResolveNode(graph, node, out plan, out var nodeError) ? FrameAnimationPlaybackError.None : nodeError, FrameAnimationPreviewTargetKind.Flow when target is AnimationFlow flow => FrameAnimationResolver.TryResolve(graph, flow.Id, default, out plan, out var flowError) ? FrameAnimationPlaybackError.None : flowError, _ => new FrameAnimationPlaybackError(FrameAnimationPlaybackErrorCode.PlayableNotFound, "没有可预览目标。") }; if (resolved.Code != FrameAnimationPlaybackErrorCode.None || plan == null) { Error = resolved; State = TargetKind == FrameAnimationPreviewTargetKind.None ? FrameAnimationPreviewState.Stopped : FrameAnimationPreviewState.Failed; return; } Timeline = new FrameAnimationPreviewTimeline(plan); session = new FrameAnimationPlaybackSession(plan); session.Start(ApplySprite); if (seek && position > 0d) { Seek(position); } } private bool TryResolvePreviewClip( FrameClip clip, out ResolvedPlaybackPlan resolvedPlan, out FrameAnimationPlaybackError error) { return graph != null ? FrameAnimationResolver.TryResolve(graph, clip.Id, default, out resolvedPlan, out error) : FrameAnimationResolver.TryResolveClip(clip, default, out resolvedPlan, out error); } private void ApplyEvaluation(FrameAnimationSessionEvaluation evaluation) { if (evaluation.IsFailed) { Error = evaluation.Error; State = FrameAnimationPreviewState.Failed; flowPresentationActive = false; return; } if (!evaluation.IsCompleted) return; State = FrameAnimationPreviewState.Stopped; terminalTransparent = evaluation.EndBehavior == FrameClipEndBehavior.Clear || evaluation.EndBehavior == FrameClipEndBehavior.HideTarget; } private void ApplySprite(Sprite sprite) { currentSprite = sprite; terminalTransparent = false; } private FrameAnimationPreviewDisplay Representative(AnimationNode node, FrameAnimationFlowElementState flowState) { var clip = graph?.Clips.FirstOrDefault(item => item != null && item.Id == node.ClipId); var sprite = clip?.Frames.FirstOrDefault(frame => frame?.Sprite != null)?.Sprite; return new FrameAnimationPreviewDisplay(sprite, sprite == null, false, flowState); } private static int IndexOfNode(ResolvedPlaybackPlan targetPlan, string nodeId) { if (targetPlan?.Steps == null) return -1; for (var index = 0; index < targetPlan.Steps.Count; index++) { if (targetPlan.Steps[index].NodeId == nodeId) return index; } return -1; } } }