feat: 帧动画编辑器美化
This commit is contained in:
@@ -12,10 +12,12 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
private readonly Label subtitle;
|
||||
private readonly Label behavior;
|
||||
private readonly Label flowBadge;
|
||||
private readonly Label issueBadge;
|
||||
private readonly Label entryBadge;
|
||||
private readonly VisualElement badgeContainer;
|
||||
private readonly FrameAnimationPreviewElement preview;
|
||||
private readonly Action<FrameAnimationClipNodeView> selectedCallback;
|
||||
private FrameAnimationFlowColorPalette? entryPalette;
|
||||
|
||||
public AnimationNode Data { get; }
|
||||
public Port InputPort { get; }
|
||||
@@ -29,30 +31,40 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
this.selectedCallback = selectedCallback;
|
||||
viewDataKey = data.InternalId;
|
||||
AddToClassList("fa-node");
|
||||
style.minWidth = 245f;
|
||||
style.maxWidth = 285f;
|
||||
|
||||
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi,
|
||||
typeof(AnimationNode));
|
||||
InputPort.portName = "In";
|
||||
InputPort.portName = string.Empty;
|
||||
inputContainer.Add(InputPort);
|
||||
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single,
|
||||
typeof(AnimationNode));
|
||||
OutputPort.portName = "Next";
|
||||
OutputPort.portName = string.Empty;
|
||||
outputContainer.Add(OutputPort);
|
||||
|
||||
subtitle = new Label();
|
||||
subtitle.AddToClassList("fa-node__clip-id");
|
||||
behavior = new Label();
|
||||
flowBadge = new Label();
|
||||
issueBadge = new Label();
|
||||
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
extensionContainer.Add(subtitle);
|
||||
extensionContainer.Add(behavior);
|
||||
extensionContainer.Add(flowBadge);
|
||||
extensionContainer.Add(issueBadge);
|
||||
behavior.AddToClassList("fa-node__behavior");
|
||||
var meta = new VisualElement();
|
||||
meta.AddToClassList("fa-node__meta");
|
||||
meta.Add(subtitle);
|
||||
meta.Add(behavior);
|
||||
extensionContainer.Add(meta);
|
||||
|
||||
preview = new FrameAnimationPreviewElement();
|
||||
preview.AddToClassList("fa-node__preview");
|
||||
extensionContainer.Add(preview);
|
||||
|
||||
badgeContainer = new VisualElement();
|
||||
badgeContainer.AddToClassList("fa-node__badges");
|
||||
entryBadge = new Label("E") { tooltip = "Flow Entry" };
|
||||
entryBadge.AddToClassList("fa-node__entry");
|
||||
badgeContainer.Add(entryBadge);
|
||||
issueBadge = new Label();
|
||||
issueBadge.AddToClassList("fa-badge");
|
||||
issueBadge.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
badgeContainer.Add(issueBadge);
|
||||
extensionContainer.Add(badgeContainer);
|
||||
RefreshExpandedState();
|
||||
}
|
||||
|
||||
@@ -65,28 +77,67 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
internal void Refresh(
|
||||
FrameAnimationGraph graph,
|
||||
IReadOnlyList<FrameAnimationEditorIssue> issues,
|
||||
IReadOnlyList<AnimationFlow> flows,
|
||||
IReadOnlyCollection<string> entryFlowIds)
|
||||
IReadOnlyList<AnimationFlow> flows)
|
||||
{
|
||||
var clip = graph.Clips.FirstOrDefault(item => item != null && item.Id == Data.ClipId);
|
||||
title = string.IsNullOrWhiteSpace(Data.DisplayName) ? Data.ClipId : Data.DisplayName;
|
||||
subtitle.text = clip != null ? $"Clip: {Data.ClipId}" : $"Clip: <Missing {Data.ClipId}>";
|
||||
subtitle.text = clip != null ? Data.ClipId : $"Missing: {Data.ClipId}";
|
||||
var speed = Data.SpeedOverride.HasValue
|
||||
? $"Speed {Data.SpeedOverride.Value:0.###} (Node)"
|
||||
: clip != null ? $"Speed {clip.Speed:0.###} (Clip)" : "Speed ?";
|
||||
var end = Data.EndBehaviorOverride?.ToString() ?? "Continue / terminal fallback";
|
||||
behavior.text = $"{speed} · {end}";
|
||||
var used = flows.Select(flow => flow.Id).ToArray();
|
||||
var entries = used.Where(entryFlowIds.Contains).ToArray();
|
||||
flowBadge.text = used.Length == 0
|
||||
? "Unused"
|
||||
: "Flows: " + string.Join(", ", used.Take(3)) + (used.Length > 3 ? $" +{used.Length - 3}" : string.Empty) +
|
||||
(entries.Length > 0 ? " [ENTRY]" : string.Empty);
|
||||
var used = flows.Where(flow => flow != null).ToArray();
|
||||
var entryFlows = used.Where(flow => flow.EntryNodeId == Data.InternalId).ToArray();
|
||||
entryPalette = entryFlows.Length > 0
|
||||
? FrameAnimationFlowColorUtility.CreatePalette(
|
||||
FrameAnimationFlowColorUtility.ResolveRaw(graph, entryFlows[0]))
|
||||
: (FrameAnimationFlowColorPalette?)null;
|
||||
entryBadge.style.display = entryPalette.HasValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (entryPalette.HasValue)
|
||||
{
|
||||
entryBadge.style.backgroundColor = entryPalette.Value.Stroke;
|
||||
entryBadge.style.color = entryPalette.Value.Text;
|
||||
entryBadge.tooltip = "Flow Entry: " + string.Join(", ", entryFlows.Select(flow => flow.Id));
|
||||
}
|
||||
|
||||
foreach (var label in badgeContainer.Query<Label>(className: "fa-flow-badge").ToList())
|
||||
{
|
||||
label.RemoveFromHierarchy();
|
||||
}
|
||||
if (used.Length == 0)
|
||||
{
|
||||
var unused = new Label("UNUSED");
|
||||
unused.AddToClassList("fa-badge");
|
||||
unused.AddToClassList("fa-badge--yellow");
|
||||
unused.AddToClassList("fa-flow-badge");
|
||||
badgeContainer.Insert(entryPalette.HasValue ? 1 : 0, unused);
|
||||
}
|
||||
else
|
||||
{
|
||||
var insertIndex = entryPalette.HasValue ? 1 : 0;
|
||||
foreach (var flow in used.Take(3))
|
||||
{
|
||||
var palette = FrameAnimationFlowColorUtility.CreatePalette(
|
||||
FrameAnimationFlowColorUtility.ResolveRaw(graph, flow));
|
||||
var badge = new Label(flow.Id) { tooltip = FlowTooltip(flow) };
|
||||
badge.AddToClassList("fa-badge");
|
||||
badge.AddToClassList("fa-flow-badge");
|
||||
badge.style.backgroundColor = palette.Muted;
|
||||
badge.style.color = palette.Text;
|
||||
badgeContainer.Insert(insertIndex++, badge);
|
||||
}
|
||||
}
|
||||
var matching = issues.Where(issue => issue.Selection.Kind == FrameAnimationEditorSelectionKind.Node &&
|
||||
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
|
||||
issueBadge.text = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? "ERROR"
|
||||
: matching.Length > 0 ? "WARNING" : string.Empty;
|
||||
issueBadge.style.display = matching.Length > 0 ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
issueBadge.EnableInClassList("fa-badge--red",
|
||||
matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error));
|
||||
issueBadge.EnableInClassList("fa-badge--yellow",
|
||||
matching.Length > 0 && matching.All(issue => issue.Severity != FrameAnimationValidationSeverity.Error));
|
||||
issueBadge.style.color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? new StyleColor(new Color(1f, 0.35f, 0.3f))
|
||||
: new StyleColor(new Color(1f, 0.72f, 0.25f));
|
||||
@@ -96,6 +147,30 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
: "创建唯一的顺序后继";
|
||||
}
|
||||
|
||||
private static string FlowTooltip(AnimationFlow flow) =>
|
||||
$"Flow: {flow.DisplayName} ({flow.Id})";
|
||||
|
||||
internal void SetFocusedFlow(FrameAnimationGraph graph, AnimationFlow flow, bool belongs)
|
||||
{
|
||||
EnableInClassList("fa-node--focused", flow != null && belongs);
|
||||
if (flow != null && belongs)
|
||||
{
|
||||
var palette = FrameAnimationFlowColorUtility.CreatePalette(
|
||||
FrameAnimationFlowColorUtility.ResolveRaw(graph, flow));
|
||||
titleContainer.style.backgroundColor = palette.Header;
|
||||
var titleLabel = titleContainer.Q<Label>(className: "title-label");
|
||||
if (titleLabel != null) titleLabel.style.color = palette.Text;
|
||||
style.borderLeftColor = palette.Stroke;
|
||||
}
|
||||
else
|
||||
{
|
||||
titleContainer.style.backgroundColor = StyleKeyword.Null;
|
||||
var titleLabel = titleContainer.Q<Label>(className: "title-label");
|
||||
if (titleLabel != null) titleLabel.style.color = StyleKeyword.Null;
|
||||
style.borderLeftColor = StyleKeyword.Null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetDimmed(bool dimmed)
|
||||
{
|
||||
style.opacity = dimmed ? 0.45f : 1f;
|
||||
@@ -119,7 +194,10 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
internal sealed class FrameAnimationEdgeView : Edge
|
||||
{
|
||||
private readonly Action<FrameAnimationEdgeView> selectedCallback;
|
||||
private Color baseColor = Color.white;
|
||||
private Color baseColor = new Color32(119, 127, 135, 255);
|
||||
private bool hasError;
|
||||
private bool hasWarning;
|
||||
private FrameAnimationFlowElementState previewState;
|
||||
public AnimationEdge Data { get; }
|
||||
|
||||
internal FrameAnimationEdgeView(AnimationEdge data, Action<FrameAnimationEdgeView> selectedCallback)
|
||||
@@ -128,6 +206,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
this.selectedCallback = selectedCallback;
|
||||
viewDataKey = data.InternalId;
|
||||
userData = data;
|
||||
RegisterCallback<AttachToPanelEvent>(_ => ApplyVisualState());
|
||||
}
|
||||
|
||||
public override void OnSelected()
|
||||
@@ -146,12 +225,9 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
var matching = (issues ?? Array.Empty<FrameAnimationEditorIssue>()).Where(issue =>
|
||||
issue.Selection.Kind == FrameAnimationEditorSelectionKind.Edge &&
|
||||
ReferenceEquals(issue.Selection.Value, Data)).ToArray();
|
||||
var color = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error)
|
||||
? new Color(1f, 0.3f, 0.25f)
|
||||
: matching.Length > 0 ? new Color(1f, 0.72f, 0.2f) : Color.white;
|
||||
baseColor = color;
|
||||
edgeControl.inputColor = color;
|
||||
edgeControl.outputColor = color;
|
||||
hasError = matching.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error);
|
||||
hasWarning = !hasError && matching.Length > 0;
|
||||
ApplyVisualState();
|
||||
tooltip = matching.Length == 0
|
||||
? $"{Data.ExitName} / {Data.Condition}"
|
||||
: string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}"));
|
||||
@@ -159,25 +235,47 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
|
||||
internal void SetPreviewState(FrameAnimationFlowElementState state)
|
||||
{
|
||||
if (state == FrameAnimationFlowElementState.Current)
|
||||
previewState = state;
|
||||
ApplyVisualState();
|
||||
}
|
||||
|
||||
internal void SetFocusedFlow(Color? flowColor, bool belongs)
|
||||
{
|
||||
baseColor = flowColor.HasValue && belongs
|
||||
? FrameAnimationFlowColorUtility.CreatePalette(flowColor.Value).Stroke
|
||||
: new Color32(119, 127, 135, 255);
|
||||
ApplyVisualState();
|
||||
}
|
||||
|
||||
private void ApplyVisualState()
|
||||
{
|
||||
var color = hasError
|
||||
? new Color32(223, 106, 106, 255)
|
||||
: hasWarning ? new Color32(224, 184, 88, 255) : baseColor;
|
||||
var width = 3f;
|
||||
if (!hasError && !hasWarning)
|
||||
{
|
||||
edgeControl.inputColor = new Color(0.25f, 0.75f, 1f);
|
||||
edgeControl.outputColor = new Color(0.25f, 0.75f, 1f);
|
||||
if (previewState == FrameAnimationFlowElementState.Current)
|
||||
{
|
||||
color = Color.Lerp(color, Color.white, 0.28f);
|
||||
width = 5f;
|
||||
}
|
||||
else if (previewState == FrameAnimationFlowElementState.Passed)
|
||||
{
|
||||
color = Color.Lerp(color, new Color32(86, 185, 123, 255), 0.22f);
|
||||
width = 3.5f;
|
||||
}
|
||||
else if (previewState == FrameAnimationFlowElementState.Upcoming)
|
||||
{
|
||||
color = Color.Lerp(color, new Color32(43, 47, 52, 255), 0.42f);
|
||||
width = 2f;
|
||||
}
|
||||
}
|
||||
else if (state == FrameAnimationFlowElementState.Passed)
|
||||
edgeControl.inputColor = color;
|
||||
edgeControl.outputColor = color;
|
||||
if (panel != null)
|
||||
{
|
||||
edgeControl.inputColor = new Color(0.35f, 0.68f, 0.46f);
|
||||
edgeControl.outputColor = new Color(0.35f, 0.68f, 0.46f);
|
||||
}
|
||||
else if (state == FrameAnimationFlowElementState.Upcoming)
|
||||
{
|
||||
edgeControl.inputColor = new Color(0.55f, 0.55f, 0.55f);
|
||||
edgeControl.outputColor = new Color(0.55f, 0.55f, 0.55f);
|
||||
}
|
||||
else
|
||||
{
|
||||
edgeControl.inputColor = baseColor;
|
||||
edgeControl.outputColor = baseColor;
|
||||
edgeControl.edgeWidth = Mathf.RoundToInt(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,14 +383,12 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
if (graph != null)
|
||||
{
|
||||
var topology = new FrameAnimationGraphTopology(graph);
|
||||
var entries = new HashSet<string>(graph.Flows.Where(flow => flow != null)
|
||||
.Select(flow => flow.EntryNodeId));
|
||||
foreach (var node in graph.Nodes.Where(node => node != null))
|
||||
{
|
||||
var view = new FrameAnimationClipNodeView(node, OnNodeSelected);
|
||||
view.SetPosition(new Rect(FrameAnimationGraphMutationService.GetNodePosition(graph, node),
|
||||
new Vector2(255f, 150f)));
|
||||
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId), entries);
|
||||
new Vector2(224f, 164f)));
|
||||
view.Refresh(graph, issues, topology.FindFlowsUsingNode(node.InternalId));
|
||||
nodeViews[node.InternalId] = view;
|
||||
AddElement(view);
|
||||
}
|
||||
@@ -372,8 +468,16 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
focusedFlowId = flowId ?? string.Empty;
|
||||
if (graph == null || string.IsNullOrEmpty(flowId))
|
||||
{
|
||||
foreach (var view in nodeViews.Values) view.SetDimmed(false);
|
||||
foreach (var view in edgeViews.Values) view.SetDimmed(false);
|
||||
foreach (var view in nodeViews.Values)
|
||||
{
|
||||
view.SetDimmed(false);
|
||||
view.SetFocusedFlow(graph, null, false);
|
||||
}
|
||||
foreach (var view in edgeViews.Values)
|
||||
{
|
||||
view.SetDimmed(false);
|
||||
view.SetFocusedFlow(null, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == flowId);
|
||||
@@ -383,8 +487,19 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
: new FrameAnimationReachability(Array.Empty<AnimationNode>(), Array.Empty<AnimationEdge>());
|
||||
var nodeIds = new HashSet<string>(reachable.Nodes.Select(node => node.InternalId));
|
||||
var edgeIds = new HashSet<string>(reachable.Edges.Select(edge => edge.InternalId));
|
||||
foreach (var pair in nodeViews) pair.Value.SetDimmed(!nodeIds.Contains(pair.Key));
|
||||
foreach (var pair in edgeViews) pair.Value.SetDimmed(!edgeIds.Contains(pair.Key));
|
||||
var flowColor = flow != null ? FrameAnimationFlowColorUtility.ResolveRaw(graph, flow) : (Color?)null;
|
||||
foreach (var pair in nodeViews)
|
||||
{
|
||||
var belongs = nodeIds.Contains(pair.Key);
|
||||
pair.Value.SetDimmed(!belongs);
|
||||
pair.Value.SetFocusedFlow(graph, flow, belongs);
|
||||
}
|
||||
foreach (var pair in edgeViews)
|
||||
{
|
||||
var belongs = edgeIds.Contains(pair.Key);
|
||||
pair.Value.SetDimmed(!belongs);
|
||||
pair.Value.SetFocusedFlow(flowColor, belongs);
|
||||
}
|
||||
}
|
||||
|
||||
internal void SelectAndFrame(FrameAnimationEditorSelection target)
|
||||
|
||||
Reference in New Issue
Block a user