feat(frame-animation): 编辑器支持共享 SpriteRect 与 Graph 属性面板
This commit is contained in:
@@ -15,18 +15,53 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
public string TexturePath { get; }
|
||||
public IReadOnlyList<SpriteRect> ExistingRects { get; }
|
||||
public IReadOnlyList<SpriteRect> PlannedRects { get; }
|
||||
public IReadOnlyDictionary<string, Sprite> ReadOnlySprites { get; }
|
||||
public IReadOnlyDictionary<int, string> SpriteNamesBySourceIndex { get; }
|
||||
public IReadOnlyDictionary<int, Sprite> ReadOnlySpritesBySourceIndex { get; }
|
||||
|
||||
public FrameAnimationSpritePlan(
|
||||
string texturePath,
|
||||
IReadOnlyList<SpriteRect> existingRects,
|
||||
IReadOnlyList<SpriteRect> plannedRects,
|
||||
IReadOnlyDictionary<string, Sprite> readOnlySprites)
|
||||
IReadOnlyDictionary<int, string> spriteNamesBySourceIndex,
|
||||
IReadOnlyDictionary<int, Sprite> readOnlySpritesBySourceIndex)
|
||||
{
|
||||
TexturePath = texturePath ?? string.Empty;
|
||||
ExistingRects = existingRects ?? Array.Empty<SpriteRect>();
|
||||
PlannedRects = plannedRects ?? Array.Empty<SpriteRect>();
|
||||
ReadOnlySprites = readOnlySprites ?? new Dictionary<string, Sprite>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +98,12 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.TextureImporterInvalid,
|
||||
$"无法读取 TextureImporter:{texturePath}"));
|
||||
return new FrameAnimationSpritePlan(texturePath, null, null, null);
|
||||
return new FrameAnimationSpritePlan(texturePath, null, null, null, null);
|
||||
}
|
||||
|
||||
var existingRects = ReadSpriteRects(importer);
|
||||
var existingRects = source.ManageSpriteSlicing && importer.spriteImportMode != SpriteImportMode.Multiple
|
||||
? Array.Empty<SpriteRect>()
|
||||
: ReadSpriteRects(importer);
|
||||
return source.ManageSpriteSlicing
|
||||
? BuildWritablePlan(source, document, preview, texturePath, existingRects)
|
||||
: BuildReadOnlyPlan(source, document, preview, texturePath, existingRects);
|
||||
@@ -145,7 +182,7 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
|
||||
var texturePath = AssetDatabase.GetAssetPath(source.Texture);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("FrameAnimationImportHash:v1");
|
||||
builder.AppendLine("FrameAnimationImportHash:v2");
|
||||
builder.AppendLine(source.AsepriteJson.text ?? string.Empty);
|
||||
builder.AppendLine(AssetDatabase.AssetPathToGUID(texturePath));
|
||||
builder.AppendLine(AssetDatabase.GetAssetDependencyHash(texturePath).ToString());
|
||||
@@ -184,56 +221,113 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
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 originalRects = planned.ToArray();
|
||||
var assigned = new HashSet<SpriteRect>();
|
||||
var spriteNamesBySourceIndex = new Dictionary<int, string>();
|
||||
foreach (var slot in document.SpriteSlots)
|
||||
{
|
||||
var unityRect = ToUnityRect(frame.Rect, source.Texture.height);
|
||||
if (byName.TryGetValue(frame.FrameName, out var matches) && matches.Count > 1)
|
||||
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)
|
||||
{
|
||||
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)
|
||||
if (matched == null)
|
||||
{
|
||||
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;
|
||||
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(
|
||||
changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged,
|
||||
frame.FrameName,
|
||||
FrameAnimationSpriteChangeKind.Added,
|
||||
matched.name,
|
||||
unityRect,
|
||||
changed ? "更新 rect/pivot,保留 spriteID" : "无变化"));
|
||||
BuildSlotSummary("新增 SpriteRect", slot),
|
||||
slot.FrameNames));
|
||||
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 };
|
||||
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(
|
||||
FrameAnimationSpriteChangeKind.Added, frame.FrameName, unityRect, "新增 SpriteRect"));
|
||||
changed ? FrameAnimationSpriteChangeKind.Updated : FrameAnimationSpriteChangeKind.Unchanged,
|
||||
matched.name,
|
||||
unityRect,
|
||||
BuildSlotSummary(changed ? "更新 rect/pivot,保留 spriteID" : "无变化", slot),
|
||||
slot.FrameNames));
|
||||
}
|
||||
|
||||
var sourceNames = new HashSet<string>(document.Frames.Select(frame => frame.FrameName));
|
||||
foreach (var retained in existingRects.Where(rect => !sourceNames.Contains(rect.name)))
|
||||
foreach (var retained in originalRects.Where(rect => !assigned.Contains(rect)))
|
||||
{
|
||||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||||
FrameAnimationSpriteChangeKind.Retained, retained.name, retained.rect, "源中已消失,保留现有 SpriteRect"));
|
||||
FrameAnimationSpriteChangeKind.Retained,
|
||||
retained.name,
|
||||
retained.rect,
|
||||
"源中已消失或不再作为共享帧主 Sprite,保留现有 SpriteRect"));
|
||||
}
|
||||
|
||||
return new FrameAnimationSpritePlan(texturePath, existingRects, planned, null);
|
||||
return new FrameAnimationSpritePlan(
|
||||
texturePath,
|
||||
existingRects,
|
||||
planned,
|
||||
spriteNamesBySourceIndex,
|
||||
null);
|
||||
}
|
||||
|
||||
private static FrameAnimationSpritePlan BuildReadOnlyPlan(
|
||||
@@ -244,38 +338,55 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
IReadOnlyList<SpriteRect> existingRects)
|
||||
{
|
||||
var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType<Sprite>().ToArray();
|
||||
var matches = new Dictionary<string, Sprite>();
|
||||
foreach (var frame in document.Frames)
|
||||
var matches = new Dictionary<int, Sprite>();
|
||||
foreach (var slot in document.SpriteSlots)
|
||||
{
|
||||
var unityRect = ToUnityRect(frame.Rect, source.Texture.height);
|
||||
var named = sprites.Where(sprite => sprite.name == frame.FrameName).ToArray();
|
||||
var unityRect = ToUnityRect(slot.Rect, source.Texture.height);
|
||||
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()))
|
||||
if (rectMatches.Length != 1)
|
||||
{
|
||||
preview.AddIssue(Error(source, FrameAnimationImportIssueCode.SpriteMatchFailed,
|
||||
$"SourceFrame '{frame.FrameName}' 无法按名称和 rect 唯一匹配 Sprite。"));
|
||||
$"共享帧 '{slot.CanonicalFrameName}' 无法按 rect 唯一匹配 Sprite,候选数:{rectMatches.Length}。"));
|
||||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||||
FrameAnimationSpriteChangeKind.Error, frame.FrameName, unityRect, "只读 Sprite 匹配失败"));
|
||||
FrameAnimationSpriteChangeKind.Error,
|
||||
slot.CanonicalFrameName,
|
||||
unityRect,
|
||||
"只读 Sprite 匹配失败",
|
||||
slot.FrameNames));
|
||||
continue;
|
||||
}
|
||||
|
||||
matches[frame.FrameName] = match;
|
||||
foreach (var sourceIndex in slot.SourceIndices)
|
||||
{
|
||||
matches[sourceIndex] = rectMatches[0];
|
||||
}
|
||||
preview.AddSpriteDiff(new FrameAnimationSpriteDiff(
|
||||
FrameAnimationSpriteChangeKind.Unchanged, frame.FrameName, unityRect, $"匹配 Sprite '{match.name}'"));
|
||||
FrameAnimationSpriteChangeKind.Unchanged,
|
||||
rectMatches[0].name,
|
||||
unityRect,
|
||||
BuildSlotSummary($"匹配 Sprite '{rectMatches[0].name}'", slot),
|
||||
slot.FrameNames));
|
||||
}
|
||||
|
||||
return new FrameAnimationSpritePlan(texturePath, existingRects, existingRects, matches);
|
||||
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 string BuildSlotSummary(string prefix, AsepriteSpriteSlot slot)
|
||||
{
|
||||
return slot.SourceIndices.Count > 1
|
||||
? $"{prefix};{slot.SourceIndices.Count} 个 SourceFrame 共享"
|
||||
: prefix;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SpriteRect> ReadSpriteRects(TextureImporter importer)
|
||||
|
||||
Reference in New Issue
Block a user