From 447d61f94166b95791680833aaf42e39e2dfb28b Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Fri, 17 Jul 2026 20:19:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(frame-animation):=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E6=94=AF=E6=8C=81=E5=85=B1=E4=BA=AB=20SpriteRect=20?= =?UTF-8?q?=E4=B8=8E=20Graph=20=E5=B1=9E=E6=80=A7=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FrameAnimationGraphEditorWindow.cs | 59 +++++ .../FrameAnimationImportModels.cs | 88 ++++++- .../FrameAnimationImportService.cs | 19 +- .../FrameAnimationSpriteUtility.cs | 229 +++++++++++++----- 4 files changed, 329 insertions(+), 66 deletions(-) diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs index e556ccf70..e9244f119 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs @@ -29,6 +29,8 @@ namespace AibisDream.FrameAnimation.Editor [SerializeField] private FrameAnimationGraph graph; private ObjectField graphField; + private ToolbarButton graphPropertiesButton; + private ToolbarButton graphBreadcrumbButton; private Label dirtyLabel; private Label canvasLabel; private FrameAnimationGraphView graphView; @@ -173,6 +175,13 @@ namespace AibisDream.FrameAnimation.Editor graphField.style.width = 330f; graphField.RegisterValueChangedCallback(evt => SetGraph(evt.newValue as FrameAnimationGraph)); toolbar.Add(graphField); + graphPropertiesButton = new ToolbarButton(SelectGraphProperties) + { + name = "graph-properties-button", + text = "Graph Properties", + tooltip = "查看和编辑当前 Graph 的参数" + }; + toolbar.Add(graphPropertiesButton); toolbar.Add(new ToolbarButton(CreateGraph) { text = "Create" }); toolbar.Add(new ToolbarButton(SaveGraph) { text = "Save" }); dirtyLabel = new Label(); @@ -212,6 +221,15 @@ namespace AibisDream.FrameAnimation.Editor } }; var canvasToolbar = new Toolbar(); + graphBreadcrumbButton = new ToolbarButton(SelectGraphProperties) + { + name = "graph-breadcrumb-button", + text = "Graph", + tooltip = "返回当前 Graph 的属性" + }; + graphBreadcrumbButton.style.minWidth = 80f; + graphBreadcrumbButton.style.maxWidth = 220f; + canvasToolbar.Add(graphBreadcrumbButton); canvasToolbar.Add(new ToolbarButton(ShowAllNodes) { text = "Show All" }); flowFocusStatusLabel = new Label("Show All") { @@ -890,6 +908,20 @@ namespace AibisDream.FrameAnimation.Editor } } + private void SelectGraphProperties() + { + if (graph == null) + { + return; + } + + graphView?.ClearSelection(); + resourceList?.ClearSelection(); + multiSelectedNodes = Array.Empty(); + lastSelectedNode = null; + SetSelection(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Graph, graph), false); + } + private void RefreshBrowser() { if (resourceList == null) @@ -1217,6 +1249,7 @@ namespace AibisDream.FrameAnimation.Editor private void RefreshCanvasSummary() { + RefreshGraphNavigation(); if (canvasLabel == null) { return; @@ -1237,6 +1270,25 @@ namespace AibisDream.FrameAnimation.Editor $"当前定位:{located}"; } + private void RefreshGraphNavigation() + { + var hasGraph = graph != null; + graphPropertiesButton?.SetEnabled(hasGraph); + graphBreadcrumbButton?.SetEnabled(hasGraph); + if (graphBreadcrumbButton == null) + { + return; + } + + var graphName = !hasGraph + ? "Graph" + : string.IsNullOrWhiteSpace(graph.DisplayName) ? graph.Id : graph.DisplayName; + graphBreadcrumbButton.text = $"Graph: {graphName}"; + graphBreadcrumbButton.tooltip = hasGraph + ? $"查看和编辑 {graphName} 的 Graph 参数" + : "未选择 Graph"; + } + private static string SelectionName(FrameAnimationEditorSelection selected) { return selected.Value switch @@ -1805,6 +1857,13 @@ namespace AibisDream.FrameAnimation.Editor foreach (var source in importPreview.Sources) { EditorGUILayout.LabelField(source.Source.DisplayName, EditorStyles.boldLabel); + if (source.Document != null) + { + var aliasCount = source.Document.Frames.Count - source.Document.SpriteSlots.Count; + EditorGUILayout.LabelField( + $"逻辑帧 {source.Document.Frames.Count} · SpriteSlot {source.Document.SpriteSlots.Count} · 共享别名 {aliasCount}", + EditorStyles.miniLabel); + } if (GUILayout.Button("Locate Source", GUILayout.Width(110f))) { SetSelection(new FrameAnimationEditorSelection(FrameAnimationEditorSelectionKind.Source, source.Source)); diff --git a/Assets/Editor/FrameAnimation/FrameAnimationImportModels.cs b/Assets/Editor/FrameAnimation/FrameAnimationImportModels.cs index 0b1180ba0..53f5a66d3 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationImportModels.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationImportModels.cs @@ -124,9 +124,31 @@ namespace AibisDream.FrameAnimation.Editor } } + internal sealed class AsepriteSpriteSlot + { + public RectInt Rect { get; } + public string CanonicalFrameName { get; } + public IReadOnlyList SourceIndices { get; } + public IReadOnlyList FrameNames { get; } + + public AsepriteSpriteSlot( + RectInt rect, + string canonicalFrameName, + IReadOnlyList sourceIndices, + IReadOnlyList frameNames) + { + Rect = rect; + CanonicalFrameName = canonicalFrameName ?? string.Empty; + SourceIndices = sourceIndices ?? Array.Empty(); + FrameNames = frameNames ?? Array.Empty(); + } + } + internal sealed class AsepriteSourceDocument { public IReadOnlyList Frames { get; } + public IReadOnlyList SpriteSlots { get; } + public IReadOnlyDictionary SpriteSlotBySourceIndex { get; } public IReadOnlyList Tags { get; } public Vector2Int TextureSize { get; } public string ImageName { get; } @@ -138,10 +160,70 @@ namespace AibisDream.FrameAnimation.Editor string imageName) { Frames = frames ?? Array.Empty(); + BuildSpriteSlots(Frames, out var spriteSlots, out var slotBySourceIndex); + SpriteSlots = spriteSlots; + SpriteSlotBySourceIndex = slotBySourceIndex; Tags = tags ?? Array.Empty(); TextureSize = textureSize; ImageName = imageName ?? string.Empty; } + + private static void BuildSpriteSlots( + IReadOnlyList frames, + out IReadOnlyList spriteSlots, + out IReadOnlyDictionary slotBySourceIndex) + { + var builders = new List(); + var buildersByRect = new Dictionary(); + for (var sourceIndex = 0; sourceIndex < frames.Count; sourceIndex++) + { + var frame = frames[sourceIndex]; + if (!buildersByRect.TryGetValue(frame.Rect, out var builder)) + { + builder = new SpriteSlotBuilder(frame.Rect, frame.FrameName); + buildersByRect.Add(frame.Rect, builder); + builders.Add(builder); + } + builder.SourceIndices.Add(sourceIndex); + builder.FrameNames.Add(frame.FrameName); + } + + var slots = builders.Select(builder => builder.Build()).ToArray(); + var index = new Dictionary(); + foreach (var slot in slots) + { + foreach (var sourceIndex in slot.SourceIndices) + { + index[sourceIndex] = slot; + } + } + + spriteSlots = slots; + slotBySourceIndex = index; + } + + private sealed class SpriteSlotBuilder + { + public RectInt Rect { get; } + public string CanonicalFrameName { get; } + public List SourceIndices { get; } = new List(); + public List FrameNames { get; } = new List(); + + public SpriteSlotBuilder(RectInt rect, string canonicalFrameName) + { + Rect = rect; + CanonicalFrameName = canonicalFrameName ?? string.Empty; + } + + public AsepriteSpriteSlot Build() + { + return new AsepriteSpriteSlot( + Rect, + CanonicalFrameName, + SourceIndices.ToArray(), + FrameNames.ToArray()); + } + } } internal sealed class FrameAnimationClipDiff @@ -171,6 +253,8 @@ namespace AibisDream.FrameAnimation.Editor { public FrameAnimationSpriteChangeKind Kind { get; } public string FrameName { get; } + public IReadOnlyList AliasFrameNames { get; } + public int SourceFrameCount => AliasFrameNames.Count; public Rect Rect { get; } public string Summary { get; } @@ -178,10 +262,12 @@ namespace AibisDream.FrameAnimation.Editor FrameAnimationSpriteChangeKind kind, string frameName, Rect rect, - string summary) + string summary, + IReadOnlyList aliasFrameNames = null) { Kind = kind; FrameName = frameName ?? string.Empty; + AliasFrameNames = aliasFrameNames ?? Array.Empty(); Rect = rect; Summary = summary ?? string.Empty; } diff --git a/Assets/Editor/FrameAnimation/FrameAnimationImportService.cs b/Assets/Editor/FrameAnimation/FrameAnimationImportService.cs index 833c0b214..b54637d96 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationImportService.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationImportService.cs @@ -471,7 +471,8 @@ namespace AibisDream.FrameAnimation.Editor continue; } - var changed = clip.ImportInfo.IsMissingFromSource || !FramesEqual(clip, document, indices); + var changed = clip.ImportInfo.IsMissingFromSource || + !FramesEqual(clip, document, preview.SpritePlan, indices); preview.AddClipDiff(new FrameAnimationClipDiff( changed ? FrameAnimationImportChangeKind.Updated : FrameAnimationImportChangeKind.Unchanged, tag.Name, @@ -498,6 +499,7 @@ namespace AibisDream.FrameAnimation.Editor private static bool FramesEqual( FrameClip clip, AsepriteSourceDocument document, + FrameAnimationSpritePlan spritePlan, IReadOnlyList indices) { if (clip.FrameCount != indices.Count) @@ -511,7 +513,7 @@ namespace AibisDream.FrameAnimation.Editor var frame = clip.Frames[index]; if (frame == null || frame.SourceIndex != sourceIndex || frame.FrameName != sourceFrame.FrameName || frame.DurationMs != sourceFrame.DurationMs || - frame.Sprite == null || frame.Sprite.name != sourceFrame.FrameName) + spritePlan == null || !spritePlan.MatchesSprite(sourceIndex, frame.Sprite)) { return false; } @@ -524,9 +526,9 @@ namespace AibisDream.FrameAnimation.Editor FrameAnimationImportSourcePreview preview) { var source = preview.Source; - var sprites = source.ManageSpriteSlicing + var writableSprites = source.ManageSpriteSlicing ? FrameAnimationSpriteUtility.LoadSpritesByName(AssetDatabase.GetAssetPath(source.Texture)) - : preview.SpritePlan.ReadOnlySprites; + : null; foreach (var diff in preview.ClipDiffs) { @@ -545,9 +547,14 @@ namespace AibisDream.FrameAnimation.Editor var frames = diff.SourceIndices.Select(sourceIndex => { var sourceFrame = preview.Document.Frames[sourceIndex]; - if (!sprites.TryGetValue(sourceFrame.FrameName, out var sprite) || sprite == null) + if (!preview.SpritePlan.TryResolveSprite(sourceIndex, writableSprites, out var sprite)) { - throw new InvalidOperationException($"刷新后找不到 Sprite '{sourceFrame.FrameName}'。 "); + var expectedName = preview.SpritePlan.SpriteNamesBySourceIndex.TryGetValue( + sourceIndex, out var plannedName) + ? plannedName + : sourceFrame.FrameName; + throw new InvalidOperationException( + $"刷新后找不到 SourceFrame '{sourceFrame.FrameName}' 对应的共享 Sprite '{expectedName}'。 "); } return new FrameAnimationFrame(sprite, sourceFrame.DurationMs, sourceFrame.FrameName, sourceIndex); }).ToArray(); diff --git a/Assets/Editor/FrameAnimation/FrameAnimationSpriteUtility.cs b/Assets/Editor/FrameAnimation/FrameAnimationSpriteUtility.cs index f354f468f..589fc1ae2 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationSpriteUtility.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationSpriteUtility.cs @@ -15,18 +15,53 @@ namespace AibisDream.FrameAnimation.Editor public string TexturePath { get; } public IReadOnlyList ExistingRects { get; } public IReadOnlyList PlannedRects { get; } - public IReadOnlyDictionary ReadOnlySprites { get; } + public IReadOnlyDictionary SpriteNamesBySourceIndex { get; } + public IReadOnlyDictionary ReadOnlySpritesBySourceIndex { get; } public FrameAnimationSpritePlan( string texturePath, IReadOnlyList existingRects, IReadOnlyList plannedRects, - IReadOnlyDictionary readOnlySprites) + IReadOnlyDictionary spriteNamesBySourceIndex, + IReadOnlyDictionary readOnlySpritesBySourceIndex) { TexturePath = texturePath ?? string.Empty; ExistingRects = existingRects ?? Array.Empty(); PlannedRects = plannedRects ?? Array.Empty(); - ReadOnlySprites = readOnlySprites ?? new Dictionary(); + SpriteNamesBySourceIndex = spriteNamesBySourceIndex ?? new Dictionary(); + ReadOnlySpritesBySourceIndex = readOnlySpritesBySourceIndex ?? new Dictionary(); + } + + 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 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() + : 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 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(); + var spriteNamesBySourceIndex = new Dictionary(); + 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 { 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(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 existingRects) { var sprites = AssetDatabase.LoadAllAssetsAtPath(texturePath).OfType().ToArray(); - var matches = new Dictionary(); - foreach (var frame in document.Frames) + var matches = new Dictionary(); + 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 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 ReadSpriteRects(TextureImporter importer)