feat(chapter): 添加章节图可视化编辑器
基于 UIToolkit 实现 ChapterGraph 编辑器窗口,可视化查看 和编辑 TalkSceneSO 之间的多出口连接关系。
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace AibisDream.SystemEditor
|
||||
{
|
||||
public class ChapterNode : Node
|
||||
{
|
||||
public TalkSceneSO SceneSo { get; }
|
||||
public Port InputPort { get; private set; }
|
||||
|
||||
private const float PortLabelMaxWidth = 120f;
|
||||
|
||||
public ChapterNode(TalkSceneSO so)
|
||||
{
|
||||
SceneSo = so ?? throw new ArgumentNullException(nameof(so));
|
||||
|
||||
// 节点整体放大
|
||||
style.minWidth = 240;
|
||||
style.paddingTop = 8;
|
||||
style.paddingBottom = 8;
|
||||
style.paddingLeft = 10;
|
||||
style.paddingRight = 10;
|
||||
|
||||
UpdateTitle();
|
||||
UpdateColor();
|
||||
BuildInputPort();
|
||||
BuildOutputPorts();
|
||||
BuildSubtitle();
|
||||
BuildAddExitButton();
|
||||
SetPosition(new Rect(so.graphPosition, Vector2.zero));
|
||||
|
||||
// 放大标题
|
||||
var titleLabel = this.Q<Label>("title-label");
|
||||
if (titleLabel != null)
|
||||
{
|
||||
titleLabel.style.fontSize = 16;
|
||||
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateTitle()
|
||||
{
|
||||
title = string.IsNullOrEmpty(SceneSo.title) ? SceneSo.name : SceneSo.title;
|
||||
}
|
||||
|
||||
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
|
||||
{
|
||||
base.BuildContextualMenu(evt);
|
||||
|
||||
if (SceneSo.exits != null && SceneSo.exits.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var exit = SceneSo.exits[i];
|
||||
var name = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
|
||||
var idx = i;
|
||||
evt.menu.AppendAction($"删除出口/{name}", _ => RemoveExitAt(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateColor()
|
||||
{
|
||||
style.backgroundColor = new StyleColor(SceneSo.nodeColor);
|
||||
}
|
||||
|
||||
private void BuildInputPort()
|
||||
{
|
||||
InputPort = Port.Create<Edge>(
|
||||
Orientation.Horizontal,
|
||||
Direction.Input,
|
||||
Port.Capacity.Single,
|
||||
typeof(TalkSceneSO));
|
||||
InputPort.portName = "▶";
|
||||
InputPort.style.fontSize = 14;
|
||||
InputPort.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
|
||||
inputContainer.Add(InputPort);
|
||||
}
|
||||
|
||||
private void BuildOutputPorts()
|
||||
{
|
||||
outputContainer.Clear();
|
||||
if (SceneSo.exits == null) return;
|
||||
|
||||
for (int i = 0; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var exit = SceneSo.exits[i];
|
||||
var port = CreateOutputPort(exit, i);
|
||||
outputContainer.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
private Port CreateOutputPort(SceneExit exit, int index)
|
||||
{
|
||||
var port = Port.Create<Edge>(
|
||||
Orientation.Horizontal,
|
||||
Direction.Output,
|
||||
Port.Capacity.Single,
|
||||
typeof(TalkSceneSO));
|
||||
|
||||
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{index}" : exit.exitName;
|
||||
port.userData = index;
|
||||
|
||||
// 限制端口标签宽度并放大字体
|
||||
var label = port.Q<Label>("type");
|
||||
if (label != null)
|
||||
{
|
||||
label.style.maxWidth = PortLabelMaxWidth;
|
||||
label.style.fontSize = 12;
|
||||
}
|
||||
|
||||
port.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
|
||||
|
||||
port.RegisterCallback<ContextualMenuPopulateEvent>((evt) =>
|
||||
{
|
||||
var currentIndex = port.userData is int idx ? idx : -1;
|
||||
evt.menu.AppendAction("删除出口", (_) => RemoveExitAt(currentIndex));
|
||||
}, TrickleDown.TrickleDown);
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
private void BuildSubtitle()
|
||||
{
|
||||
var subtitleLabel = new Label($"{SceneSo.name}")
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 12,
|
||||
unityTextAlign = TextAnchor.MiddleCenter,
|
||||
color = new Color(0.8f, 0.8f, 0.8f),
|
||||
paddingTop = 4,
|
||||
paddingBottom = 2
|
||||
}
|
||||
};
|
||||
extensionContainer.Add(subtitleLabel);
|
||||
RefreshExpandedState();
|
||||
}
|
||||
|
||||
private void BuildAddExitButton()
|
||||
{
|
||||
var removeButton = new Button(() => RemoveLastExit())
|
||||
{
|
||||
text = "-",
|
||||
style =
|
||||
{
|
||||
width = 26,
|
||||
height = 22,
|
||||
fontSize = 16,
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginRight = 4
|
||||
}
|
||||
};
|
||||
titleButtonContainer.Add(removeButton);
|
||||
|
||||
var addButton = new Button(() => AddExit())
|
||||
{
|
||||
text = "+",
|
||||
style = { width = 26, height = 22, fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold }
|
||||
};
|
||||
titleButtonContainer.Add(addButton);
|
||||
}
|
||||
|
||||
private void RemoveLastExit()
|
||||
{
|
||||
if (SceneSo.exits == null || SceneSo.exits.Count == 0) return;
|
||||
RemoveExitAt(SceneSo.exits.Count - 1);
|
||||
}
|
||||
|
||||
public void AddExit()
|
||||
{
|
||||
Undo.RecordObject(SceneSo, "Add Exit");
|
||||
if (SceneSo.exits == null)
|
||||
SceneSo.exits = new System.Collections.Generic.List<SceneExit>();
|
||||
|
||||
var newExit = new SceneExit
|
||||
{
|
||||
exitName = $"exit_{SceneSo.exits.Count}"
|
||||
};
|
||||
SceneSo.exits.Add(newExit);
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
|
||||
var newPort = CreateOutputPort(newExit, SceneSo.exits.Count - 1);
|
||||
outputContainer.Add(newPort);
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public void RemoveExitAt(int index)
|
||||
{
|
||||
if (SceneSo.exits == null || index < 0 || index >= SceneSo.exits.Count) return;
|
||||
|
||||
// 断开并移除该端口的所有边
|
||||
var portToRemove = GetOutputPort(index);
|
||||
if (portToRemove != null)
|
||||
{
|
||||
var edges = portToRemove.connections.ToList();
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
edge.input?.Disconnect(edge);
|
||||
edge.output?.Disconnect(edge);
|
||||
edge.RemoveFromHierarchy();
|
||||
}
|
||||
portToRemove.RemoveFromHierarchy();
|
||||
}
|
||||
|
||||
Undo.RecordObject(SceneSo, "Remove Exit");
|
||||
SceneSo.exits.RemoveAt(index);
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
|
||||
// 更新后续端口的 userData 和 portName
|
||||
for (int i = index; i < SceneSo.exits.Count; i++)
|
||||
{
|
||||
var port = GetOutputPort(i);
|
||||
if (port != null)
|
||||
{
|
||||
port.userData = i;
|
||||
var exit = SceneSo.exits[i];
|
||||
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public void RefreshOutputPorts()
|
||||
{
|
||||
// 先断开所有输出端口的边
|
||||
var edgesToRemove = outputContainer.Children()
|
||||
.OfType<Port>()
|
||||
.SelectMany(p => p.connections.ToList())
|
||||
.ToList();
|
||||
|
||||
foreach (var edge in edgesToRemove)
|
||||
{
|
||||
edge.input?.Disconnect(edge);
|
||||
edge.output?.Disconnect(edge);
|
||||
edge.RemoveFromHierarchy();
|
||||
}
|
||||
|
||||
BuildOutputPorts();
|
||||
RefreshPorts();
|
||||
}
|
||||
|
||||
public Port GetOutputPort(int index)
|
||||
{
|
||||
return outputContainer.Children().OfType<Port>().ElementAtOrDefault(index);
|
||||
}
|
||||
|
||||
public override void SetPosition(Rect newPos)
|
||||
{
|
||||
base.SetPosition(newPos);
|
||||
if (SceneSo != null)
|
||||
{
|
||||
SceneSo.graphPosition = newPos.position;
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncPositionToSo()
|
||||
{
|
||||
if (SceneSo == null) return;
|
||||
var pos = GetPosition();
|
||||
if (SceneSo.graphPosition != pos.position)
|
||||
{
|
||||
SceneSo.graphPosition = pos.position;
|
||||
EditorUtility.SetDirty(SceneSo);
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyEdgeListener : IEdgeConnectorListener
|
||||
{
|
||||
public void OnDropOutsidePort(Edge edge, Vector2 position) { }
|
||||
public void OnDrop(GraphView graphView, Edge edge) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user