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
@@ -33,6 +33,8 @@ namespace AibisDream.FrameAnimation.Editor
private Label canvasLabel;
private FrameAnimationGraphView graphView;
private PopupField<string> flowFocusField;
private Label flowFocusStatusLabel;
private ToolbarButton exitFlowFocusButton;
private string focusedFlowId = string.Empty;
private IReadOnlyList<AnimationNode> multiSelectedNodes = Array.Empty<AnimationNode>();
private AnimationNode lastSelectedNode;
@@ -64,6 +66,21 @@ namespace AibisDream.FrameAnimation.Editor
private SerializedObject clipSerializedObject;
private FrameClip frameListClip;
private ReorderableList frameList;
private readonly FrameAnimationPreviewCoordinator previewCoordinator = new FrameAnimationPreviewCoordinator();
private readonly List<Slider> previewTimelineSliders = new List<Slider>();
private readonly List<Label> previewTimeLabels = new List<Label>();
private readonly List<Button> previewPlayButtons = new List<Button>();
private readonly List<Label> previewTargetLabels = new List<Label>();
private VisualElement clipPreviewPanel;
private FrameAnimationPreviewElement clipPreviewElement;
private Label clipPreviewStatus;
private EnumField nodePreviewPolicyField;
private EnumField previewBackgroundField;
private EnumField previewZoomField;
private Slider manualZoomSlider;
private PopupField<string> previewSpeedField;
private double lastPreviewTick;
private bool updatingPreviewUi;
[MenuItem("Window/Aibis Dream/Frame Animation Graph Editor")]
public static void ShowWindow()
@@ -90,6 +107,11 @@ namespace AibisDream.FrameAnimation.Editor
{
rootVisualElement.Clear();
rootVisualElement.style.flexDirection = FlexDirection.Column;
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uss");
if (styleSheet != null) rootVisualElement.styleSheets.Add(styleSheet);
previewCoordinator.Changed -= OnPreviewChanged;
previewCoordinator.Changed += OnPreviewChanged;
BuildToolbar();
BuildMainArea();
BuildBottomArea();
@@ -100,23 +122,35 @@ namespace AibisDream.FrameAnimation.Editor
RefreshBrowser();
RefreshFlowChoices();
RefreshGraphView();
UpdatePreviewTargetForSelection();
RefreshPreviewUi();
}
private void OnEnable()
{
Undo.undoRedoPerformed += OnProjectDataChanged;
EditorApplication.projectChanged += OnProjectDataChanged;
lastPreviewTick = EditorApplication.timeSinceStartup;
}
private void OnDisable()
{
Undo.undoRedoPerformed -= OnProjectDataChanged;
EditorApplication.projectChanged -= OnProjectDataChanged;
previewCoordinator.Changed -= OnPreviewChanged;
previewCoordinator.ClearTarget();
SaveWorkspaceState();
}
private void Update()
{
var now = EditorApplication.timeSinceStartup;
if (now - lastPreviewTick >= 1d / 30d)
{
var delta = Math.Max(0d, now - lastPreviewTick);
lastPreviewTick = now;
previewCoordinator.Tick(delta, graphView?.VisibleNodes ?? Array.Empty<AnimationNode>());
}
if (validateAt > 0d && EditorApplication.timeSinceStartup >= validateAt)
{
validateAt = -1d;
@@ -179,21 +213,36 @@ namespace AibisDream.FrameAnimation.Editor
};
var canvasToolbar = new Toolbar();
canvasToolbar.Add(new ToolbarButton(ShowAllNodes) { text = "Show All" });
flowFocusStatusLabel = new Label("Show All")
{
style =
{
minWidth = 110f,
maxWidth = 240f,
flexShrink = 1f,
unityFontStyleAndWeight = FontStyle.Bold,
unityTextAlign = TextAnchor.MiddleLeft
}
};
canvasToolbar.Add(flowFocusStatusLabel);
exitFlowFocusButton = new ToolbarButton(() => ExitFlowFocus(false)) { text = "Exit Focus" };
exitFlowFocusButton.style.display = DisplayStyle.None;
canvasToolbar.Add(exitFlowFocusButton);
flowFocusField = new PopupField<string>(new List<string> { "Show All" }, 0)
{
style = { minWidth = 170f }
style = { minWidth = 135f }
};
flowFocusField.RegisterValueChangedCallback(evt =>
{
if (evt.newValue == "Show All")
{
SetFocusedFlow(string.Empty);
ExitFlowFocus(false);
}
else
{
var flow = graph?.Flows.FirstOrDefault(item => item != null &&
FlowChoiceLabel(item) == evt.newValue);
SetFocusedFlow(flow?.Id ?? string.Empty);
EnterFlowFocus(flow?.Id);
}
});
canvasToolbar.Add(flowFocusField);
@@ -201,6 +250,8 @@ namespace AibisDream.FrameAnimation.Editor
canvasToolbar.Add(new ToolbarButton(() => graphView?.FrameCurrentFlow(focusedFlowId)) { text = "Frame Flow" });
canvasToolbar.Add(new ToolbarButton(AutoLayoutCanvas) { text = "Auto Layout" });
canvasToolbar.Add(new ToolbarButton(CreateFlowFromCanvasSelection) { text = "Create Flow" });
canvasToolbar.Add(new ToolbarSpacer());
canvasToolbar.Add(BuildPreviewTransport(true, true));
canvas.Add(canvasToolbar);
graphView = new FrameAnimationGraphView();
graphView.SelectionRequested += value => SetSelection(value, false);
@@ -209,9 +260,19 @@ namespace AibisDream.FrameAnimation.Editor
multiSelectedNodes = nodes ?? Array.Empty<AnimationNode>();
propertyContainer?.MarkDirtyRepaint();
};
graphView.SelectionSetChanged += OnCanvasSelectionSetChanged;
graphView.CreateFlowRequested += ShowCreateFlow;
graphView.NodeCreated += _ => ExitFlowFocus(false);
graphView.ExitFocusRequested += () => ExitFlowFocus(false);
graphView.GraphChanged += OnCanvasGraphChanged;
graphView.NotificationRequested += message => ShowNotification(new GUIContent(message));
graphView.WorkspaceViewTransformChanged += (position, scale) =>
{
if (graph == null) return;
FrameAnimationWorkspaceState.SetFloat(graph, "CanvasX", position.x);
FrameAnimationWorkspaceState.SetFloat(graph, "CanvasY", position.y);
FrameAnimationWorkspaceState.SetFloat(graph, "CanvasScale", scale);
};
canvas.Add(graphView);
canvasLabel = new Label("选择或创建 FrameAnimationGraph")
{
@@ -242,6 +303,9 @@ namespace AibisDream.FrameAnimation.Editor
}
};
rightPanel.Add(CreatePanelHeader("Properties"));
clipPreviewPanel = BuildClipPreviewPanel();
clipPreviewPanel.style.display = DisplayStyle.None;
rightPanel.Add(clipPreviewPanel);
propertyContainer = new IMGUIContainer(DrawSelectionProperties) { style = { flexGrow = 1f } };
rightPanel.Add(propertyContainer);
main.Add(CreateVerticalResizer(rightPanel, 260f, 650f, "RightWidth", resizeFromLeft: true));
@@ -249,6 +313,301 @@ namespace AibisDream.FrameAnimation.Editor
rootVisualElement.Add(main);
}
private VisualElement BuildPreviewTransport(bool includeTarget, bool includePreferences)
{
var controls = new VisualElement();
controls.AddToClassList("fa-preview-controls");
if (includeTarget)
{
var targetLabel = new Label("Preview: None")
{
style = { minWidth = 120f, maxWidth = 300f }
};
previewTargetLabels.Add(targetLabel);
controls.Add(targetLabel);
}
controls.Add(new ToolbarButton(previewCoordinator.Restart) { text = "↶", tooltip = "Restart" });
controls.Add(new ToolbarButton(() => previewCoordinator.Step(-1)) { text = "◀", tooltip = "Previous Frame" });
var play = new ToolbarButton(PreviewPlayPause) { text = "▶", tooltip = "Play / Pause" };
previewPlayButtons.Add(play);
controls.Add(play);
controls.Add(new ToolbarButton(() => previewCoordinator.Step(1)) { text = "▶|", tooltip = "Next Frame" });
controls.Add(new ToolbarButton(previewCoordinator.Stop) { text = "■", tooltip = "Stop" });
var timeline = new Slider(0f, 1f)
{
style = { minWidth = 90f, maxWidth = 240f, flexGrow = 1f }
};
timeline.RegisterValueChangedCallback(evt =>
{
if (!updatingPreviewUi) previewCoordinator.Seek(evt.newValue);
});
previewTimelineSliders.Add(timeline);
controls.Add(timeline);
var time = new Label("0 / 0 ms") { style = { minWidth = 82f } };
previewTimeLabels.Add(time);
controls.Add(time);
if (includePreferences)
{
previewSpeedField = new PopupField<string>(
new List<string> { "0.25×", "0.5×", "1×", "2×", "4×" }, "1×")
{
style = { width = 64f }
};
previewSpeedField.RegisterValueChangedCallback(evt =>
{
previewCoordinator.PreviewSpeed = ParsePreviewSpeed(evt.newValue);
SaveWorkspaceState();
});
controls.Add(previewSpeedField);
nodePreviewPolicyField = new EnumField(NodePreviewPolicy.SelectedOnly)
{
style = { width = 100f }
};
nodePreviewPolicyField.RegisterValueChangedCallback(evt =>
{
previewCoordinator.SetNodePolicy((NodePreviewPolicy)evt.newValue);
SaveWorkspaceState();
});
controls.Add(nodePreviewPolicyField);
previewBackgroundField = new EnumField(FrameAnimationPreviewBackground.Checkerboard)
{
style = { width = 100f }
};
previewBackgroundField.RegisterValueChangedCallback(evt =>
{
previewCoordinator.Background = (FrameAnimationPreviewBackground)evt.newValue;
SaveWorkspaceState();
RefreshPreviewUi();
});
controls.Add(previewBackgroundField);
}
return controls;
}
private VisualElement BuildClipPreviewPanel()
{
var panel = new VisualElement();
panel.AddToClassList("fa-clip-preview-panel");
panel.Add(new Label("Clip Preview") { style = { unityFontStyleAndWeight = FontStyle.Bold } });
clipPreviewElement = new FrameAnimationPreviewElement();
panel.Add(clipPreviewElement);
panel.Add(BuildPreviewTransport(false, false));
var zoomRow = new VisualElement { style = { flexDirection = FlexDirection.Row } };
previewZoomField = new EnumField(FrameAnimationPreviewZoomMode.Fit) { style = { width = 100f } };
previewZoomField.RegisterValueChangedCallback(evt =>
{
previewCoordinator.ZoomMode = (FrameAnimationPreviewZoomMode)evt.newValue;
SaveWorkspaceState();
RefreshPreviewUi();
});
zoomRow.Add(previewZoomField);
manualZoomSlider = new Slider("Manual", 0.25f, 8f) { style = { flexGrow = 1f } };
manualZoomSlider.RegisterValueChangedCallback(evt =>
{
previewCoordinator.ManualZoom = evt.newValue;
SaveWorkspaceState();
RefreshPreviewUi();
});
zoomRow.Add(manualZoomSlider);
panel.Add(zoomRow);
clipPreviewStatus = new Label();
clipPreviewStatus.AddToClassList("fa-preview-status");
panel.Add(clipPreviewStatus);
return panel;
}
private void PreviewPlayPause()
{
if (previewCoordinator.State == FrameAnimationPreviewState.Playing)
{
previewCoordinator.Pause();
return;
}
if (previewCoordinator.TargetKind == FrameAnimationPreviewTargetKind.Flow)
{
validationIssues = FrameAnimationEditorValidationService.Validate(graph, false, out _);
RefreshBrowser();
bottomContainer?.MarkDirtyRepaint();
RefreshGraphView();
}
previewCoordinator.Play();
if (previewCoordinator.State == FrameAnimationPreviewState.Failed)
{
ShowNotification(new GUIContent(previewCoordinator.Error.Message));
}
}
private void UpdatePreviewTargetForSelection()
{
if (graph == null)
{
previewCoordinator.ClearTarget();
return;
}
if (previewCoordinator.TargetKind == FrameAnimationPreviewTargetKind.Flow &&
previewCoordinator.Target is AnimationFlow activeFlow &&
IsSelectionInsideFlow(activeFlow, selection))
{
RefreshPreviewUi();
return;
}
switch (selection.Kind)
{
case FrameAnimationEditorSelectionKind.Clip:
previewCoordinator.SetTarget(FrameAnimationPreviewTargetKind.Clip, selection.Value);
break;
case FrameAnimationEditorSelectionKind.Node:
previewCoordinator.SetTarget(FrameAnimationPreviewTargetKind.Node, selection.Value);
break;
case FrameAnimationEditorSelectionKind.Flow:
previewCoordinator.SetTarget(FrameAnimationPreviewTargetKind.Flow, selection.Value);
break;
default:
previewCoordinator.ClearTarget();
break;
}
}
private bool IsSelectionInsideFlow(AnimationFlow flow, FrameAnimationEditorSelection value)
{
if (flow == null || graph == null) return false;
if (value.Kind == FrameAnimationEditorSelectionKind.Flow) return ReferenceEquals(value.Value, flow);
var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId);
return value.Value switch
{
AnimationNode node => reachable.Nodes.Contains(node),
AnimationEdge edge => reachable.Edges.Contains(edge),
_ => false
};
}
private void OnPreviewChanged()
{
RefreshPreviewUi();
Repaint();
}
private void RefreshPreviewUi()
{
if (rootVisualElement == null) return;
updatingPreviewUi = true;
var timeline = previewCoordinator.Timeline;
var maximum = timeline?.DisplayDurationSeconds ?? 0d;
var position = Math.Min(maximum, previewCoordinator.PositionSeconds);
foreach (var slider in previewTimelineSliders)
{
slider.lowValue = 0f;
slider.highValue = Mathf.Max(0.0001f, (float)maximum);
slider.SetValueWithoutNotify((float)position);
slider.SetEnabled(timeline != null && timeline.Segments.Count > 0 && maximum > 0d);
}
var total = timeline?.IsInfinite == true ? "∞" : FormatPreviewTime(maximum);
foreach (var label in previewTimeLabels)
{
label.text = $"{FormatPreviewTime(position)} / {total}";
}
foreach (var button in previewPlayButtons)
{
button.text = previewCoordinator.State == FrameAnimationPreviewState.Playing ? "❚❚" : "▶";
button.SetEnabled(previewCoordinator.TargetKind != FrameAnimationPreviewTargetKind.None);
}
var targetText = PreviewTargetName();
foreach (var label in previewTargetLabels)
{
label.text = targetText;
label.tooltip = targetText;
}
if (clipPreviewPanel != null)
{
clipPreviewPanel.style.display = selection.Kind == FrameAnimationEditorSelectionKind.Clip
? DisplayStyle.Flex : DisplayStyle.None;
}
if (clipPreviewElement != null)
{
clipPreviewElement.SetBackground(previewCoordinator.Background);
clipPreviewElement.SetZoom(previewCoordinator.ZoomMode, previewCoordinator.ManualZoom);
clipPreviewElement.SetDisplay(
previewCoordinator.CurrentSprite,
previewCoordinator.CurrentSprite == null,
previewCoordinator.Snapshot.IsCompleted ? previewCoordinator.Snapshot.EndBehavior.ToString() : "Empty Frame");
}
if (clipPreviewStatus != null)
{
clipPreviewStatus.text = BuildPreviewStatus();
}
if (manualZoomSlider != null)
{
manualZoomSlider.SetValueWithoutNotify(previewCoordinator.ManualZoom);
manualZoomSlider.SetEnabled(previewCoordinator.ZoomMode == FrameAnimationPreviewZoomMode.Manual);
}
previewZoomField?.SetValueWithoutNotify(previewCoordinator.ZoomMode);
nodePreviewPolicyField?.SetValueWithoutNotify(previewCoordinator.NodePolicy);
previewBackgroundField?.SetValueWithoutNotify(previewCoordinator.Background);
previewSpeedField?.SetValueWithoutNotify(FormatPreviewSpeed(previewCoordinator.PreviewSpeed));
graphView?.ApplyPreview(previewCoordinator);
updatingPreviewUi = false;
}
private string BuildPreviewStatus()
{
if (previewCoordinator.State == FrameAnimationPreviewState.Failed)
{
return $"ERROR [{previewCoordinator.Error.Code}] {previewCoordinator.Error.Message}";
}
var snapshot = previewCoordinator.Snapshot;
if (!snapshot.HasStarted || snapshot.Clip == null) return "No preview target";
var duration = snapshot.FrameIndex >= 0 && snapshot.FrameIndex < snapshot.Clip.FrameCount
? snapshot.Clip.Frames[snapshot.FrameIndex].DurationMs : 0;
var effective = snapshot.StepSpeed > 0f ? duration / snapshot.StepSpeed : double.PositiveInfinity;
return $"{previewCoordinator.State} · Frame {snapshot.FrameIndex + 1}/{snapshot.Clip.FrameCount} · " +
$"source {duration} ms · effective {(double.IsInfinity(effective) ? "" : effective.ToString("0") + " ms")} · {snapshot.Clip.Id}" +
(string.IsNullOrEmpty(snapshot.NodeId) ? string.Empty : $" · Node {snapshot.NodeId}") +
(previewCoordinator.Timeline?.IsBlocked == true ? " · Blocked (speed 0)" : string.Empty);
}
private string PreviewTargetName()
{
return previewCoordinator.Target switch
{
FrameClip clip => $"Clip: {clip.DisplayName}",
AnimationNode node => $"Node: {node.DisplayName}",
AnimationFlow flow => BuildFlowPreviewTargetName(flow),
_ => "Preview: None"
};
}
private string BuildFlowPreviewTargetName(AnimationFlow flow)
{
var snapshot = previewCoordinator.Snapshot;
if (!snapshot.HasStarted || snapshot.Clip == null) return $"Flow: {flow.DisplayName}";
var node = graph?.Nodes.FirstOrDefault(item => item != null && item.InternalId == snapshot.NodeId);
var ending = snapshot.IsCompleted && previewCoordinator.CurrentSprite == null
? $" · {snapshot.EndBehavior}" : string.Empty;
var blocked = previewCoordinator.Timeline?.IsBlocked == true ? " · Blocked" : string.Empty;
return $"Flow: {flow.DisplayName} · Node: {node?.DisplayName ?? snapshot.NodeId} · " +
$"{snapshot.Clip.Id} F{snapshot.FrameIndex + 1}/{snapshot.Clip.FrameCount}{ending}{blocked}";
}
private static string FormatPreviewTime(double seconds) => $"{Math.Max(0d, seconds) * 1000d:0} ms";
private static float ParsePreviewSpeed(string value)
{
return value switch { "0.25×" => 0.25f, "0.5×" => 0.5f, "2×" => 2f, "4×" => 4f, _ => 1f };
}
private static string FormatPreviewSpeed(float value)
{
if (Mathf.Approximately(value, 0.25f)) return "0.25×";
if (Mathf.Approximately(value, 0.5f)) return "0.5×";
if (Mathf.Approximately(value, 2f)) return "2×";
if (Mathf.Approximately(value, 4f)) return "4×";
return "1×";
}
private VisualElement BuildLeftPanel()
{
var panel = new VisualElement
@@ -263,9 +622,9 @@ namespace AibisDream.FrameAnimation.Editor
}
};
var tabs = new Toolbar();
tabs.Add(new ToolbarButton(() => SetResourceTab(ResourceTab.Clips)) { text = "Clips" });
tabs.Add(new ToolbarButton(() => SetResourceTab(ResourceTab.Flows)) { text = "Flows" });
tabs.Add(new ToolbarButton(() => SetResourceTab(ResourceTab.Sources)) { text = "Sources" });
tabs.Add(new ToolbarButton(() => SetResourceTab(ResourceTab.Clips, true)) { text = "Clips" });
tabs.Add(new ToolbarButton(() => SetResourceTab(ResourceTab.Flows, true)) { text = "Flows" });
tabs.Add(new ToolbarButton(() => SetResourceTab(ResourceTab.Sources, true)) { text = "Sources" });
panel.Add(tabs);
searchField = new ToolbarSearchField();
searchField.RegisterValueChangedCallback(_ =>
@@ -432,7 +791,9 @@ namespace AibisDream.FrameAnimation.Editor
{
SaveGraph();
SaveWorkspaceState();
focusedFlowId = string.Empty;
graph = value;
previewCoordinator.SetGraph(graph);
selection = graph != null
? new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph)
: default;
@@ -451,6 +812,7 @@ namespace AibisDream.FrameAnimation.Editor
}
}
previewCoordinator.SetGraph(graph);
graphField?.SetValueWithoutNotify(graph);
if (graph != null)
{
@@ -460,7 +822,9 @@ namespace AibisDream.FrameAnimation.Editor
RefreshBrowser();
RefreshFlowChoices();
RefreshGraphView();
RestoreCanvasView();
RefreshCanvasSummary();
UpdatePreviewTargetForSelection();
propertyContainer?.MarkDirtyRepaint();
bottomContainer?.MarkDirtyRepaint();
}
@@ -478,8 +842,12 @@ namespace AibisDream.FrameAnimation.Editor
}
}
private void SetResourceTab(ResourceTab tab)
private void SetResourceTab(ResourceTab tab, bool userInitiated = false)
{
if (userInitiated && tab != ResourceTab.Flows)
{
ExitFlowFocus(false);
}
resourceTab = tab;
SaveWorkspaceState();
RefreshBrowser();
@@ -495,28 +863,29 @@ namespace AibisDream.FrameAnimation.Editor
private void SetSelection(FrameAnimationEditorSelection value, bool locateOnCanvas = true)
{
if (value.Kind == FrameAnimationEditorSelectionKind.Flow && value.Value is AnimationFlow selectedFlow)
{
EnterFlowFocus(selectedFlow.Id);
}
else if (FrameAnimationFlowFocusPolicy.ShouldExitForSelection(graph, focusedFlowId, value))
{
ExitFlowFocus(false);
}
selection = value;
if (value.Kind == FrameAnimationEditorSelectionKind.Node && value.Value is AnimationNode node)
{
lastSelectedNode = node;
}
if (value.Kind == FrameAnimationEditorSelectionKind.Flow && value.Value is AnimationFlow flow)
{
SetFocusedFlow(flow.Id);
}
frameListClip = null;
frameList = null;
clipSerializedObject = null;
propertyContainer?.MarkDirtyRepaint();
RefreshCanvasSummary();
SaveWorkspaceState();
UpdatePreviewTargetForSelection();
if (locateOnCanvas && (value.Kind == FrameAnimationEditorSelectionKind.Node ||
value.Kind == FrameAnimationEditorSelectionKind.Edge))
{
if (!SelectionBelongsToFocusedFlow(value))
{
SetFocusedFlow(string.Empty);
}
graphView?.SelectAndFrame(value);
}
}
@@ -633,10 +1002,13 @@ namespace AibisDream.FrameAnimation.Editor
private void RefreshGraphView()
{
graphView?.Bind(graph, validationIssues, focusedFlowId);
graphView?.ApplyPreview(previewCoordinator);
}
private void OnCanvasGraphChanged()
{
previewCoordinator.RefreshTarget(true);
RevalidateFlowFocus();
ScheduleLightValidation();
RefreshBrowser();
RefreshFlowChoices();
@@ -655,57 +1027,127 @@ namespace AibisDream.FrameAnimation.Editor
private void RefreshFlowChoices()
{
if (flowFocusField == null)
if (flowFocusField != null)
{
return;
var choices = new List<string> { "Show All" };
if (graph != null)
{
choices.AddRange(graph.Flows.Where(flow => flow != null)
.OrderBy(flow => flow.DisplayName, StringComparer.Ordinal)
.ThenBy(flow => flow.Id, StringComparer.Ordinal)
.Select(FlowChoiceLabel));
}
flowFocusField.choices = choices;
var focused = FrameAnimationFlowFocusPolicy.FindFocusedFlow(graph, focusedFlowId);
flowFocusField.SetValueWithoutNotify(focused != null ? FlowChoiceLabel(focused) : "Show All");
}
var choices = new List<string> { "Show All" };
if (graph != null)
{
choices.AddRange(graph.Flows.Where(flow => flow != null)
.OrderBy(flow => flow.DisplayName, StringComparer.Ordinal)
.ThenBy(flow => flow.Id, StringComparer.Ordinal)
.Select(FlowChoiceLabel));
}
flowFocusField.choices = choices;
var focused = graph?.Flows.FirstOrDefault(flow => flow != null && flow.Id == focusedFlowId);
flowFocusField.SetValueWithoutNotify(focused != null ? FlowChoiceLabel(focused) : "Show All");
RefreshFlowFocusUi();
}
private void SetFocusedFlow(string flowId)
private void EnterFlowFocus(string flowId)
{
focusedFlowId = graph?.Flows.Any(flow => flow != null && flow.Id == flowId) == true
? flowId
: string.Empty;
var flow = FrameAnimationFlowFocusPolicy.FindFocusedFlow(graph, flowId);
if (flow == null)
{
ExitFlowFocus(false);
return;
}
focusedFlowId = flow.Id;
RefreshFlowChoices();
graphView?.SetFocusedFlow(focusedFlowId);
SaveWorkspaceState();
}
private void ExitFlowFocus(bool frameAll)
{
focusedFlowId = string.Empty;
RefreshFlowChoices();
graphView?.SetFocusedFlow(string.Empty);
if (frameAll)
{
graphView?.FrameAll();
}
}
private void RevalidateFlowFocus()
{
if (string.IsNullOrEmpty(focusedFlowId))
{
RefreshFlowFocusUi();
return;
}
var flow = FrameAnimationFlowFocusPolicy.FindFocusedFlow(graph, focusedFlowId);
if (flow == null)
{
if (selection.Kind == FrameAnimationEditorSelectionKind.Flow &&
selection.Value is AnimationFlow selectedFlow && graph?.Flows.Contains(selectedFlow) == true)
{
EnterFlowFocus(selectedFlow.Id);
}
else
{
ExitFlowFocus(false);
}
return;
}
if (selection.Kind == FrameAnimationEditorSelectionKind.Flow &&
selection.Value is AnimationFlow activeSelection &&
graph.Flows.Contains(activeSelection) && activeSelection != flow)
{
EnterFlowFocus(activeSelection.Id);
return;
}
var selectedElementWasRemoved = selection.Value switch
{
AnimationNode node => !graph.Nodes.Contains(node),
AnimationEdge edge => !graph.Edges.Contains(edge),
_ => false
};
if (selectedElementWasRemoved)
{
selection = new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow);
lastSelectedNode = null;
propertyContainer?.MarkDirtyRepaint();
SaveWorkspaceState();
}
if (FrameAnimationFlowFocusPolicy.ShouldExitForSelection(graph, focusedFlowId, selection))
{
ExitFlowFocus(false);
return;
}
RefreshFlowChoices();
graphView?.SetFocusedFlow(focusedFlowId);
}
private void RefreshFlowFocusUi()
{
var flow = FrameAnimationFlowFocusPolicy.FindFocusedFlow(graph, focusedFlowId);
if (flowFocusStatusLabel != null)
{
flowFocusStatusLabel.text = flow != null
? $"Focused: {flow.DisplayName} ({flow.Id})"
: "Show All";
flowFocusStatusLabel.tooltip = flowFocusStatusLabel.text;
flowFocusStatusLabel.style.color = flow != null
? new StyleColor(new Color(0.45f, 0.78f, 1f))
: new StyleColor(Color.gray);
}
if (exitFlowFocusButton != null)
{
exitFlowFocusButton.style.display = flow != null ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private void OnCanvasSelectionSetChanged(IReadOnlyList<FrameAnimationEditorSelection> selections)
{
if (FrameAnimationFlowFocusPolicy.ShouldExitForSelectionSet(graph, focusedFlowId, selections))
{
ExitFlowFocus(false);
}
}
private void ShowAllNodes()
{
SetFocusedFlow(string.Empty);
graphView?.FrameAll();
}
private bool SelectionBelongsToFocusedFlow(FrameAnimationEditorSelection value)
{
if (graph == null || string.IsNullOrEmpty(focusedFlowId))
{
return true;
}
var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == focusedFlowId);
if (flow == null)
{
return false;
}
var reachable = new FrameAnimationGraphTopology(graph).GetReachable(flow.EntryNodeId);
return value.Value switch
{
AnimationNode node => reachable.Nodes.Contains(node),
AnimationEdge edge => reachable.Edges.Contains(edge),
_ => true
};
ExitFlowFocus(true);
}
private void CreateFlowFromCanvasSelection()
@@ -728,7 +1170,6 @@ namespace AibisDream.FrameAnimation.Editor
}
FrameAnimationFlowWindow.Show(graph, entry, flow =>
{
SetFocusedFlow(flow.Id);
SetSelection(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Flow, flow), false);
OnCanvasGraphChanged();
});
@@ -861,6 +1302,7 @@ namespace AibisDream.FrameAnimation.Editor
}
if (EditorGUI.EndChangeCheck())
{
previewCoordinator.RefreshTarget(false);
ScheduleLightValidation();
RefreshBrowser();
RefreshCanvasSummary();
@@ -1117,14 +1559,14 @@ namespace AibisDream.FrameAnimation.Editor
}
else
{
SetFocusedFlow(flow.Id);
EnterFlowFocus(flow.Id);
OnCanvasGraphChanged();
}
}
}
if (GUILayout.Button("Focus Flow On Canvas"))
{
SetFocusedFlow(flow.Id);
EnterFlowFocus(flow.Id);
graphView?.FrameCurrentFlow(flow.Id);
}
if (GUILayout.Button("Rename Flow ID"))
@@ -1152,7 +1594,7 @@ namespace AibisDream.FrameAnimation.Editor
}
else
{
SetFocusedFlow(string.Empty);
ExitFlowFocus(false);
SetSelection(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph));
OnCanvasGraphChanged();
}
@@ -1567,6 +2009,7 @@ namespace AibisDream.FrameAnimation.Editor
{
EditorUtility.DisplayDialog("刷新失败", error, "确定");
}
previewCoordinator.RefreshTarget(true);
importPreview = FrameAnimationImportService.PreviewAll(graph);
RefreshBrowser();
ValidateFull();
@@ -1638,7 +2081,7 @@ namespace AibisDream.FrameAnimation.Editor
}
else if (focusedFlowId == oldId)
{
focusedFlowId = flow.Id;
EnterFlowFocus(flow.Id);
}
RefreshBrowser();
RefreshFlowChoices();
@@ -1814,13 +2257,40 @@ namespace AibisDream.FrameAnimation.Editor
FrameAnimationWorkspaceState.GetString(graph, "ValidationKind", "All"),
out ValidationKindFilter restoredValidationKind)
? restoredValidationKind : ValidationKindFilter.All;
var nodePolicy = Enum.TryParse(
FrameAnimationWorkspaceState.GetString(graph, "NodePreviewPolicy", "SelectedOnly"),
out NodePreviewPolicy restoredNodePolicy)
? restoredNodePolicy : NodePreviewPolicy.SelectedOnly;
var background = Enum.TryParse(
FrameAnimationWorkspaceState.GetString(graph, "PreviewBackground", "Checkerboard"),
out FrameAnimationPreviewBackground restoredBackground)
? restoredBackground : FrameAnimationPreviewBackground.Checkerboard;
var zoomMode = Enum.TryParse(
FrameAnimationWorkspaceState.GetString(graph, "PreviewZoomMode", "Fit"),
out FrameAnimationPreviewZoomMode restoredZoom)
? restoredZoom : FrameAnimationPreviewZoomMode.Fit;
previewCoordinator.NodePolicy = nodePolicy;
previewCoordinator.Background = background;
previewCoordinator.ZoomMode = zoomMode;
previewCoordinator.PreviewSpeed = Mathf.Clamp(
FrameAnimationWorkspaceState.GetFloat(graph, "PreviewSpeed", 1f), 0.25f, 4f);
previewCoordinator.ManualZoom = Mathf.Clamp(
FrameAnimationWorkspaceState.GetFloat(graph, "PreviewManualZoom", 1f), 0.25f, 8f);
bottomPanel.style.height = FrameAnimationWorkspaceState.GetFloat(graph, "BottomHeight", 230f);
focusedFlowId = FrameAnimationWorkspaceState.GetString(graph, "FocusedFlow", string.Empty);
if (!graph.Flows.Any(flow => flow != null && flow.Id == focusedFlowId))
{
focusedFlowId = string.Empty;
}
ApplyBottomState();
RestoreCanvasView();
RefreshPreviewUi();
}
private void RestoreCanvasView()
{
if (graph == null || graphView == null) return;
var position = new Vector3(
FrameAnimationWorkspaceState.GetFloat(graph, "CanvasX", 0f),
FrameAnimationWorkspaceState.GetFloat(graph, "CanvasY", 0f),
0f);
var scale = FrameAnimationWorkspaceState.GetFloat(graph, "CanvasScale", 1f);
graphView.RestoreViewTransform(position, scale);
}
private void RestoreSelection()
@@ -1895,23 +2365,31 @@ namespace AibisDream.FrameAnimation.Editor
FrameAnimationWorkspaceState.SetString(graph, "ValidationKind", validationKindFilter.ToString());
FrameAnimationWorkspaceState.SetString(graph, "SelectionKind", selection.Kind.ToString());
FrameAnimationWorkspaceState.SetString(graph, "SelectionId", GetSelectionId());
FrameAnimationWorkspaceState.SetString(graph, "FocusedFlow", focusedFlowId);
FrameAnimationWorkspaceState.SetString(graph, "NodePreviewPolicy", previewCoordinator.NodePolicy.ToString());
FrameAnimationWorkspaceState.SetString(graph, "PreviewBackground", previewCoordinator.Background.ToString());
FrameAnimationWorkspaceState.SetString(graph, "PreviewZoomMode", previewCoordinator.ZoomMode.ToString());
FrameAnimationWorkspaceState.SetFloat(graph, "PreviewSpeed", previewCoordinator.PreviewSpeed);
FrameAnimationWorkspaceState.SetFloat(graph, "PreviewManualZoom", previewCoordinator.ManualZoom);
}
private void OnProjectDataChanged()
{
previewCoordinator.RefreshTarget(true);
if (graph == null)
{
RestoreLastGraph();
}
if (graph != null)
{
var previouslySelectedFlow = selection.Value as AnimationFlow;
RestoreSelection();
lastSelectedNode = selection.Value as AnimationNode;
if (!graph.Flows.Any(flow => flow != null && flow.Id == focusedFlowId))
if (previouslySelectedFlow != null && graph.Flows.Contains(previouslySelectedFlow))
{
focusedFlowId = string.Empty;
selection = new FrameAnimationEditorSelection(
FrameAnimationEditorSelectionKind.Flow, previouslySelectedFlow);
}
lastSelectedNode = selection.Value as AnimationNode;
RevalidateFlowFocus();
}
RefreshBrowser();
RefreshFlowChoices();