Files
aibis-dream/Assets/Editor/FrameAnimation/FrameClipEditorWindow.cs
T

361 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(AibisEditorMenus.FrameClipEditor)]
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"));
}
}
}
}