feat: 动画系统第五阶段(未验收)

This commit is contained in:
2026-07-16 18:49:12 +08:00
parent 9b6d008263
commit 46602803dc
23 changed files with 2591 additions and 116 deletions
@@ -14,6 +14,7 @@ namespace AibisDream.FrameAnimation.Editor
private readonly Label behavior;
private readonly Label flowBadge;
private readonly Label issueBadge;
private readonly FrameAnimationPreviewElement preview;
private readonly Action<FrameAnimationClipNodeView> selectedCallback;
public AnimationNode Data { get; }
@@ -27,6 +28,7 @@ namespace AibisDream.FrameAnimation.Editor
Data = data;
this.selectedCallback = selectedCallback;
viewDataKey = data.InternalId;
AddToClassList("fa-node");
style.minWidth = 245f;
style.maxWidth = 285f;
@@ -48,16 +50,9 @@ namespace AibisDream.FrameAnimation.Editor
extensionContainer.Add(behavior);
extensionContainer.Add(flowBadge);
extensionContainer.Add(issueBadge);
extensionContainer.Add(new Label("Preview · Phase 5")
{
style =
{
height = 72f,
unityTextAlign = TextAnchor.MiddleCenter,
backgroundColor = new StyleColor(new Color(0.12f, 0.12f, 0.12f, 0.8f)),
marginTop = 5f
}
});
preview = new FrameAnimationPreviewElement();
preview.AddToClassList("fa-node__preview");
extensionContainer.Add(preview);
RefreshExpandedState();
}
@@ -103,13 +98,28 @@ namespace AibisDream.FrameAnimation.Editor
internal void SetDimmed(bool dimmed)
{
style.opacity = dimmed ? 0.22f : 1f;
style.opacity = dimmed ? 0.45f : 1f;
}
internal void SetPreview(
FrameAnimationPreviewDisplay display,
FrameAnimationPreviewBackground background)
{
preview.SetBackground(background);
preview.SetZoom(FrameAnimationPreviewZoomMode.Fit, 1f);
preview.SetDisplay(display.Sprite, display.IsEmpty);
preview.style.opacity = display.FlowState == FrameAnimationFlowElementState.Upcoming
? 0.55f : display.FlowState == FrameAnimationFlowElementState.Passed ? 0.82f : 1f;
EnableInClassList("fa-node--preview-current", display.FlowState == FrameAnimationFlowElementState.Current);
EnableInClassList("fa-node--preview-passed", display.FlowState == FrameAnimationFlowElementState.Passed);
EnableInClassList("fa-node--preview-upcoming", display.FlowState == FrameAnimationFlowElementState.Upcoming);
}
}
internal sealed class FrameAnimationEdgeView : Edge
{
private readonly Action<FrameAnimationEdgeView> selectedCallback;
private Color baseColor = Color.white;
public AnimationEdge Data { get; }
internal FrameAnimationEdgeView(AnimationEdge data, Action<FrameAnimationEdgeView> selectedCallback)
@@ -128,7 +138,7 @@ namespace AibisDream.FrameAnimation.Editor
internal void SetDimmed(bool dimmed)
{
style.opacity = dimmed ? 0.16f : 1f;
style.opacity = dimmed ? 0.30f : 1f;
}
internal void Refresh(IReadOnlyList<FrameAnimationEditorIssue> issues)
@@ -139,12 +149,37 @@ namespace AibisDream.FrameAnimation.Editor
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;
tooltip = matching.Length == 0
? $"{Data.ExitName} / {Data.Condition}"
: string.Join("\n", matching.Select(issue => $"[{issue.Code}] {issue.Message}"));
}
internal void SetPreviewState(FrameAnimationFlowElementState state)
{
if (state == FrameAnimationFlowElementState.Current)
{
edgeControl.inputColor = new Color(0.25f, 0.75f, 1f);
edgeControl.outputColor = new Color(0.25f, 0.75f, 1f);
}
else if (state == FrameAnimationFlowElementState.Passed)
{
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;
}
}
}
internal sealed class FrameAnimationClipSearchProvider : ScriptableObject, ISearchWindowProvider
@@ -200,16 +235,22 @@ namespace AibisDream.FrameAnimation.Editor
private IReadOnlyList<FrameAnimationEditorIssue> issues = Array.Empty<FrameAnimationEditorIssue>();
private bool rebuilding;
private Vector2 contextPosition;
private string focusedFlowId = string.Empty;
internal event Action<FrameAnimationEditorSelection> SelectionRequested;
internal event Action<IReadOnlyList<AnimationNode>> MultiSelectionChanged;
internal event Action<IReadOnlyList<FrameAnimationEditorSelection>> SelectionSetChanged;
internal event Action<AnimationNode> CreateFlowRequested;
internal event Action<AnimationNode> NodeCreated;
internal event Action ExitFocusRequested;
internal event Action GraphChanged;
internal event Action<string> NotificationRequested;
internal event Action<Vector3, float> WorkspaceViewTransformChanged;
internal FrameAnimationGraphView()
{
style.flexGrow = 1f;
focusable = true;
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
@@ -218,9 +259,12 @@ namespace AibisDream.FrameAnimation.Editor
Insert(0, grid);
grid.StretchToParentSize();
graphViewChanged = OnGraphViewChanged;
viewTransformChanged += _ =>
WorkspaceViewTransformChanged?.Invoke(viewTransform.position, viewTransform.scale.x);
RegisterCallback<MouseUpEvent>(_ => NotifySelectionSummary());
RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
RegisterCallback<DragPerformEvent>(OnDragPerform);
RegisterCallback<KeyDownEvent>(OnKeyDown);
}
internal void Bind(
@@ -325,6 +369,7 @@ namespace AibisDream.FrameAnimation.Editor
internal void SetFocusedFlow(string flowId)
{
focusedFlowId = flowId ?? string.Empty;
if (graph == null || string.IsNullOrEmpty(flowId))
{
foreach (var view in nodeViews.Values) view.SetDimmed(false);
@@ -359,6 +404,36 @@ namespace AibisDream.FrameAnimation.Editor
internal IReadOnlyList<AnimationNode> SelectedNodes =>
selection.OfType<FrameAnimationClipNodeView>().Select(view => view.Data).ToArray();
internal IReadOnlyList<AnimationNode> VisibleNodes => nodeViews.Values
.Where(view => view.worldBound.Overlaps(worldBound))
.Select(view => view.Data)
.ToArray();
internal void ApplyPreview(FrameAnimationPreviewCoordinator coordinator)
{
if (coordinator == null) return;
foreach (var view in nodeViews.Values)
{
view.SetPreview(coordinator.GetNodeDisplay(view.Data), coordinator.Background);
}
foreach (var view in edgeViews.Values)
{
view.SetPreviewState(coordinator.GetEdgeState(view.Data));
}
}
internal void RestoreViewTransform(Vector3 position, float scale)
{
var safeScale = float.IsNaN(scale) || float.IsInfinity(scale)
? 1f : Mathf.Clamp(scale, ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
if (float.IsNaN(position.x) || float.IsInfinity(position.x) ||
float.IsNaN(position.y) || float.IsInfinity(position.y))
{
position = Vector3.zero;
}
UpdateViewTransform(position, new Vector3(safeScale, safeScale, 1f));
}
internal void RequestDeleteNodes(IEnumerable<AnimationNode> nodes)
{
var items = (nodes ?? Array.Empty<AnimationNode>()).Where(node => node != null).Distinct().ToArray();
@@ -437,10 +512,11 @@ namespace AibisDream.FrameAnimation.Editor
SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(panelPosition)), provider);
}
private void CreateNode(FrameClip clip, Vector2 position)
internal void CreateNode(FrameClip clip, Vector2 position)
{
if (FrameAnimationGraphMutationService.CreateNode(graph, clip, position, out var node, out var error))
{
NodeCreated?.Invoke(node);
GraphChanged?.Invoke();
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, node));
}
@@ -583,19 +659,61 @@ namespace AibisDream.FrameAnimation.Editor
private void OnNodeSelected(FrameAnimationClipNodeView view)
{
if (rebuilding)
{
return;
}
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Node, view.Data));
NotifySelectionSummary();
}
private void OnEdgeSelected(FrameAnimationEdgeView view)
{
if (rebuilding)
{
return;
}
SelectionRequested?.Invoke(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Edge, view.Data));
NotifySelectionSummary();
}
private void NotifySelectionSummary()
{
if (rebuilding)
{
return;
}
MultiSelectionChanged?.Invoke(SelectedNodes);
var selected = selection.Select(item => item switch
{
FrameAnimationClipNodeView nodeView => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Node, nodeView.Data),
FrameAnimationEdgeView edgeView => new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Edge, edgeView.Data),
_ => default
})
.Where(item => item.Kind != FrameAnimationEditorSelectionKind.None)
.ToArray();
SelectionSetChanged?.Invoke(selected);
}
private void OnKeyDown(KeyDownEvent evt)
{
if (!TryHandleFocusExitShortcut(evt.keyCode, evt.target is TextField))
{
return;
}
evt.StopImmediatePropagation();
}
internal bool TryHandleFocusExitShortcut(KeyCode keyCode, bool textInputActive)
{
if (keyCode != KeyCode.Escape || textInputActive || string.IsNullOrEmpty(focusedFlowId))
{
return false;
}
ExitFocusRequested?.Invoke();
return true;
}
private void OnDragUpdated(DragUpdatedEvent evt)