345 lines
15 KiB
C#
345 lines
15 KiB
C#
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 sealed class FrameAnimationSpritePlan
|
||
{
|
||
public string TexturePath { get; }
|
||
public IReadOnlyList<SpriteRect> ExistingRects { get; }
|
||
public IReadOnlyList<SpriteRect> PlannedRects { get; }
|
||
public IReadOnlyDictionary<string, Sprite> ReadOnlySprites { get; }
|
||
|
||
public FrameAnimationSpritePlan(
|
||
string texturePath,
|
||
IReadOnlyList<SpriteRect> existingRects,
|
||
IReadOnlyList<SpriteRect> plannedRects,
|
||
IReadOnlyDictionary<string, Sprite> readOnlySprites)
|
||
{
|
||
TexturePath = texturePath ?? string.Empty;
|
||
ExistingRects = existingRects ?? Array.Empty<SpriteRect>();
|
||
PlannedRects = plannedRects ?? Array.Empty<SpriteRect>();
|
||
ReadOnlySprites = readOnlySprites ?? new Dictionary<string, Sprite>();
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
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);
|
||
}
|
||
|
||
var existingRects = ReadSpriteRects(importer);
|
||
return source.ManageSpriteSlicing
|
||
? BuildWritablePlan(source, document, preview, texturePath, existingRects)
|
||
: BuildReadOnlyPlan(source, document, preview, texturePath, existingRects);
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (source == null || source.Texture == null || source.AsepriteJson == null)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var texturePath = AssetDatabase.GetAssetPath(source.Texture);
|
||
var builder = new StringBuilder();
|
||
builder.AppendLine("FrameAnimationImportHash:v1");
|
||
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(
|
||
FrameAnimationImportSource source,
|
||
AsepriteSourceDocument document,
|
||
FrameAnimationImportSourcePreview preview,
|
||
string texturePath,
|
||
IReadOnlyList<SpriteRect> existingRects)
|
||
{
|
||
var planned = CloneRects(existingRects).ToList();
|
||
var byName = planned.GroupBy(rect => rect.name).ToDictionary(group => group.Key, group => group.ToList());
|
||
foreach (var frame in document.Frames)
|
||
{
|
||
var unityRect = ToUnityRect(frame.Rect, source.Texture.height);
|
||
if (byName.TryGetValue(frame.FrameName, out var matches) && matches.Count > 1)
|
||
{
|
||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
|
||
$"Texture 中存在多个名为 '{frame.FrameName}' 的 SpriteRect。"));
|
||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||
FrameAnimationSpriteChangeKind.Error, frame.FrameName, unityRect, "SpriteRect 名称重复"));
|
||
continue;
|
||
}
|
||
|
||
if (matches != null && matches.Count == 1)
|
||
{
|
||
var rect = matches[0];
|
||
var changed = !RectEquals(rect.rect, unityRect) || rect.pivot != source.Pivot;
|
||
rect.rect = unityRect;
|
||
rect.pivot = source.Pivot;
|
||
rect.alignment = SpriteAlignment.Custom;
|
||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||
changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged,
|
||
frame.FrameName,
|
||
unityRect,
|
||
changed ? "更新 rect/pivot,保留 spriteID" : "无变化"));
|
||
continue;
|
||
}
|
||
|
||
var newRect = new SpriteRect
|
||
{
|
||
name = frame.FrameName,
|
||
rect = unityRect,
|
||
pivot = source.Pivot,
|
||
alignment = SpriteAlignment.Custom,
|
||
spriteID = GUID.Generate()
|
||
};
|
||
planned.Add(newRect);
|
||
byName[frame.FrameName] = new List<SpriteRect> { newRect };
|
||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||
FrameAnimationSpriteChangeKind.Added, frame.FrameName, unityRect, "新增 SpriteRect"));
|
||
}
|
||
|
||
var sourceNames = new HashSet<string>(document.Frames.Select(frame => frame.FrameName));
|
||
foreach (var retained in existingRects.Where(rect => !sourceNames.Contains(rect.name)))
|
||
{
|
||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||
FrameAnimationSpriteChangeKind.Retained, retained.name, retained.rect, "源中已消失,保留现有 SpriteRect"));
|
||
}
|
||
|
||
return new FrameAnimationSpritePlan(texturePath, existingRects, planned, null);
|
||
}
|
||
|
||
private static FrameAnimationSpritePlan BuildReadOnlyPlan(
|
||
FrameAnimationImportSource source,
|
||
AsepriteSourceDocument document,
|
||
FrameAnimationImportSourcePreview preview,
|
||
string texturePath,
|
||
IReadOnlyList<SpriteRect> existingRects)
|
||
{
|
||
var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType<Sprite>().ToArray();
|
||
var matches = new Dictionary<string, Sprite>();
|
||
foreach (var frame in document.Frames)
|
||
{
|
||
var unityRect = ToUnityRect(frame.Rect, source.Texture.height);
|
||
var named = sprites.Where(sprite => sprite.name == frame.FrameName).ToArray();
|
||
var rectMatches = sprites.Where(sprite => RectEquals(sprite.rect, unityRect)).ToArray();
|
||
Sprite match = null;
|
||
|
||
if (named.Length == 1 && RectEquals(named[0].rect, unityRect))
|
||
{
|
||
match = named[0];
|
||
}
|
||
else if (named.Length == 0 && rectMatches.Length == 1)
|
||
{
|
||
match = rectMatches[0];
|
||
}
|
||
|
||
if (match == null || (named.Length > 0 && rectMatches.Length > 0 && !named.Intersect(rectMatches).Any()))
|
||
{
|
||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
|
||
$"SourceFrame '{frame.FrameName}' 无法按名称和 rect 唯一匹配 Sprite。"));
|
||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||
FrameAnimationSpriteChangeKind.Error, frame.FrameName, unityRect, "只读 Sprite 匹配失败"));
|
||
continue;
|
||
}
|
||
|
||
matches[frame.FrameName] = match;
|
||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||
FrameAnimationSpriteChangeKind.Unchanged, frame.FrameName, unityRect, $"匹配 Sprite '{match.name}'"));
|
||
}
|
||
|
||
return new FrameAnimationSpritePlan(texturePath, existingRects, existingRects, matches);
|
||
}
|
||
|
||
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(
|
||
FrameAnimationImportSource source,
|
||
FrameAnimationImportIssueCode code,
|
||
string message)
|
||
{
|
||
return new FrameAnimationImportIssue(
|
||
FrameAnimationValidationSeverity.Error,
|
||
code,
|
||
source?.InternalId,
|
||
source?.InternalId,
|
||
message);
|
||
}
|
||
}
|
||
}
|