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

530 lines
22 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.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEditor.U2D.Sprites;
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; }
public IReadOnlyList<SpriteRect> ExistingRects { get; }
public IReadOnlyList<SpriteRect> PlannedRects { get; }
public IReadOnlyDictionary<int, string> SpriteNamesBySourceIndex { get; }
public IReadOnlyDictionary<int, Sprite> ReadOnlySpritesBySourceIndex { get; }
public FrameAnimationSpritePlan(
string texturePath,
IReadOnlyList<SpriteRect> existingRects,
IReadOnlyList<SpriteRect> plannedRects,
IReadOnlyDictionary<int, string> spriteNamesBySourceIndex,
IReadOnlyDictionary<int, Sprite> readOnlySpritesBySourceIndex)
{
TexturePath = texturePath ?? string.Empty;
ExistingRects = existingRects ?? Array.Empty<SpriteRect>();
PlannedRects = plannedRects ?? Array.Empty<SpriteRect>();
SpriteNamesBySourceIndex = spriteNamesBySourceIndex ?? new Dictionary<int, string>();
ReadOnlySpritesBySourceIndex = readOnlySpritesBySourceIndex ?? new Dictionary<int, Sprite>();
}
public bool MatchesSprite(int sourceIndex, Sprite sprite)
{
if (sprite == null)
{
return false;
}
if (ReadOnlySpritesBySourceIndex.TryGetValue(sourceIndex, out var readOnlySprite))
{
return readOnlySprite == sprite;
}
return SpriteNamesBySourceIndex.TryGetValue(sourceIndex, out var spriteName) &&
sprite.name == spriteName;
}
public bool TryResolveSprite(
int sourceIndex,
IReadOnlyDictionary<string, Sprite> writableSprites,
out Sprite sprite)
{
if (ReadOnlySpritesBySourceIndex.TryGetValue(sourceIndex, out sprite))
{
return sprite != null;
}
if (SpriteNamesBySourceIndex.TryGetValue(sourceIndex, out var spriteName) &&
writableSprites != null && writableSprites.TryGetValue(spriteName, out sprite))
{
return sprite != null;
}
sprite = null;
return false;
}
}
internal sealed class FrameAnimationTextureSnapshot
{
public string TexturePath { get; }
public TextureImporterType TextureType { get; }
public SpriteImportMode SpriteImportMode { get; }
public IReadOnlyList<SpriteRect> SpriteRects { get; }
public FrameAnimationTextureSnapshot(
string texturePath,
TextureImporterType textureType,
SpriteImportMode spriteImportMode,
IReadOnlyList<SpriteRect> spriteRects)
{
TexturePath = texturePath;
TextureType = textureType;
SpriteImportMode = spriteImportMode;
SpriteRects = spriteRects;
}
}
internal static class FrameAnimationSpriteUtility
{
public static FrameAnimationSpritePlan BuildPlan(
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;
if (importer == null)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureImporterInvalid,
$"无法读取 TextureImporter{texturePath}"));
return new FrameAnimationSpritePlan(texturePath, null, null, null, null);
}
var existingRects = source.ManageSpriteSlicing && importer.spriteImportMode != SpriteImportMode.Multiple
? Array.Empty<SpriteRect>()
: ReadSpriteRects(importer);
return source.ManageSpriteSlicing
? BuildWritablePlan(source, document, preview, texturePath, existingRects, includedSourceIndices)
: BuildReadOnlyPlan(source, document, preview, texturePath, existingRects, includedSourceIndices);
}
public static Rect ToUnityRect(RectInt asepriteRect, int textureHeight)
{
return new Rect(
asepriteRect.x,
textureHeight - asepriteRect.y - asepriteRect.height,
asepriteRect.width,
asepriteRect.height);
}
public static FrameAnimationTextureSnapshot CaptureSnapshot(FrameAnimationSpritePlan plan)
{
var importer = AssetImporter.GetAtPath(plan.TexturePath) as TextureImporter;
if (importer == null)
{
throw new InvalidOperationException($"无法读取 TextureImporter{plan.TexturePath}");
}
return new FrameAnimationTextureSnapshot(
plan.TexturePath,
importer.textureType,
importer.spriteImportMode,
CloneRects(ReadSpriteRects(importer)));
}
public static void ApplyPlan(FrameAnimationSpritePlan plan)
{
var importer = AssetImporter.GetAtPath(plan.TexturePath) as TextureImporter;
if (importer == null)
{
throw new InvalidOperationException($"无法读取 TextureImporter{plan.TexturePath}");
}
Undo.RecordObject(importer, "Refresh Frame Animation Sprite Slicing");
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Multiple;
WriteSpriteRects(importer, plan.PlannedRects);
importer.SaveAndReimport();
}
public static void RestoreSnapshot(FrameAnimationTextureSnapshot snapshot)
{
var importer = AssetImporter.GetAtPath(snapshot.TexturePath) as TextureImporter;
if (importer == null)
{
return;
}
importer.textureType = snapshot.TextureType;
importer.spriteImportMode = snapshot.SpriteImportMode;
WriteSpriteRects(importer, snapshot.SpriteRects);
importer.SaveAndReimport();
}
public static IReadOnlyDictionary<string, Sprite> LoadSpritesByName(string texturePath)
{
return AssetDatabase.LoadAllAssetsAtPath(texturePath)
.OfType<Sprite>()
.GroupBy(sprite => sprite.name)
.Where(group => group.Count() == 1)
.ToDictionary(group => group.Key, group => group.Single());
}
public static string ComputeSourceHash(
FrameAnimationImportSource source,
IReadOnlyList<SpriteRect> spriteRects = 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;
}
var texturePath = AssetDatabase.GetAssetPath(source.Texture);
var builder = new StringBuilder();
builder.AppendLine("FrameAnimationImportHash:v2");
builder.AppendLine(source.AsepriteJson.text ?? string.Empty);
builder.AppendLine(AssetDatabase.AssetPathToGUID(texturePath));
builder.AppendLine(AssetDatabase.GetAssetDependencyHash(texturePath).ToString());
builder.AppendLine(source.Pivot.x.ToString("R", CultureInfo.InvariantCulture));
builder.AppendLine(source.Pivot.y.ToString("R", CultureInfo.InvariantCulture));
builder.AppendLine(source.ManageSpriteSlicing ? "write" : "read");
var rects = spriteRects;
if (rects == null && AssetImporter.GetAtPath(texturePath) is TextureImporter importer)
{
rects = ReadSpriteRects(importer);
}
foreach (var rect in (rects ?? Array.Empty<SpriteRect>()).OrderBy(item => item.name))
{
builder.Append(rect.name).Append('|')
.Append(rect.spriteID).Append('|')
.Append(rect.rect.x.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.rect.y.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.rect.width.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.rect.height.ToString("R", CultureInfo.InvariantCulture)).Append('|')
.Append(rect.pivot.x.ToString("R", CultureInfo.InvariantCulture)).Append(',')
.Append(rect.pivot.y.ToString("R", CultureInfo.InvariantCulture)).AppendLine();
}
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString()));
return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();
}
private static FrameAnimationSpritePlan BuildWritablePlan(
FrameAnimationSpriteSourceContext source,
AsepriteSourceDocument document,
IFrameAnimationImportPreviewSink preview,
string texturePath,
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 EnumerateSlots(document, includedSourceIndices))
{
var unityRect = ToUnityRect(slot.Rect, source.Texture.height);
SpriteRect matched = null;
var slotHasError = false;
foreach (var aliasName in slot.FrameNames)
{
var nameMatches = planned.Where(rect => rect.name == aliasName).ToArray();
if (nameMatches.Length > 1)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
$"Texture 中存在多个名为 '{aliasName}' 的 SpriteRect。"));
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Error,
slot.CanonicalFrameName,
unityRect,
"SpriteRect 名称重复",
slot.FrameNames));
slotHasError = true;
break;
}
if (nameMatches.Length == 1 && !assigned.Contains(nameMatches[0]))
{
matched = nameMatches[0];
break;
}
}
if (slotHasError)
{
continue;
}
if (matched == null)
{
var rectMatches = planned.Where(rect => !assigned.Contains(rect) && RectEquals(rect.rect, unityRect))
.ToArray();
if (rectMatches.Length > 1)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
$"共享帧 '{slot.CanonicalFrameName}' 的 rect 对应多个现有 SpriteRect。"));
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Error,
slot.CanonicalFrameName,
unityRect,
"SpriteRect rect 匹配不唯一",
slot.FrameNames));
continue;
}
matched = rectMatches.SingleOrDefault();
}
if (matched == null)
{
matched = new SpriteRect
{
name = slot.CanonicalFrameName,
rect = unityRect,
pivot = source.Pivot,
alignment = SpriteAlignment.Custom,
spriteID = GUID.Generate()
};
planned.Add(matched);
assigned.Add(matched);
MapSlot(slot, matched.name, spriteNamesBySourceIndex);
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Added,
matched.name,
unityRect,
BuildSlotSummary("新增 SpriteRect", slot),
slot.FrameNames));
continue;
}
var changed = !RectEquals(matched.rect, unityRect) || matched.pivot != source.Pivot ||
matched.alignment != SpriteAlignment.Custom;
matched.rect = unityRect;
matched.pivot = source.Pivot;
matched.alignment = SpriteAlignment.Custom;
assigned.Add(matched);
MapSlot(slot, matched.name, spriteNamesBySourceIndex);
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged,
matched.name,
unityRect,
BuildSlotSummary(changed ? "更新 rect/pivot,保留 spriteID" : "无变化", slot),
slot.FrameNames));
}
foreach (var retained in originalRects.Where(rect => !assigned.Contains(rect)))
{
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Retained,
retained.name,
retained.rect,
"源中已消失或不再作为共享帧主 Sprite,保留现有 SpriteRect"));
}
return new FrameAnimationSpritePlan(
texturePath,
existingRects,
planned,
spriteNamesBySourceIndex,
null);
}
private static FrameAnimationSpritePlan BuildReadOnlyPlan(
FrameAnimationSpriteSourceContext source,
AsepriteSourceDocument document,
IFrameAnimationImportPreviewSink preview,
string texturePath,
IReadOnlyList<SpriteRect> existingRects,
IReadOnlyCollection<int> includedSourceIndices)
{
var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType<Sprite>().ToArray();
var matches = new Dictionary<int, Sprite>();
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();
if (rectMatches.Length != 1)
{
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
$"共享帧 '{slot.CanonicalFrameName}' 无法按 rect 唯一匹配 Sprite,候选数:{rectMatches.Length}。"));
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Error,
slot.CanonicalFrameName,
unityRect,
"只读 Sprite 匹配失败",
slot.FrameNames));
continue;
}
foreach (var sourceIndex in slot.SourceIndices)
{
matches[sourceIndex] = rectMatches[0];
}
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
FrameAnimationSpriteChangeKind.Unchanged,
rectMatches[0].name,
unityRect,
BuildSlotSummary($"匹配 Sprite '{rectMatches[0].name}'", slot),
slot.FrameNames));
}
return new FrameAnimationSpritePlan(texturePath, existingRects, existingRects, null, matches);
}
private static void MapSlot(
AsepriteSpriteSlot slot,
string spriteName,
IDictionary<int, string> spriteNamesBySourceIndex)
{
foreach (var sourceIndex in slot.SourceIndices)
{
spriteNamesBySourceIndex[sourceIndex] = spriteName;
}
}
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
? $"{prefix}{slot.SourceIndices.Count} 个 SourceFrame 共享"
: prefix;
}
private static IReadOnlyList<SpriteRect> ReadSpriteRects(TextureImporter importer)
{
var factories = new SpriteDataProviderFactories();
factories.Init();
var provider = factories.GetSpriteEditorDataProviderFromObject(importer);
if (provider == null)
{
return Array.Empty<SpriteRect>();
}
provider.InitSpriteEditorDataProvider();
return CloneRects(provider.GetSpriteRects());
}
private static void WriteSpriteRects(TextureImporter importer, IReadOnlyList<SpriteRect> rects)
{
var factories = new SpriteDataProviderFactories();
factories.Init();
var provider = factories.GetSpriteEditorDataProviderFromObject(importer);
if (provider == null)
{
throw new InvalidOperationException($"无法获取 Sprite Data Provider{importer.assetPath}");
}
provider.InitSpriteEditorDataProvider();
provider.SetSpriteRects(CloneRects(rects).ToArray());
provider.Apply();
}
private static IReadOnlyList<SpriteRect> CloneRects(IEnumerable<SpriteRect> rects)
{
return (rects ?? Array.Empty<SpriteRect>()).Select(rect => new SpriteRect
{
name = rect.name,
rect = rect.rect,
pivot = rect.pivot,
alignment = rect.alignment,
border = rect.border,
spriteID = rect.spriteID
}).ToArray();
}
private static bool RectEquals(Rect left, Rect right)
{
return Mathf.Approximately(left.x, right.x) &&
Mathf.Approximately(left.y, right.y) &&
Mathf.Approximately(left.width, right.width) &&
Mathf.Approximately(left.height, right.height);
}
private static FrameAnimationImportIssue Error(
FrameAnimationSpriteSourceContext source,
FrameAnimationImportIssueCode code,
string message)
{
return new FrameAnimationImportIssue(
FrameAnimationValidationSeverity.Error,
code,
source.SourceId,
source.SourceId,
message);
}
}
}