361 lines
16 KiB
C#
361 lines
16 KiB
C#
using System;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEditorInternal;
|
||
using UnityEngine;
|
||
|
||
namespace AibisDream.FrameAnimation.Editor
|
||
{
|
||
public sealed class FrameClipEditorWindow : EditorWindow
|
||
{
|
||
private FrameClip clip;
|
||
private SerializedObject clipSerializedObject;
|
||
private ReorderableList frameList;
|
||
private Vector2 scroll;
|
||
private readonly FrameAnimationPreviewCoordinator preview = new FrameAnimationPreviewCoordinator();
|
||
private FrameClipImportPreview importPreview;
|
||
private double lastUpdateTime;
|
||
|
||
[MenuItem("Window/AibisDream/Frame Animation/Frame Clip Editor")]
|
||
public static void ShowWindow()
|
||
{
|
||
GetWindow<FrameClipEditorWindow>("Frame Clip Editor");
|
||
}
|
||
|
||
public static void Open(FrameClip value)
|
||
{
|
||
var window = GetWindow<FrameClipEditorWindow>("Frame Clip Editor");
|
||
window.SetClip(value);
|
||
window.Show();
|
||
window.Focus();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
EditorApplication.update += OnEditorUpdate;
|
||
Undo.undoRedoPerformed += OnUndoRedo;
|
||
lastUpdateTime = EditorApplication.timeSinceStartup;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
EditorApplication.update -= OnEditorUpdate;
|
||
Undo.undoRedoPerformed -= OnUndoRedo;
|
||
}
|
||
|
||
private void OnUndoRedo()
|
||
{
|
||
RebuildSerializedState();
|
||
preview.RefreshTarget(false);
|
||
Repaint();
|
||
}
|
||
|
||
private void OnEditorUpdate()
|
||
{
|
||
var now = EditorApplication.timeSinceStartup;
|
||
var delta = Math.Max(0d, now - lastUpdateTime);
|
||
lastUpdateTime = now;
|
||
if (preview.Tick(delta, null)) Repaint();
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
var next = (FrameClip)EditorGUILayout.ObjectField("FrameClip", clip, typeof(FrameClip), false);
|
||
if (next != clip) SetClip(next);
|
||
if (clip == null)
|
||
{
|
||
EditorGUILayout.HelpBox("选择一个外部 FrameClip .asset 开始编辑。", MessageType.Info);
|
||
return;
|
||
}
|
||
if (AssetDatabase.IsSubAsset(clip))
|
||
{
|
||
EditorGUILayout.HelpBox("Graph sub-asset Clip 请在 Frame Animation Graph Editor 中编辑。", MessageType.Info);
|
||
if (GUILayout.Button("Open Referencing Graph"))
|
||
{
|
||
var owner = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip).FirstOrDefault();
|
||
if (owner != null) FrameAnimationGraphEditorWindow.Open(owner);
|
||
}
|
||
return;
|
||
}
|
||
|
||
DrawPreview();
|
||
scroll = EditorGUILayout.BeginScrollView(scroll);
|
||
DrawClipProperties();
|
||
EditorGUILayout.Space();
|
||
DrawSourceProperties();
|
||
EditorGUILayout.Space();
|
||
frameList?.DoLayoutList();
|
||
if (clipSerializedObject != null && clipSerializedObject.ApplyModifiedProperties())
|
||
{
|
||
EditorUtility.SetDirty(clip);
|
||
preview.RefreshTarget(false);
|
||
}
|
||
if (!clip.IsImported && !clip.HasStandaloneImportSource &&
|
||
frameList != null && frameList.index >= 0 && GUILayout.Button("Duplicate Selected Frame"))
|
||
{
|
||
var frames = clipSerializedObject.FindProperty("frames");
|
||
frames.InsertArrayElementAtIndex(frameList.index);
|
||
clipSerializedObject.ApplyModifiedProperties();
|
||
EditorUtility.SetDirty(clip);
|
||
preview.RefreshTarget(false);
|
||
}
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawPreview()
|
||
{
|
||
var rect = GUILayoutUtility.GetRect(100f, 260f, GUILayout.ExpandWidth(true));
|
||
EditorGUI.DrawRect(rect, new Color(0.12f, 0.12f, 0.12f));
|
||
var sprite = preview.CurrentSprite;
|
||
if (sprite != null && sprite.texture != null)
|
||
{
|
||
var uv = new Rect(
|
||
sprite.textureRect.x / sprite.texture.width,
|
||
sprite.textureRect.y / sprite.texture.height,
|
||
sprite.textureRect.width / sprite.texture.width,
|
||
sprite.textureRect.height / sprite.texture.height);
|
||
var scale = Mathf.Min(rect.width / sprite.rect.width, rect.height / sprite.rect.height);
|
||
var size = sprite.rect.size * scale;
|
||
var destination = new Rect(rect.center - size * 0.5f, size);
|
||
GUI.DrawTextureWithTexCoords(destination, sprite.texture, uv, true);
|
||
}
|
||
else
|
||
{
|
||
GUI.Label(rect, preview.Error.Code == FrameAnimationPlaybackErrorCode.None
|
||
? "Empty Frame" : preview.Error.Message, EditorStyles.centeredGreyMiniLabel);
|
||
}
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
if (GUILayout.Button(preview.State == FrameAnimationPreviewState.Playing ? "Pause" : "Play"))
|
||
{
|
||
if (preview.State == FrameAnimationPreviewState.Playing) preview.Pause();
|
||
else preview.Play();
|
||
}
|
||
if (GUILayout.Button("Stop")) preview.Stop();
|
||
if (GUILayout.Button("<")) preview.Step(-1);
|
||
if (GUILayout.Button(">")) preview.Step(1);
|
||
}
|
||
var duration = preview.Timeline?.DisplayDurationSeconds ?? 0d;
|
||
var position = EditorGUILayout.Slider("Time", (float)preview.PositionSeconds, 0f,
|
||
Mathf.Max(0.0001f, (float)duration));
|
||
if (!Mathf.Approximately(position, (float)preview.PositionSeconds)) preview.Seek(position);
|
||
}
|
||
|
||
private void DrawClipProperties()
|
||
{
|
||
clipSerializedObject.Update();
|
||
EditorGUILayout.LabelField(clip.HasStandaloneImportSource ? "Source-linked External Clip" : "Manual External Clip",
|
||
EditorStyles.boldLabel);
|
||
var idProperty = clipSerializedObject.FindProperty("id");
|
||
var nextId = EditorGUILayout.DelayedTextField("ID", idProperty.stringValue);
|
||
if (nextId != idProperty.stringValue) RenameClip(nextId);
|
||
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("displayName"));
|
||
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("speed"));
|
||
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("defaultEndBehavior"));
|
||
EditorGUILayout.LabelField("Frame Count", clip.FrameCount.ToString());
|
||
EditorGUILayout.LabelField("Total Duration", clip.TotalDurationMs + " ms");
|
||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||
EditorGUILayout.LabelField("Referencing Graphs", owners.Count.ToString());
|
||
if (owners.Count > 1)
|
||
EditorGUILayout.HelpBox("该 Clip 被多个 Graph 共享,ID 重命名被锁定。", MessageType.Warning);
|
||
if (clipSerializedObject.ApplyModifiedProperties())
|
||
{
|
||
EditorUtility.SetDirty(clip);
|
||
preview.RefreshTarget(false);
|
||
importPreview = null;
|
||
}
|
||
}
|
||
|
||
private void DrawSourceProperties()
|
||
{
|
||
EditorGUILayout.LabelField("Standalone Import Source", EditorStyles.boldLabel);
|
||
if (!clip.HasStandaloneImportSource)
|
||
{
|
||
if (GUILayout.Button("Enable Texture + JSON Source"))
|
||
{
|
||
if (!FrameClipImportService.EnableSource(clip, out var error))
|
||
EditorUtility.DisplayDialog("无法启用来源", error, "确定");
|
||
RebuildSerializedState();
|
||
}
|
||
return;
|
||
}
|
||
|
||
clipSerializedObject.Update();
|
||
var source = clipSerializedObject.FindProperty("standaloneImportSource");
|
||
EditorGUILayout.PropertyField(source.FindPropertyRelative("texture"));
|
||
EditorGUILayout.PropertyField(source.FindPropertyRelative("asepriteJson"));
|
||
EditorGUILayout.PropertyField(source.FindPropertyRelative("pivot"));
|
||
EditorGUILayout.PropertyField(source.FindPropertyRelative("manageSpriteSlicing"));
|
||
using (new EditorGUI.DisabledScope(true))
|
||
{
|
||
EditorGUILayout.PropertyField(source.FindPropertyRelative("lastImportedTagName"));
|
||
EditorGUILayout.PropertyField(source.FindPropertyRelative("lastSourceHash"));
|
||
}
|
||
if (clipSerializedObject.ApplyModifiedProperties())
|
||
{
|
||
EditorUtility.SetDirty(clip);
|
||
importPreview = null;
|
||
}
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
if (GUILayout.Button("Preview Import")) importPreview = FrameClipImportService.Preview(clip);
|
||
if (GUILayout.Button("Refresh"))
|
||
{
|
||
importPreview = FrameClipImportService.Preview(clip);
|
||
ApplyImportPreview();
|
||
}
|
||
if (GUILayout.Button("Detach Source"))
|
||
{
|
||
if (EditorUtility.DisplayDialog("断开来源", "保留当前帧并转为手工 Clip?", "断开", "取消"))
|
||
{
|
||
FrameClipImportService.DetachSource(clip);
|
||
importPreview = null;
|
||
RebuildSerializedState();
|
||
}
|
||
}
|
||
}
|
||
DrawImportPreview();
|
||
}
|
||
|
||
private void DrawImportPreview()
|
||
{
|
||
if (importPreview == null) return;
|
||
EditorGUILayout.LabelField("Import Preview", EditorStyles.boldLabel);
|
||
EditorGUILayout.LabelField("Selected Tag",
|
||
string.IsNullOrEmpty(importPreview.SourceTagName) ? "<all frames>" : importPreview.SourceTagName);
|
||
EditorGUILayout.LabelField("Selected Frames", importPreview.SourceIndices.Count.ToString());
|
||
EditorGUILayout.LabelField("Frame Changes", importPreview.HasFrameChanges ? "Yes" : "No");
|
||
EditorGUILayout.LabelField("Sprite Changes", importPreview.HasSpriteChanges ? "Yes" : "No");
|
||
foreach (var issue in importPreview.Issues)
|
||
{
|
||
var type = issue.Severity == FrameAnimationValidationSeverity.Error
|
||
? MessageType.Error : MessageType.Warning;
|
||
EditorGUILayout.HelpBox($"{issue.Code}: {issue.Message}", type);
|
||
}
|
||
}
|
||
|
||
private void ApplyImportPreview()
|
||
{
|
||
if (importPreview == null || importPreview.HasErrors) return;
|
||
var allow = !importPreview.HasSpriteChanges || EditorUtility.DisplayDialog(
|
||
"确认修改 TextureImporter",
|
||
"独立 Clip 刷新需要更新 Sprite 切图。是否继续?",
|
||
"应用切图并刷新",
|
||
"取消");
|
||
if (!allow) return;
|
||
if (!FrameClipImportService.Apply(importPreview, true, out var error))
|
||
{
|
||
EditorUtility.DisplayDialog("刷新失败", error, "确定");
|
||
return;
|
||
}
|
||
importPreview = FrameClipImportService.Preview(clip);
|
||
RebuildSerializedState();
|
||
preview.RefreshTarget(false);
|
||
}
|
||
|
||
private void RenameClip(string newId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(newId))
|
||
{
|
||
EditorUtility.DisplayDialog("无法重命名", "Clip ID 不能为空。", "确定");
|
||
return;
|
||
}
|
||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||
if (owners.Count > 1)
|
||
{
|
||
EditorUtility.DisplayDialog("无法重命名", "共享外部 Clip 被多个 Graph 引用。", "确定");
|
||
return;
|
||
}
|
||
if (owners.Count == 1)
|
||
{
|
||
if (!FrameAnimationAssetOperations.RenameClip(owners[0], clip, newId, out var error))
|
||
EditorUtility.DisplayDialog("无法重命名", error, "确定");
|
||
return;
|
||
}
|
||
Undo.RecordObject(clip, "Rename Standalone Frame Clip");
|
||
clip.SetId(newId);
|
||
EditorUtility.SetDirty(clip);
|
||
}
|
||
|
||
private void SetClip(FrameClip value)
|
||
{
|
||
clip = value;
|
||
importPreview = null;
|
||
RebuildSerializedState();
|
||
preview.SetGraph(null);
|
||
if (clip != null) preview.SetTarget(FrameAnimationPreviewTargetKind.Clip, clip);
|
||
else preview.ClearTarget();
|
||
Repaint();
|
||
}
|
||
|
||
private void RebuildSerializedState()
|
||
{
|
||
clipSerializedObject = clip != null ? new SerializedObject(clip) : null;
|
||
frameList = clip != null ? BuildFrameList() : null;
|
||
}
|
||
|
||
private ReorderableList BuildFrameList()
|
||
{
|
||
var frames = clipSerializedObject.FindProperty("frames");
|
||
var editable = !clip.IsImported && !clip.HasStandaloneImportSource;
|
||
var list = new ReorderableList(clipSerializedObject, frames, editable, true, editable, editable)
|
||
{
|
||
elementHeight = 72f,
|
||
drawHeaderCallback = rect => EditorGUI.LabelField(rect,
|
||
editable ? "Frames" : "Frames (source-linked, read-only)"),
|
||
drawElementCallback = (rect, index, active, focused) => DrawFrame(frames, rect, index),
|
||
onAddCallback = value =>
|
||
{
|
||
var index = frames.arraySize;
|
||
frames.InsertArrayElementAtIndex(index);
|
||
var element = frames.GetArrayElementAtIndex(index);
|
||
element.FindPropertyRelative("sprite").objectReferenceValue = null;
|
||
element.FindPropertyRelative("durationMs").intValue = 100;
|
||
element.FindPropertyRelative("frameName").stringValue = string.Empty;
|
||
element.FindPropertyRelative("sourceIndex").intValue = -1;
|
||
clipSerializedObject.ApplyModifiedProperties();
|
||
value.index = index;
|
||
EditorUtility.SetDirty(clip);
|
||
preview.RefreshTarget(false);
|
||
},
|
||
onRemoveCallback = value =>
|
||
{
|
||
ReorderableList.defaultBehaviours.DoRemoveButton(value);
|
||
clipSerializedObject.ApplyModifiedProperties();
|
||
EditorUtility.SetDirty(clip);
|
||
preview.RefreshTarget(false);
|
||
},
|
||
onReorderCallback = _ =>
|
||
{
|
||
clipSerializedObject.ApplyModifiedProperties();
|
||
EditorUtility.SetDirty(clip);
|
||
preview.RefreshTarget(false);
|
||
}
|
||
};
|
||
return list;
|
||
}
|
||
|
||
private static void DrawFrame(SerializedProperty frames, Rect rect, int index)
|
||
{
|
||
if (index < 0 || index >= frames.arraySize) return;
|
||
var element = frames.GetArrayElementAtIndex(index);
|
||
rect.y += 2f;
|
||
var line = EditorGUIUtility.singleLineHeight;
|
||
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, line),
|
||
element.FindPropertyRelative("sprite"), new GUIContent($"#{index} Sprite"));
|
||
EditorGUI.PropertyField(new Rect(rect.x, rect.y + line + 2f, rect.width, line),
|
||
element.FindPropertyRelative("durationMs"));
|
||
using (new EditorGUI.DisabledScope(true))
|
||
{
|
||
var half = (rect.width - 4f) * 0.5f;
|
||
EditorGUI.PropertyField(new Rect(rect.x, rect.y + (line + 2f) * 2f, half, line),
|
||
element.FindPropertyRelative("frameName"));
|
||
EditorGUI.PropertyField(new Rect(rect.x + half + 4f, rect.y + (line + 2f) * 2f, half, line),
|
||
element.FindPropertyRelative("sourceIndex"));
|
||
}
|
||
}
|
||
}
|
||
}
|