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

345 lines
15 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 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);
}
}
}