Merge branch 'develop' into feature/帧动画编辑器重构
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using AibisDream.Utility;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
using Yarn.Markup;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream.SystemEditor.Tests
|
||||
{
|
||||
public sealed class AutoNextMetadataTests
|
||||
{
|
||||
[Test]
|
||||
public void AutoNextWithoutParameter_UsesExistingFixedDelay()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoNextWithParameter_UsesSpecifiedSeconds()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoNextWithZeroDelay_IsValid()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:0"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoNextParameter_UsesInvariantCulture()
|
||||
{
|
||||
var originalCulture = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5"));
|
||||
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("auto_next:")]
|
||||
[TestCase("auto_next:abc")]
|
||||
[TestCase("auto_next:-1")]
|
||||
[TestCase("auto_next:NaN")]
|
||||
[TestCase("auto_next:Infinity")]
|
||||
[TestCase("auto_next:2147484")]
|
||||
public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata)
|
||||
{
|
||||
LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效"));
|
||||
|
||||
var lineInfo = LineInfo.Generate(CreateLine(metadata));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.Null);
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay));
|
||||
}
|
||||
|
||||
[TestCase("auto_next_extra")]
|
||||
[TestCase("auto_next_extra:4.5")]
|
||||
[TestCase("AUTO_NEXT")]
|
||||
[TestCase("AUTO_NEXT:4.5")]
|
||||
public void SimilarMetadata_IsNotRecognized(string metadata)
|
||||
{
|
||||
var line = CreateLine(metadata);
|
||||
|
||||
Assert.That(line.IsAutoSkipLine(), Is.False);
|
||||
Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParameterizedTag_TakesPrecedenceOverPlainTag()
|
||||
{
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5"));
|
||||
|
||||
Assert.That(lineInfo.isAutoSkip, Is.True);
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MultipleParameterizedTags_UseFirstAndWarn()
|
||||
{
|
||||
LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签"));
|
||||
|
||||
var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5"));
|
||||
|
||||
Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f));
|
||||
Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500));
|
||||
}
|
||||
|
||||
private static LocalizedLine CreateLine(params string[] metadata)
|
||||
{
|
||||
return new LocalizedLine
|
||||
{
|
||||
TextID = "line:auto-next-test",
|
||||
Metadata = metadata,
|
||||
Text = new MarkupParseResult("Test line", new List<MarkupAttribute>())
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d476e493adf4855468ce46409dae6661
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -122,7 +122,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
if (clip == null || !clip.IsImported)
|
||||
{
|
||||
return "Manual";
|
||||
return clip != null && clip.HasStandaloneImportSource ? "Standalone Source" : "Manual";
|
||||
}
|
||||
var sourceId = clip.ImportInfo?.ImportSourceId;
|
||||
if (sourceNames != null && !string.IsNullOrEmpty(sourceId) &&
|
||||
@@ -232,7 +232,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
if (clip.IsImported || AssetDatabase.IsSubAsset(clip) ||
|
||||
string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip)))
|
||||
{
|
||||
error = "只能添加独立 .asset 形式的 Manual Clip。";
|
||||
error = "只能添加独立 .asset 形式、且不由 Graph ImportSource 管理的 Clip。";
|
||||
return false;
|
||||
}
|
||||
if (graph.Clips.Contains(clip))
|
||||
|
||||
@@ -403,7 +403,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
|
||||
public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node)
|
||||
{
|
||||
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "Flow";
|
||||
var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "_Flow";
|
||||
var candidate = root;
|
||||
var suffix = 2;
|
||||
while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate))
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
private SerializedObject clipSerializedObject;
|
||||
private FrameClip frameListClip;
|
||||
private ReorderableList frameList;
|
||||
private bool frameListEditable;
|
||||
private readonly FrameAnimationPreviewCoordinator previewCoordinator = new FrameAnimationPreviewCoordinator();
|
||||
private readonly List<Slider> previewTimelineSliders = new List<Slider>();
|
||||
private readonly List<Label> previewTimeLabels = new List<Label>();
|
||||
@@ -2276,7 +2277,10 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
EnsureFrameList(clip);
|
||||
clipSerializedObject.Update();
|
||||
EditorGUILayout.LabelField(clip.IsImported ? "Imported Clip" : "Manual Clip", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField(
|
||||
clip.IsImported ? "Graph Imported Clip" :
|
||||
clip.HasStandaloneImportSource ? "Source-linked External Clip" : "Manual Clip",
|
||||
EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.PropertyField(clipSerializedObject.FindProperty("id"));
|
||||
@@ -2297,14 +2301,29 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
EditorGUILayout.LabelField("Source Tag", clip.ImportInfo.SourceTagName);
|
||||
EditorGUILayout.LabelField("Missing", clip.ImportInfo.IsMissingFromSource ? "Yes" : "No");
|
||||
}
|
||||
else if (clip.HasStandaloneImportSource)
|
||||
{
|
||||
EditorGUILayout.LabelField("Source Tag",
|
||||
string.IsNullOrEmpty(clip.StandaloneImportSource.LastImportedTagName)
|
||||
? "<all frames>"
|
||||
: clip.StandaloneImportSource.LastImportedTagName);
|
||||
if (GUILayout.Button("Open Standalone Clip Editor"))
|
||||
{
|
||||
FrameClipEditorWindow.Open(clip);
|
||||
}
|
||||
}
|
||||
if (clipSerializedObject.ApplyModifiedProperties())
|
||||
{
|
||||
EditorUtility.SetDirty(clip);
|
||||
}
|
||||
|
||||
if (!clip.IsImported)
|
||||
if (!clip.IsImported && !clip.HasStandaloneImportSource)
|
||||
{
|
||||
frameList.DoLayoutList();
|
||||
if (clipSerializedObject.ApplyModifiedProperties())
|
||||
{
|
||||
EditorUtility.SetDirty(clip);
|
||||
}
|
||||
if (frameList.index >= 0 && GUILayout.Button("Duplicate Selected Frame"))
|
||||
{
|
||||
DuplicateFrame(clipSerializedObject.FindProperty("frames"), frameList.index);
|
||||
@@ -2333,6 +2352,13 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (clip.HasStandaloneImportSource)
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
frameList.DoLayoutList();
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Rename Clip ID"))
|
||||
{
|
||||
@@ -2350,17 +2376,20 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
|
||||
private void EnsureFrameList(FrameClip clip)
|
||||
{
|
||||
if (frameListClip == clip && frameList != null)
|
||||
var editable = !clip.IsImported && !clip.HasStandaloneImportSource;
|
||||
if (frameListClip == clip && frameList != null && frameListEditable == editable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
frameListClip = clip;
|
||||
frameListEditable = editable;
|
||||
clipSerializedObject = new SerializedObject(clip);
|
||||
var frames = clipSerializedObject.FindProperty("frames");
|
||||
frameList = new ReorderableList(clipSerializedObject, frames, !clip.IsImported, true, !clip.IsImported, !clip.IsImported)
|
||||
frameList = new ReorderableList(clipSerializedObject, frames, editable, true, editable, editable)
|
||||
{
|
||||
elementHeight = 72f,
|
||||
drawHeaderCallback = rect => EditorGUI.LabelField(rect, "Frames (frameName/sourceIndex are read-only)"),
|
||||
drawHeaderCallback = rect => EditorGUI.LabelField(rect,
|
||||
editable ? "Frames (frameName/sourceIndex are read-only)" : "Frames (source-managed, read-only)"),
|
||||
drawElementCallback = (rect, index, active, focused) => DrawFrameElement(frames, rect, index),
|
||||
onAddCallback = list => AddFrame(frames),
|
||||
onRemoveCallback = list =>
|
||||
@@ -3337,6 +3366,11 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
if (asset is FrameClip clip)
|
||||
{
|
||||
if (!AssetDatabase.IsSubAsset(clip))
|
||||
{
|
||||
FrameClipEditorWindow.Open(clip);
|
||||
return true;
|
||||
}
|
||||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||||
if (owners.Count == 1)
|
||||
{
|
||||
|
||||
@@ -273,7 +273,13 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationImportSourcePreview
|
||||
internal interface IFrameAnimationImportPreviewSink
|
||||
{
|
||||
void AddIssue(FrameAnimationImportIssue issue);
|
||||
void AddSpriteDiff(FrameAnimationSpriteDiff diff);
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationImportSourcePreview : IFrameAnimationImportPreviewSink
|
||||
{
|
||||
private readonly List<FrameAnimationImportIssue> issues = new List<FrameAnimationImportIssue>();
|
||||
private readonly List<FrameAnimationClipDiff> clipDiffs = new List<FrameAnimationClipDiff>();
|
||||
|
||||
@@ -434,6 +434,13 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
source.ManageSpriteSlicing && source.Texture != null).Select(source => (currentGraph, source)));
|
||||
}
|
||||
|
||||
var standaloneOwners = AssetDatabase.FindAssets("t:FrameClip")
|
||||
.Select(guid => AssetDatabase.LoadAssetAtPath<FrameClip>(AssetDatabase.GUIDToAssetPath(guid)))
|
||||
.Where(clip => clip?.StandaloneImportSource != null &&
|
||||
clip.StandaloneImportSource.ManageSpriteSlicing &&
|
||||
clip.StandaloneImportSource.Texture != null)
|
||||
.ToArray();
|
||||
|
||||
foreach (var pair in targetPreviews.Where(pair => pair.Key.ManageSpriteSlicing && pair.Key.Texture != null))
|
||||
{
|
||||
var conflicts = owners.Where(item => item.source.Texture == pair.Key.Texture &&
|
||||
@@ -444,6 +451,14 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
$"Texture '{pair.Key.Texture.name}' 已被其他可写 ImportSource 占用:" +
|
||||
string.Join(", ", conflicts.Select(item => $"{item.graph.name}/{item.source.DisplayName}"))));
|
||||
}
|
||||
var standaloneConflicts = standaloneOwners.Where(clip =>
|
||||
clip.StandaloneImportSource.Texture == pair.Key.Texture).ToArray();
|
||||
if (standaloneConflicts.Length > 0)
|
||||
{
|
||||
pair.Value.AddIssue(Error(pair.Key, FrameAnimationImportIssueCode.TextureOwnershipConflict,
|
||||
$"Texture '{pair.Key.Texture.name}' 已被独立 Clip 来源占用:" +
|
||||
string.Join(", ", standaloneConflicts.Select(clip => clip.name))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
var spriteRenderer = player.GetComponent<SpriteRenderer>();
|
||||
var image = player.GetComponent<Image>();
|
||||
|
||||
if (player.GetComponent<FrameClipPlayer>() != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"同一 GameObject 不能同时包含 FrameAnimationPlayer 和 FrameClipPlayer。",
|
||||
MessageType.Error);
|
||||
}
|
||||
|
||||
if (spriteRenderer == null && image == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
|
||||
@@ -526,7 +526,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
var resolved = TargetKind switch
|
||||
{
|
||||
FrameAnimationPreviewTargetKind.Clip when target is FrameClip clip =>
|
||||
FrameAnimationResolver.TryResolve(graph, clip.Id, default, out plan, out var clipError)
|
||||
TryResolvePreviewClip(clip, out plan, out var clipError)
|
||||
? FrameAnimationPlaybackError.None : clipError,
|
||||
FrameAnimationPreviewTargetKind.Node when target is AnimationNode node =>
|
||||
FrameAnimationResolver.TryResolveNode(graph, node, out plan, out var nodeError)
|
||||
@@ -552,6 +552,16 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolvePreviewClip(
|
||||
FrameClip clip,
|
||||
out ResolvedPlaybackPlan resolvedPlan,
|
||||
out FrameAnimationPlaybackError error)
|
||||
{
|
||||
return graph != null
|
||||
? FrameAnimationResolver.TryResolve(graph, clip.Id, default, out resolvedPlan, out error)
|
||||
: FrameAnimationResolver.TryResolveClip(clip, default, out resolvedPlan, out error);
|
||||
}
|
||||
|
||||
private void ApplyEvaluation(FrameAnimationSessionEvaluation evaluation)
|
||||
{
|
||||
if (evaluation.IsFailed)
|
||||
|
||||
@@ -10,6 +10,36 @@ using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal readonly struct FrameAnimationSpriteSourceContext
|
||||
{
|
||||
public string SourceId { get; }
|
||||
public Texture2D Texture { get; }
|
||||
public TextAsset AsepriteJson { get; }
|
||||
public Vector2 Pivot { get; }
|
||||
public bool ManageSpriteSlicing { get; }
|
||||
|
||||
public FrameAnimationSpriteSourceContext(
|
||||
string sourceId,
|
||||
Texture2D texture,
|
||||
TextAsset asepriteJson,
|
||||
Vector2 pivot,
|
||||
bool manageSpriteSlicing)
|
||||
{
|
||||
SourceId = sourceId ?? string.Empty;
|
||||
Texture = texture;
|
||||
AsepriteJson = asepriteJson;
|
||||
Pivot = pivot;
|
||||
ManageSpriteSlicing = manageSpriteSlicing;
|
||||
}
|
||||
|
||||
public static FrameAnimationSpriteSourceContext From(FrameAnimationImportSource source)
|
||||
{
|
||||
return new FrameAnimationSpriteSourceContext(
|
||||
source?.InternalId, source?.Texture, source?.AsepriteJson,
|
||||
source?.Pivot ?? new Vector2(0.5f, 0.5f), source?.ManageSpriteSlicing == true);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrameAnimationSpritePlan
|
||||
{
|
||||
public string TexturePath { get; }
|
||||
@@ -91,6 +121,15 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
FrameAnimationImportSource source,
|
||||
AsepriteSourceDocument document,
|
||||
FrameAnimationImportSourcePreview preview)
|
||||
{
|
||||
return BuildPlan(FrameAnimationSpriteSourceContext.From(source), document, preview);
|
||||
}
|
||||
|
||||
public static FrameAnimationSpritePlan BuildPlan(
|
||||
FrameAnimationSpriteSourceContext source,
|
||||
AsepriteSourceDocument document,
|
||||
IFrameAnimationImportPreviewSink preview,
|
||||
IReadOnlyCollection<int> includedSourceIndices = null)
|
||||
{
|
||||
var texturePath = AssetDatabase.GetAssetPath(source.Texture);
|
||||
var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
|
||||
@@ -105,8 +144,8 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
? Array.Empty<SpriteRect>()
|
||||
: ReadSpriteRects(importer);
|
||||
return source.ManageSpriteSlicing
|
||||
? BuildWritablePlan(source, document, preview, texturePath, existingRects)
|
||||
: BuildReadOnlyPlan(source, document, preview, texturePath, existingRects);
|
||||
? BuildWritablePlan(source, document, preview, texturePath, existingRects, includedSourceIndices)
|
||||
: BuildReadOnlyPlan(source, document, preview, texturePath, existingRects, includedSourceIndices);
|
||||
}
|
||||
|
||||
public static Rect ToUnityRect(RectInt asepriteRect, int textureHeight)
|
||||
@@ -175,7 +214,14 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
FrameAnimationImportSource source,
|
||||
IReadOnlyList<SpriteRect> spriteRects = null)
|
||||
{
|
||||
if (source == null || source.Texture == null || source.AsepriteJson == null)
|
||||
return ComputeSourceHash(FrameAnimationSpriteSourceContext.From(source), spriteRects);
|
||||
}
|
||||
|
||||
public static string ComputeSourceHash(
|
||||
FrameAnimationSpriteSourceContext source,
|
||||
IReadOnlyList<SpriteRect> spriteRects = null)
|
||||
{
|
||||
if (source.Texture == null || source.AsepriteJson == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -214,17 +260,18 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
|
||||
private static FrameAnimationSpritePlan BuildWritablePlan(
|
||||
FrameAnimationImportSource source,
|
||||
FrameAnimationSpriteSourceContext source,
|
||||
AsepriteSourceDocument document,
|
||||
FrameAnimationImportSourcePreview preview,
|
||||
IFrameAnimationImportPreviewSink preview,
|
||||
string texturePath,
|
||||
IReadOnlyList<SpriteRect> existingRects)
|
||||
IReadOnlyList<SpriteRect> existingRects,
|
||||
IReadOnlyCollection<int> includedSourceIndices)
|
||||
{
|
||||
var planned = CloneRects(existingRects).ToList();
|
||||
var originalRects = planned.ToArray();
|
||||
var assigned = new HashSet<SpriteRect>();
|
||||
var spriteNamesBySourceIndex = new Dictionary<int, string>();
|
||||
foreach (var slot in document.SpriteSlots)
|
||||
foreach (var slot in EnumerateSlots(document, includedSourceIndices))
|
||||
{
|
||||
var unityRect = ToUnityRect(slot.Rect, source.Texture.height);
|
||||
SpriteRect matched = null;
|
||||
@@ -331,15 +378,16 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
|
||||
private static FrameAnimationSpritePlan BuildReadOnlyPlan(
|
||||
FrameAnimationImportSource source,
|
||||
FrameAnimationSpriteSourceContext source,
|
||||
AsepriteSourceDocument document,
|
||||
FrameAnimationImportSourcePreview preview,
|
||||
IFrameAnimationImportPreviewSink preview,
|
||||
string texturePath,
|
||||
IReadOnlyList<SpriteRect> existingRects)
|
||||
IReadOnlyList<SpriteRect> existingRects,
|
||||
IReadOnlyCollection<int> includedSourceIndices)
|
||||
{
|
||||
var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType<Sprite>().ToArray();
|
||||
var matches = new Dictionary<int, Sprite>();
|
||||
foreach (var slot in document.SpriteSlots)
|
||||
foreach (var slot in EnumerateSlots(document, includedSourceIndices))
|
||||
{
|
||||
var unityRect = ToUnityRect(slot.Rect, source.Texture.height);
|
||||
var rectMatches = sprites.Where(sprite => RectEquals(sprite.rect, unityRect)).ToArray();
|
||||
@@ -382,6 +430,32 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<AsepriteSpriteSlot> EnumerateSlots(
|
||||
AsepriteSourceDocument document,
|
||||
IReadOnlyCollection<int> includedSourceIndices)
|
||||
{
|
||||
if (includedSourceIndices == null)
|
||||
{
|
||||
return document.SpriteSlots;
|
||||
}
|
||||
|
||||
var included = new HashSet<int>(includedSourceIndices);
|
||||
return document.SpriteSlots.Select(slot =>
|
||||
{
|
||||
var indices = new List<int>();
|
||||
var names = new List<string>();
|
||||
for (var index = 0; index < slot.SourceIndices.Count; index++)
|
||||
{
|
||||
if (!included.Contains(slot.SourceIndices[index])) continue;
|
||||
indices.Add(slot.SourceIndices[index]);
|
||||
names.Add(slot.FrameNames[index]);
|
||||
}
|
||||
return indices.Count == 0
|
||||
? null
|
||||
: new AsepriteSpriteSlot(slot.Rect, names[0], indices, names);
|
||||
}).Where(slot => slot != null);
|
||||
}
|
||||
|
||||
private static string BuildSlotSummary(string prefix, AsepriteSpriteSlot slot)
|
||||
{
|
||||
return slot.SourceIndices.Count > 1
|
||||
@@ -440,15 +514,15 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
|
||||
private static FrameAnimationImportIssue Error(
|
||||
FrameAnimationImportSource source,
|
||||
FrameAnimationSpriteSourceContext source,
|
||||
FrameAnimationImportIssueCode code,
|
||||
string message)
|
||||
{
|
||||
return new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
code,
|
||||
source?.InternalId,
|
||||
source?.InternalId,
|
||||
source.SourceId,
|
||||
source.SourceId,
|
||||
message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
var clip = (FrameClip)target;
|
||||
var owners = FrameAnimationAssetReferenceIndex.FindGraphsReferencing(clip);
|
||||
EditorGUILayout.LabelField(clip.IsImported ? "Imported FrameClip" : "Manual FrameClip", EditorStyles.boldLabel);
|
||||
var kind = clip.IsImported
|
||||
? "Graph Imported FrameClip"
|
||||
: clip.HasStandaloneImportSource ? "Source-linked External FrameClip" : "Manual FrameClip";
|
||||
EditorGUILayout.LabelField(kind, EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.TextField("ID", clip.Id);
|
||||
@@ -23,6 +26,8 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
EditorGUILayout.TextField("Storage", AssetDatabase.IsSubAsset(clip) ? "Graph sub-asset" : "External .asset");
|
||||
EditorGUILayout.TextField("Source Tag", clip.ImportInfo?.SourceTagName ?? string.Empty);
|
||||
EditorGUILayout.Toggle("Missing", clip.ImportInfo?.IsMissingFromSource ?? false);
|
||||
EditorGUILayout.ObjectField("Standalone Texture", clip.StandaloneImportSource?.Texture, typeof(Texture2D), false);
|
||||
EditorGUILayout.ObjectField("Standalone JSON", clip.StandaloneImportSource?.AsepriteJson, typeof(TextAsset), false);
|
||||
}
|
||||
if (!AssetDatabase.IsSubAsset(clip) && owners.Count > 1)
|
||||
{
|
||||
@@ -31,8 +36,14 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
MessageType.Warning);
|
||||
}
|
||||
EditorGUILayout.HelpBox(
|
||||
"Clip id、帧表和资产所有权操作只能在 Frame Animation Graph Editor 中执行。",
|
||||
AssetDatabase.IsSubAsset(clip)
|
||||
? "Graph sub-asset 的 Clip 数据请在 Frame Animation Graph Editor 中编辑。"
|
||||
: "双击资产或使用下方按钮,在独立 Frame Clip Editor 中编辑。",
|
||||
MessageType.Info);
|
||||
if (!AssetDatabase.IsSubAsset(clip) && GUILayout.Button("Open Frame Clip Editor"))
|
||||
{
|
||||
FrameClipEditorWindow.Open(clip);
|
||||
}
|
||||
if (owners.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("当前没有 FrameAnimationGraph 引用该 Clip。", MessageType.Info);
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5673c406c6069e241b0fad51881213fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,333 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
internal sealed class FrameClipImportPreview : IFrameAnimationImportPreviewSink
|
||||
{
|
||||
private readonly List<FrameAnimationImportIssue> issues = new List<FrameAnimationImportIssue>();
|
||||
private readonly List<FrameAnimationSpriteDiff> spriteDiffs = new List<FrameAnimationSpriteDiff>();
|
||||
|
||||
public FrameClip Clip { get; }
|
||||
public AsepriteSourceDocument Document { get; internal set; }
|
||||
public FrameAnimationSpritePlan SpritePlan { get; internal set; }
|
||||
public IReadOnlyList<int> SourceIndices { get; internal set; } = Array.Empty<int>();
|
||||
public string SourceTagName { get; internal set; } = string.Empty;
|
||||
public string CurrentHash { get; internal set; } = string.Empty;
|
||||
public bool HasFrameChanges { get; internal set; }
|
||||
public IReadOnlyList<FrameAnimationImportIssue> Issues => issues;
|
||||
public IReadOnlyList<FrameAnimationSpriteDiff> SpriteDiffs => spriteDiffs;
|
||||
public bool HasErrors => issues.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error);
|
||||
public bool HasSpriteChanges => spriteDiffs.Any(diff =>
|
||||
diff.Kind == FrameAnimationSpriteChangeKind.Added ||
|
||||
diff.Kind == FrameAnimationSpriteChangeKind.Updated);
|
||||
|
||||
public FrameClipImportPreview(FrameClip clip)
|
||||
{
|
||||
Clip = clip;
|
||||
}
|
||||
|
||||
public void AddIssue(FrameAnimationImportIssue issue)
|
||||
{
|
||||
if (issue != null) issues.Add(issue);
|
||||
}
|
||||
|
||||
public void AddSpriteDiff(FrameAnimationSpriteDiff diff)
|
||||
{
|
||||
if (diff != null) spriteDiffs.Add(diff);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class FrameClipImportService
|
||||
{
|
||||
public static bool EnableSource(FrameClip clip, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (!IsExternalClip(clip))
|
||||
{
|
||||
error = "独立来源只能添加到外部 .asset FrameClip。";
|
||||
return false;
|
||||
}
|
||||
if (clip.IsImported)
|
||||
{
|
||||
error = "Graph Imported Clip 必须继续由所属 Graph 的 ImportSource 管理。";
|
||||
return false;
|
||||
}
|
||||
if (clip.HasStandaloneImportSource) return true;
|
||||
Undo.RecordObject(clip, "Enable Standalone Frame Clip Source");
|
||||
clip.GetOrCreateStandaloneImportSource();
|
||||
EditorUtility.SetDirty(clip);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void DetachSource(FrameClip clip)
|
||||
{
|
||||
if (clip == null || !clip.HasStandaloneImportSource) return;
|
||||
Undo.RecordObject(clip, "Detach Standalone Frame Clip Source");
|
||||
clip.ClearStandaloneImportSource();
|
||||
EditorUtility.SetDirty(clip);
|
||||
}
|
||||
|
||||
public static FrameClipImportPreview Preview(FrameClip clip)
|
||||
{
|
||||
var preview = new FrameClipImportPreview(clip);
|
||||
if (!IsExternalClip(clip))
|
||||
{
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.ImportedClipOwnershipInvalid,
|
||||
"独立来源只能用于外部 .asset FrameClip。"));
|
||||
return preview;
|
||||
}
|
||||
var source = clip.StandaloneImportSource;
|
||||
if (source == null)
|
||||
{
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.ImportSourceReferenceInvalid,
|
||||
"FrameClip 尚未启用独立来源。"));
|
||||
return preview;
|
||||
}
|
||||
if (source.Texture == null)
|
||||
{
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureMissing, "独立来源缺少 Texture。"));
|
||||
return preview;
|
||||
}
|
||||
if (source.AsepriteJson == null)
|
||||
{
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.JsonMissing, "独立来源缺少 Aseprite JSON。"));
|
||||
return preview;
|
||||
}
|
||||
|
||||
var sourceId = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(clip));
|
||||
if (!AsepriteJsonParser.TryParse(source.AsepriteJson.text, sourceId, out var document, out var parseIssue))
|
||||
{
|
||||
preview.AddIssue(parseIssue);
|
||||
return preview;
|
||||
}
|
||||
preview.Document = document;
|
||||
ValidateFrames(clip, source, document, preview);
|
||||
if (preview.HasErrors) return preview;
|
||||
|
||||
if (document.Tags.Count > 0)
|
||||
{
|
||||
var first = document.Tags[0];
|
||||
preview.SourceTagName = first.Name;
|
||||
if (string.IsNullOrWhiteSpace(first.Name))
|
||||
{
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TagNameInvalid,
|
||||
"第一个 Tag 的名称不能为空。"));
|
||||
return preview;
|
||||
}
|
||||
if (!AsepriteJsonParser.TryExpandTag(first, document.Frames.Count, sourceId,
|
||||
out var indices, out var tagIssue))
|
||||
{
|
||||
preview.AddIssue(tagIssue);
|
||||
return preview;
|
||||
}
|
||||
preview.SourceIndices = indices;
|
||||
}
|
||||
else
|
||||
{
|
||||
preview.SourceIndices = Enumerable.Range(0, document.Frames.Count).ToArray();
|
||||
}
|
||||
|
||||
ValidateTextureOwnership(clip, source, preview);
|
||||
if (preview.HasErrors) return preview;
|
||||
|
||||
var context = Context(clip, source);
|
||||
preview.SpritePlan = FrameAnimationSpriteUtility.BuildPlan(
|
||||
context, document, preview, preview.SourceIndices.Distinct().ToArray());
|
||||
preview.CurrentHash = FrameAnimationSpriteUtility.ComputeSourceHash(context);
|
||||
preview.HasFrameChanges = !FramesEqual(clip, preview);
|
||||
if (!string.IsNullOrEmpty(source.LastSourceHash) && source.LastSourceHash != preview.CurrentHash)
|
||||
{
|
||||
preview.AddIssue(new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Warning,
|
||||
FrameAnimationImportIssueCode.SourceChanged,
|
||||
sourceId,
|
||||
clip.Id,
|
||||
"来源内容或切图设置自上次刷新后已发生变化。"));
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
public static bool Apply(FrameClipImportPreview preview, bool allowSpriteChanges, out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (preview?.Clip == null || preview.Document == null || preview.SpritePlan == null)
|
||||
{
|
||||
error = "独立 Clip 导入预览无效。";
|
||||
return false;
|
||||
}
|
||||
if (preview.HasErrors)
|
||||
{
|
||||
error = "导入预览包含阻断错误,未修改任何资产。";
|
||||
return false;
|
||||
}
|
||||
if (preview.HasSpriteChanges && !allowSpriteChanges)
|
||||
{
|
||||
error = "导入需要修改 TextureImporter,但尚未获得确认。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var clip = preview.Clip;
|
||||
var source = clip.StandaloneImportSource;
|
||||
FrameAnimationTextureSnapshot snapshot = null;
|
||||
Undo.IncrementCurrentGroup();
|
||||
var group = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Refresh Standalone Frame Clip Source");
|
||||
try
|
||||
{
|
||||
if (source.ManageSpriteSlicing && preview.HasSpriteChanges)
|
||||
{
|
||||
snapshot = FrameAnimationSpriteUtility.CaptureSnapshot(preview.SpritePlan);
|
||||
FrameAnimationSpriteUtility.ApplyPlan(preview.SpritePlan);
|
||||
}
|
||||
var sprites = source.ManageSpriteSlicing
|
||||
? FrameAnimationSpriteUtility.LoadSpritesByName(preview.SpritePlan.TexturePath)
|
||||
: null;
|
||||
var frames = preview.SourceIndices.Select(sourceIndex =>
|
||||
{
|
||||
var sourceFrame = preview.Document.Frames[sourceIndex];
|
||||
if (!preview.SpritePlan.TryResolveSprite(sourceIndex, sprites, out var sprite))
|
||||
throw new InvalidOperationException($"刷新后找不到 SourceFrame '{sourceFrame.FrameName}' 对应的 Sprite。");
|
||||
return new FrameAnimationFrame(sprite, sourceFrame.DurationMs, sourceFrame.FrameName, sourceIndex);
|
||||
}).ToArray();
|
||||
|
||||
Undo.RecordObject(clip, "Refresh Standalone Frame Clip");
|
||||
clip.SetFrames(frames);
|
||||
if (string.IsNullOrWhiteSpace(clip.Id))
|
||||
{
|
||||
var initialId = !string.IsNullOrWhiteSpace(preview.SourceTagName)
|
||||
? preview.SourceTagName
|
||||
: Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(clip));
|
||||
clip.SetId(initialId);
|
||||
if (string.IsNullOrWhiteSpace(clip.DisplayName)) clip.SetDisplayName(initialId);
|
||||
}
|
||||
source.SetLastImport(
|
||||
FrameAnimationSpriteUtility.ComputeSourceHash(Context(clip, source)),
|
||||
preview.SourceTagName);
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
Undo.CollapseUndoOperations(group);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Undo.RevertAllDownToGroup(group);
|
||||
if (snapshot != null) FrameAnimationSpriteUtility.RestoreSnapshot(snapshot);
|
||||
AssetDatabase.SaveAssets();
|
||||
error = $"独立 Clip 导入失败,已尝试回滚:{exception.Message}";
|
||||
Debug.LogException(exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateFrames(
|
||||
FrameClip clip,
|
||||
FrameClipImportSource source,
|
||||
AsepriteSourceDocument document,
|
||||
FrameClipImportPreview preview)
|
||||
{
|
||||
if (document.Frames.Count == 0)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FramesMissing, "Aseprite JSON 的 frames 为空。"));
|
||||
foreach (var duplicate in document.Frames.GroupBy(frame => frame.FrameName).Where(group => group.Count() > 1))
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameNameDuplicate,
|
||||
$"frameName '{duplicate.Key}' 在来源内重复。"));
|
||||
for (var index = 0; index < document.Frames.Count; index++)
|
||||
{
|
||||
var frame = document.Frames[index];
|
||||
if (string.IsNullOrWhiteSpace(frame.FrameName))
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameNameInvalid,
|
||||
$"SourceFrame[{index}] 的 frameName 为空。"));
|
||||
if (frame.DurationMs <= 0)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameDurationInvalid,
|
||||
$"SourceFrame '{frame.FrameName}' 的 duration 必须大于 0。"));
|
||||
if (frame.Rect.width <= 0 || frame.Rect.height <= 0 || frame.Rect.x < 0 || frame.Rect.y < 0 ||
|
||||
frame.Rect.xMax > source.Texture.width || frame.Rect.yMax > source.Texture.height)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.FrameRectInvalid,
|
||||
$"SourceFrame '{frame.FrameName}' 的 rect 越出 Texture 范围。"));
|
||||
if (frame.Trimmed)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TrimmedUnsupported,
|
||||
$"SourceFrame '{frame.FrameName}' 使用了不支持的 trimmed。"));
|
||||
if (frame.Rotated)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.RotatedUnsupported,
|
||||
$"SourceFrame '{frame.FrameName}' 使用了不支持的 rotated。"));
|
||||
}
|
||||
if (document.TextureSize.x != source.Texture.width || document.TextureSize.y != source.Texture.height)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureSizeMismatch,
|
||||
$"meta.size {document.TextureSize.x}x{document.TextureSize.y} 与 Texture {source.Texture.width}x{source.Texture.height} 不一致。"));
|
||||
}
|
||||
|
||||
private static void ValidateTextureOwnership(
|
||||
FrameClip clip,
|
||||
FrameClipImportSource source,
|
||||
FrameClipImportPreview preview)
|
||||
{
|
||||
if (!source.ManageSpriteSlicing) return;
|
||||
var owners = new List<string>();
|
||||
foreach (var guid in AssetDatabase.FindAssets("t:FrameAnimationGraph"))
|
||||
{
|
||||
var graph = AssetDatabase.LoadAssetAtPath<FrameAnimationGraph>(AssetDatabase.GUIDToAssetPath(guid));
|
||||
if (graph == null) continue;
|
||||
owners.AddRange(graph.ImportSources.Where(item => item != null && item.IsEnabled &&
|
||||
item.ManageSpriteSlicing && item.Texture == source.Texture)
|
||||
.Select(item => $"Graph '{graph.name}' / {item.DisplayName}"));
|
||||
}
|
||||
foreach (var guid in AssetDatabase.FindAssets("t:FrameClip"))
|
||||
{
|
||||
var other = AssetDatabase.LoadAssetAtPath<FrameClip>(AssetDatabase.GUIDToAssetPath(guid));
|
||||
var otherSource = other?.StandaloneImportSource;
|
||||
if (other == null || other == clip || otherSource == null || !otherSource.ManageSpriteSlicing ||
|
||||
otherSource.Texture != source.Texture) continue;
|
||||
owners.Add($"FrameClip '{other.name}'");
|
||||
}
|
||||
if (owners.Count > 0)
|
||||
preview.AddIssue(Error(clip, FrameAnimationImportIssueCode.TextureOwnershipConflict,
|
||||
$"Texture '{source.Texture.name}' 已有其他可写来源:{string.Join(", ", owners)}"));
|
||||
}
|
||||
|
||||
private static bool FramesEqual(FrameClip clip, FrameClipImportPreview preview)
|
||||
{
|
||||
if (clip.FrameCount != preview.SourceIndices.Count) return false;
|
||||
for (var index = 0; index < preview.SourceIndices.Count; index++)
|
||||
{
|
||||
var sourceIndex = preview.SourceIndices[index];
|
||||
var sourceFrame = preview.Document.Frames[sourceIndex];
|
||||
var frame = clip.Frames[index];
|
||||
if (frame == null || frame.SourceIndex != sourceIndex || frame.FrameName != sourceFrame.FrameName ||
|
||||
frame.DurationMs != sourceFrame.DurationMs ||
|
||||
!preview.SpritePlan.MatchesSprite(sourceIndex, frame.Sprite)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static FrameAnimationSpriteSourceContext Context(FrameClip clip, FrameClipImportSource source)
|
||||
{
|
||||
return new FrameAnimationSpriteSourceContext(
|
||||
AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(clip)),
|
||||
source.Texture, source.AsepriteJson, source.Pivot, source.ManageSpriteSlicing);
|
||||
}
|
||||
|
||||
private static bool IsExternalClip(FrameClip clip)
|
||||
{
|
||||
return clip != null && !AssetDatabase.IsSubAsset(clip) &&
|
||||
!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip));
|
||||
}
|
||||
|
||||
private static FrameAnimationImportIssue Error(
|
||||
FrameClip clip,
|
||||
FrameAnimationImportIssueCode code,
|
||||
string message)
|
||||
{
|
||||
var path = clip != null ? AssetDatabase.GetAssetPath(clip) : string.Empty;
|
||||
return new FrameAnimationImportIssue(
|
||||
FrameAnimationValidationSeverity.Error,
|
||||
code,
|
||||
AssetDatabase.AssetPathToGUID(path),
|
||||
clip != null ? clip.Id : string.Empty,
|
||||
message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4448f0db0909671479f225061710e439
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
[CustomEditor(typeof(FrameClipPlayer))]
|
||||
public sealed class FrameClipPlayerEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("clip"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("playOnEnable"));
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("speed"));
|
||||
DrawWarnings();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
if (!Application.isPlaying) return;
|
||||
var player = (FrameClipPlayer)target;
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Runtime State", EditorStyles.boldLabel);
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.EnumPopup("State", player.State);
|
||||
EditorGUILayout.ObjectField("Clip", player.CurrentClip, typeof(FrameClip), false);
|
||||
EditorGUILayout.TextField("Clip ID", player.CurrentClipId);
|
||||
EditorGUILayout.IntField("Frame", player.CurrentFrameIndex);
|
||||
}
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void DrawWarnings()
|
||||
{
|
||||
var player = (FrameClipPlayer)target;
|
||||
var renderer = player.GetComponent<SpriteRenderer>();
|
||||
var image = player.GetComponent<Image>();
|
||||
if (player.GetComponent<FrameAnimationPlayer>() != null)
|
||||
EditorGUILayout.HelpBox("同一 GameObject 不能同时包含 FrameAnimationPlayer 和 FrameClipPlayer。", MessageType.Error);
|
||||
if (renderer == null && image == null)
|
||||
EditorGUILayout.HelpBox("同一 GameObject 上必须存在一个 SpriteRenderer 或 Image。", MessageType.Error);
|
||||
else if (renderer != null && image != null)
|
||||
EditorGUILayout.HelpBox("同一 GameObject 不能同时存在 SpriteRenderer 和 Image。", MessageType.Error);
|
||||
if (serializedObject.FindProperty("clip").objectReferenceValue == null)
|
||||
EditorGUILayout.HelpBox("尚未绑定默认 FrameClip;仍可通过 Play(FrameClip) 临时播放。", MessageType.Info);
|
||||
var value = serializedObject.FindProperty("speed").floatValue;
|
||||
if (!FrameAnimationValueUtility.IsValidSpeed(value))
|
||||
EditorGUILayout.HelpBox("Speed 必须是有限且不小于 0 的数值。", MessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7eddf60189f61740be2fe59cda06957
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -8,20 +8,23 @@ namespace AibisDream.SystemEditor.Tests
|
||||
public sealed class PresentationSnapshotContractTests
|
||||
{
|
||||
[Test]
|
||||
public void Providers_UseDistinctStableIdsAndRestoreBeforeScreen()
|
||||
public void PresentationProviders_UseStableIdsAndRestoreInDependencyOrder()
|
||||
{
|
||||
var showcase = new ShowcaseSnapshotProvider();
|
||||
var day2Sleep = new Day2SleepPresentationSnapshotProvider();
|
||||
var playTool = new PlayToolSnapshotProvider();
|
||||
var screen = new ScreenSnapshotProvider();
|
||||
|
||||
Assert.That(showcase.SaveId, Is.EqualTo("showcase"));
|
||||
Assert.That(day2Sleep.SaveId, Is.EqualTo("day2SleepPresentation"));
|
||||
Assert.That(playTool.SaveId, Is.EqualTo("playTool"));
|
||||
Assert.That(showcase.RestoreOrder, Is.LessThan(playTool.RestoreOrder));
|
||||
Assert.That(showcase.RestoreOrder, Is.LessThan(day2Sleep.RestoreOrder));
|
||||
Assert.That(day2Sleep.RestoreOrder, Is.LessThan(playTool.RestoreOrder));
|
||||
Assert.That(playTool.RestoreOrder, Is.LessThan(screen.RestoreOrder));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PresentationSections_RoundTripThroughSnapshotJson()
|
||||
public void PlayToolSection_RoundTripsThroughSnapshotJson()
|
||||
{
|
||||
var source = new SaveSnapshot();
|
||||
source.sections[SnapshotProviderIds.PlayTool] = new PlayToolSnapshotDto
|
||||
@@ -31,30 +34,55 @@ namespace AibisDream.SystemEditor.Tests
|
||||
isFullScreenVisible = true,
|
||||
fullScreenPicName = "教室"
|
||||
};
|
||||
source.sections[SnapshotProviderIds.Showcase] = new ShowcaseSnapshotDto
|
||||
{
|
||||
displayMode = nameof(ShowcaseDisplayMode.Small),
|
||||
picName = "D2S轱辘",
|
||||
usesSplitLayers = true
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(source);
|
||||
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
|
||||
var playTool = ((JObject)restored.sections[SnapshotProviderIds.PlayTool])
|
||||
.ToObject<PlayToolSnapshotDto>();
|
||||
var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase])
|
||||
.ToObject<ShowcaseSnapshotDto>();
|
||||
|
||||
Assert.That(playTool.isObjVisible, Is.True);
|
||||
Assert.That(playTool.objPicName, Is.EqualTo("证物"));
|
||||
Assert.That(playTool.isFullScreenVisible, Is.True);
|
||||
Assert.That(playTool.fullScreenPicName, Is.EqualTo("教室"));
|
||||
Assert.That(showcase.displayMode, Is.EqualTo(nameof(ShowcaseDisplayMode.Small)));
|
||||
Assert.That(showcase.picName, Is.EqualTo("D2S轱辘"));
|
||||
Assert.That(showcase.usesSplitLayers, Is.True);
|
||||
Assert.That(restored.schemaVersion, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShowcaseAndDay2SleepSections_RoundTripSemanticState()
|
||||
{
|
||||
var source = new SaveSnapshot();
|
||||
source.sections[SnapshotProviderIds.Showcase] = new ShowcaseSnapshotDto
|
||||
{
|
||||
displayMode = ShowcaseDisplayMode.Large.ToString(),
|
||||
picName = "D2S星图",
|
||||
isBackgroundVisible = true
|
||||
};
|
||||
source.sections[SnapshotProviderIds.Day2SleepPresentation] =
|
||||
new Day2SleepPresentationSnapshotDto
|
||||
{
|
||||
mode = Day2SleepPresentationMode.LargeEffects.ToString(),
|
||||
largeBlurAmount = 0.72f,
|
||||
largeBlurSize = 0.015f,
|
||||
largeScaleMultiplier = 1.1f,
|
||||
largeAlpha = 0.4f
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(source);
|
||||
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
|
||||
var showcase = ((JObject)restored.sections[SnapshotProviderIds.Showcase])
|
||||
.ToObject<ShowcaseSnapshotDto>();
|
||||
var day2Sleep = ((JObject)restored.sections[SnapshotProviderIds.Day2SleepPresentation])
|
||||
.ToObject<Day2SleepPresentationSnapshotDto>();
|
||||
|
||||
Assert.That(showcase.displayMode, Is.EqualTo("Large"));
|
||||
Assert.That(showcase.picName, Is.EqualTo("D2S星图"));
|
||||
Assert.That(showcase.isBackgroundVisible, Is.True);
|
||||
Assert.That(day2Sleep.mode, Is.EqualTo("LargeEffects"));
|
||||
Assert.That(day2Sleep.largeBlurAmount, Is.EqualTo(0.72f));
|
||||
Assert.That(day2Sleep.largeScaleMultiplier, Is.EqualTo(1.1f));
|
||||
Assert.That(day2Sleep.largeAlpha, Is.EqualTo(0.4f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OldSnapshotWithoutPresentationSections_RemainsCompatible()
|
||||
{
|
||||
@@ -62,8 +90,9 @@ namespace AibisDream.SystemEditor.Tests
|
||||
"{\"schemaVersion\":1,\"sections\":{}}");
|
||||
|
||||
Assert.That(restored.schemaVersion, Is.EqualTo(SaveSnapshotSchema.CurrentVersion));
|
||||
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.PlayTool), Is.False);
|
||||
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Showcase), Is.False);
|
||||
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.Day2SleepPresentation), Is.False);
|
||||
Assert.That(restored.sections.ContainsKey(SnapshotProviderIds.PlayTool), Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user