feat(chapter): 添加章节图可视化编辑器

基于 UIToolkit 实现 ChapterGraph 编辑器窗口,可视化查看
和编辑 TalkSceneSO 之间的多出口连接关系。
This commit is contained in:
2026-04-25 20:13:55 +08:00
parent 93012f5f7c
commit 94d89330dc
11 changed files with 1217 additions and 0 deletions
@@ -0,0 +1,267 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphEditorWindow : EditorWindow
{
private ChapterGraphView _graphView;
private ChapterGraphInspector _inspector;
private IMGUIContainer _inspectorContainer;
private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder";
private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder";
private string _rootFolder;
private string _currentFolder;
private DropdownField _folderDropdown;
private readonly Dictionary<string, string> _displayToPath = new();
[MenuItem("Window/Chapter Graph Editor")]
public static void ShowWindow()
{
var window = GetWindow<ChapterGraphEditorWindow>("Chapter Graph Editor");
window.minSize = new Vector2(800, 600);
}
private void OnEnable()
{
if (_graphView == null) return;
_graphView.LoadGraph(_currentFolder);
}
private void OnDisable()
{
if (_graphView != null)
{
_graphView.OnNodeDeleted -= OnNodeDeleted;
}
if (_inspector != null)
{
_inspector.OnColorChanged -= OnNodeColorChanged;
_inspector.Destroy();
}
}
private void CreateGUI()
{
// 根容器:纵向排列
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Column;
// 顶部工具栏
var toolbar = new Toolbar();
toolbar.Add(CreateToolbarButton("Save", OnSaveClicked));
toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked));
toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked));
_rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO");
_folderDropdown = new DropdownField("路径");
_folderDropdown.style.width = 160;
_folderDropdown.style.marginLeft = 8;
_folderDropdown.style.marginRight = 4;
RefreshFolderChoices();
_folderDropdown.RegisterValueChangedCallback(OnFolderChanged);
toolbar.Add(_folderDropdown);
var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." };
setRootBtn.style.width = 28;
toolbar.Add(setRootBtn);
toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } });
toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked));
root.Add(toolbar);
// 主内容区:横向分栏
var mainContainer = new VisualElement();
mainContainer.style.flexGrow = 1;
mainContainer.style.flexDirection = FlexDirection.Row;
mainContainer.style.overflow = Overflow.Hidden;
// 左侧 Inspector 面板
var leftPanel = new VisualElement();
leftPanel.style.width = 320;
leftPanel.style.minWidth = 200;
leftPanel.style.maxWidth = 500;
leftPanel.style.flexShrink = 0;
leftPanel.style.flexDirection = FlexDirection.Column;
leftPanel.style.borderRightWidth = 1;
leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
// Inspector 标题
var header = new Label("章节属性")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
fontSize = 14,
paddingTop = 8,
paddingBottom = 8,
paddingLeft = 12,
paddingRight = 12,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
}
};
leftPanel.Add(header);
// IMGUI Inspector
_inspector = new ChapterGraphInspector();
_inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI());
_inspectorContainer.style.flexGrow = 1;
leftPanel.Add(_inspectorContainer);
mainContainer.Add(leftPanel);
// 拖拽条
var resizer = new VisualElement();
resizer.style.width = 5;
resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
mainContainer.Add(resizer);
float startWidth = 0;
float startMouseX = 0;
resizer.RegisterCallback<MouseDownEvent>(evt =>
{
startWidth = leftPanel.resolvedStyle.width;
startMouseX = evt.mousePosition.x;
resizer.CaptureMouse();
evt.StopPropagation();
});
resizer.RegisterCallback<MouseMoveEvent>(evt =>
{
if (!resizer.HasMouseCapture()) return;
var delta = evt.mousePosition.x - startMouseX;
var newWidth = Mathf.Clamp(startWidth + delta, 200, 600);
leftPanel.style.width = newWidth;
});
resizer.RegisterCallback<MouseUpEvent>(evt =>
{
if (resizer.HasMouseCapture())
resizer.ReleaseMouse();
});
// 右侧 GraphView
_graphView = new ChapterGraphView();
_graphView.style.flexGrow = 1;
mainContainer.Add(_graphView);
root.Add(mainContainer);
// 加载图数据
_graphView.LoadGraph(_currentFolder);
// 选中节点同步到 Inspector
_graphView.OnNodeSelected += OnNodeSelected;
_graphView.OnNodeDeleted += OnNodeDeleted;
_inspector.OnColorChanged += OnNodeColorChanged;
}
private void OnNodeDeleted(TalkSceneSO so)
{
_inspector?.SetTarget(null);
_inspectorContainer?.MarkDirtyRepaint();
}
private void OnNodeColorChanged(TalkSceneSO so)
{
_graphView?.UpdateNodeColor(so);
}
private Button CreateToolbarButton(string text, System.Action onClick)
{
var btn = new Button(onClick) { text = text };
return btn;
}
private void OnSaveClicked()
{
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定");
}
private void OnAutoLayoutClicked()
{
_graphView?.AutoLayout();
}
private void OnValidateClicked()
{
_graphView?.ValidateGraph();
}
private void OnCreateChapterClicked()
{
var center = new Vector2(position.width / 2f, position.height / 2f);
var localPos = _graphView.contentViewContainer.WorldToLocal(center);
var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder);
_graphView?.CreateChapterNodeAt(localPos, defaultFolder);
}
private void OnNodeSelected(TalkSceneSO so)
{
_inspector?.SetTarget(so);
_inspectorContainer?.MarkDirtyRepaint();
}
private void RefreshFolderChoices()
{
_displayToPath.Clear();
_displayToPath["All"] = null;
var subFolders = AssetDatabase.GetSubFolders(_rootFolder);
foreach (var folder in subFolders)
{
var name = Path.GetFileName(folder);
_displayToPath[name] = folder;
}
var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder })
.Select(g => AssetDatabase.GUIDToAssetPath(g))
.Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder);
if (rootHasSO)
{
_displayToPath["(Root)"] = _rootFolder;
}
_folderDropdown.choices = _displayToPath.Keys.ToList();
var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All");
_folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All";
_currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null;
}
private void OnFolderChanged(ChangeEvent<string> evt)
{
_currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null;
EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue);
_graphView?.LoadGraph(_currentFolder);
}
private void OnSetRootFolderClicked()
{
var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, "");
if (string.IsNullOrEmpty(path)) return;
var absoluteRoot = Application.dataPath.Replace('\\', '/');
var selected = path.Replace('\\', '/');
if (!selected.StartsWith(absoluteRoot))
{
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定");
return;
}
_rootFolder = "Assets" + selected.Substring(absoluteRoot.Length);
EditorPrefs.SetString(RootFolderPrefKey, _rootFolder);
RefreshFolderChoices();
_graphView?.LoadGraph(_currentFolder);
}
}
}