feat: 动画系统第五阶段(未验收)
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user