diff --git a/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Assets/AddressableAssetsData/AddressableAssetSettings.asset index 8840dbc9b..04056350e 100644 --- a/Assets/AddressableAssetsData/AddressableAssetSettings.asset +++ b/Assets/AddressableAssetsData/AddressableAssetSettings.asset @@ -15,7 +15,7 @@ MonoBehaviour: m_DefaultGroup: 09546aca93a801441859af2a811fe53b m_currentHash: serializedVersion: 2 - Hash: cf6c8ca853b0b2e0e22d46c14944816d + Hash: e92b9b9776d0601449874b8c1b0ed1ae m_OptimizeCatalogSize: 0 m_BuildRemoteCatalog: 0 m_BundleLocalCatalog: 0 diff --git a/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset b/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset index 8c048b600..ccf825a58 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Scene_Subway.asset @@ -22,23 +22,8 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 - - m_GUID: 2d4ccf1c582289344976ab5c3af05515 - m_Address: "Animation/\u5730\u94C1\u533B\u751F" - m_ReadOnly: 0 - m_SerializedLabels: [] - FlaggedDuringContentUpdateRestriction: 0 - - m_GUID: 0057bf4b5fe16aa4b8c7db27d0ddfc0a - m_Address: "Animation/\u5730\u94C1\u706B\u5C71" - m_ReadOnly: 0 - m_SerializedLabels: [] - FlaggedDuringContentUpdateRestriction: 0 - - m_GUID: 40f07326ace570241bd93fcfc3decb87 - m_Address: "Animation/\u5730\u94C1\u706B\u5C71\u6697" - m_ReadOnly: 0 - m_SerializedLabels: [] - FlaggedDuringContentUpdateRestriction: 0 - - m_GUID: a63b52b3e8d9e0441aaff086b9c1ae6b - m_Address: "Animation/\u5730\u94C1\u533B\u751F\u6697" + - m_GUID: b0ee8d48dab31814da87651bba9b5041 + m_Address: "FrameAnimation/\u5730\u94C1\u533B\u751F" m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 @@ -57,6 +42,11 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: b02d1e0ae4bc971408a0b6a667597624 + m_Address: "FrameAnimation/\u5730\u94C1\u706B\u5C71" + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: 77169ce22e430f64fb36c771815a4a7b, type: 2} m_SchemaSet: diff --git a/Assets/Editor/AutoNextMetadataTests.cs b/Assets/Editor/AutoNextMetadataTests.cs new file mode 100644 index 000000000..39be85165 --- /dev/null +++ b/Assets/Editor/AutoNextMetadataTests.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using AibisDream.Utility; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Yarn.Markup; +using Yarn.Unity; + +namespace AibisDream.SystemEditor.Tests +{ + public sealed class AutoNextMetadataTests + { + [Test] + public void AutoNextWithoutParameter_UsesExistingFixedDelay() + { + var lineInfo = LineInfo.Generate(CreateLine("auto_next")); + + Assert.That(lineInfo.isAutoSkip, Is.True); + Assert.That(lineInfo.autoNextDelaySeconds, Is.Null); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay)); + } + + [Test] + public void AutoNextWithParameter_UsesSpecifiedSeconds() + { + var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5")); + + Assert.That(lineInfo.isAutoSkip, Is.True); + Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f)); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500)); + } + + [Test] + public void AutoNextWithZeroDelay_IsValid() + { + var lineInfo = LineInfo.Generate(CreateLine("auto_next:0")); + + Assert.That(lineInfo.isAutoSkip, Is.True); + Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(0f)); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.Zero); + } + + [Test] + public void AutoNextParameter_UsesInvariantCulture() + { + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo("de-DE"); + var lineInfo = LineInfo.Generate(CreateLine("auto_next:4.5")); + + Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(4.5f)); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(4500)); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + [TestCase("auto_next:")] + [TestCase("auto_next:abc")] + [TestCase("auto_next:-1")] + [TestCase("auto_next:NaN")] + [TestCase("auto_next:Infinity")] + [TestCase("auto_next:2147484")] + public void AutoNextWithInvalidParameter_FallsBackToExistingFixedDelay(string metadata) + { + LogAssert.Expect(LogType.Warning, new Regex("auto_next 参数无效")); + + var lineInfo = LineInfo.Generate(CreateLine(metadata)); + + Assert.That(lineInfo.isAutoSkip, Is.True); + Assert.That(lineInfo.autoNextDelaySeconds, Is.Null); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(ConstRef.FixedDelay)); + } + + [TestCase("auto_next_extra")] + [TestCase("auto_next_extra:4.5")] + [TestCase("AUTO_NEXT")] + [TestCase("AUTO_NEXT:4.5")] + public void SimilarMetadata_IsNotRecognized(string metadata) + { + var line = CreateLine(metadata); + + Assert.That(line.IsAutoSkipLine(), Is.False); + Assert.That(line.TryGetAutoNextDelaySeconds(out _), Is.False); + } + + [Test] + public void ParameterizedTag_TakesPrecedenceOverPlainTag() + { + var lineInfo = LineInfo.Generate(CreateLine("auto_next", "auto_next:2.5")); + + Assert.That(lineInfo.isAutoSkip, Is.True); + Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f)); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500)); + } + + [Test] + public void MultipleParameterizedTags_UseFirstAndWarn() + { + LogAssert.Expect(LogType.Warning, new Regex("存在多个 auto_next 参数标签")); + + var lineInfo = LineInfo.Generate(CreateLine("auto_next:2.5", "auto_next:4.5")); + + Assert.That(lineInfo.autoNextDelaySeconds, Is.EqualTo(2.5f)); + Assert.That(lineInfo.CalcAutoNextDelayTime(), Is.EqualTo(2500)); + } + + private static LocalizedLine CreateLine(params string[] metadata) + { + return new LocalizedLine + { + TextID = "line:auto-next-test", + Metadata = metadata, + Text = new MarkupParseResult("Test line", new List()) + }; + } + } +} diff --git a/Assets/Editor/AutoNextMetadataTests.cs.meta b/Assets/Editor/AutoNextMetadataTests.cs.meta new file mode 100644 index 000000000..dca6be407 --- /dev/null +++ b/Assets/Editor/AutoNextMetadataTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d476e493adf4855468ce46409dae6661 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/FrameAnimation/FrameAnimationEditorElements.cs b/Assets/Editor/FrameAnimation/FrameAnimationEditorElements.cs new file mode 100644 index 000000000..25463b299 --- /dev/null +++ b/Assets/Editor/FrameAnimation/FrameAnimationEditorElements.cs @@ -0,0 +1,254 @@ +using System; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AibisDream.FrameAnimation.Editor +{ + internal enum FrameAnimationBrowserEntryKind + { + Header, + Clip, + Flow, + Source + } + + internal enum FrameAnimationBrowserIssueState + { + None, + Warning, + Error + } + + internal sealed class FrameAnimationBrowserEntry + { + public FrameAnimationBrowserEntryKind Kind; + public string Title = string.Empty; + public string Id = string.Empty; + public string TypeLabel = string.Empty; + public string Meta = string.Empty; + public string StatusLabel = string.Empty; + public Color? AccentColor; + public FrameAnimationBrowserIssueState IssueState; + public FrameAnimationEditorSelection Selection; + + public bool IsHeader => Kind == FrameAnimationBrowserEntryKind.Header; + } + + internal readonly struct FrameAnimationFlowColorPalette + { + public readonly Color Raw; + public readonly Color Stroke; + public readonly Color Header; + public readonly Color Muted; + public readonly Color Text; + + public FrameAnimationFlowColorPalette(Color raw, Color stroke, Color header, Color muted, Color text) + { + Raw = raw; + Stroke = stroke; + Header = header; + Muted = muted; + Text = text; + } + } + + internal static class FrameAnimationFlowColorUtility + { + private static readonly Color Panel = new Color32(43, 47, 52, 255); + private static readonly Color LightText = new Color32(244, 246, 248, 255); + private static readonly Color DarkText = new Color32(24, 27, 30, 255); + private static readonly Color Neutral = new Color32(100, 168, 216, 255); + + internal static Color ResolveRaw(FrameAnimationGraph graph, AnimationFlow flow) + { + return Opaque(ResolveStored(graph, flow)); + } + + internal static Color ResolveStored(FrameAnimationGraph graph, AnimationFlow flow) + { + if (graph == null || flow == null) + { + return Neutral; + } + var data = graph.EditorData?.FlowEditorData == null + ? null + : System.Linq.Enumerable.FirstOrDefault(graph.EditorData.FlowEditorData, + item => item != null && item.FlowId == flow.Id); + return data?.Color ?? Neutral; + } + + internal static FrameAnimationFlowColorPalette CreatePalette(Color value) + { + var raw = Opaque(value); + var luminance = Luminance(raw); + var stroke = luminance < 0.30f + ? Color.Lerp(raw, Color.white, Mathf.InverseLerp(0.30f, 0.02f, luminance) * 0.42f) + : luminance > 0.82f + ? Color.Lerp(raw, Color.black, Mathf.InverseLerp(0.82f, 1f, luminance) * 0.22f) + : raw; + stroke.a = 1f; + var header = Color.Lerp(Panel, stroke, 0.68f); + header.a = 1f; + var muted = Color.Lerp(Panel, stroke, 0.42f); + muted.a = 1f; + var text = Luminance(header) > 0.22f ? DarkText : LightText; + return new FrameAnimationFlowColorPalette(raw, stroke, header, muted, text); + } + + internal static Color Opaque(Color value) => new Color( + Mathf.Clamp01(value.r), Mathf.Clamp01(value.g), Mathf.Clamp01(value.b), 1f); + + internal static float Luminance(Color value) + { + var linear = value.linear; + return linear.r * 0.2126f + linear.g * 0.7152f + linear.b * 0.0722f; + } + } + + internal sealed class FrameAnimationResourceRowElement : VisualElement + { + private readonly VisualElement flowDot; + private readonly Label typeBadge; + private readonly Label titleLabel; + private readonly Label idLabel; + private readonly Label metaLabel; + private readonly Label statusBadge; + + internal FrameAnimationResourceRowElement() + { + AddToClassList("fa-resource-row"); + + var main = new VisualElement(); + main.AddToClassList("fa-resource-row__main"); + flowDot = new VisualElement { pickingMode = PickingMode.Ignore }; + flowDot.AddToClassList("fa-flow-dot"); + typeBadge = new Label { pickingMode = PickingMode.Ignore }; + typeBadge.AddToClassList("fa-badge"); + titleLabel = new Label { pickingMode = PickingMode.Ignore }; + titleLabel.AddToClassList("fa-resource-row__name"); + idLabel = new Label { pickingMode = PickingMode.Ignore }; + idLabel.AddToClassList("fa-resource-row__id"); + statusBadge = new Label { pickingMode = PickingMode.Ignore }; + statusBadge.AddToClassList("fa-badge"); + main.Add(flowDot); + main.Add(typeBadge); + main.Add(titleLabel); + main.Add(idLabel); + main.Add(statusBadge); + + metaLabel = new Label { pickingMode = PickingMode.Ignore }; + metaLabel.AddToClassList("fa-resource-row__meta"); + Add(main); + Add(metaLabel); + } + + internal void Bind(FrameAnimationBrowserEntry entry, bool selected) + { + userData = entry; + var isHeader = entry?.IsHeader == true; + EnableInClassList("fa-resource-group", isHeader); + EnableInClassList("fa-resource-row--selected", selected); + EnableInClassList("fa-resource-row--error", entry?.IssueState == FrameAnimationBrowserIssueState.Error); + EnableInClassList("fa-resource-row--warning", entry?.IssueState == FrameAnimationBrowserIssueState.Warning); + if (entry == null) + { + style.display = DisplayStyle.None; + return; + } + style.display = DisplayStyle.Flex; + titleLabel.text = entry.Title; + titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + idLabel.text = entry.Id; + metaLabel.text = entry.Meta; + typeBadge.text = entry.TypeLabel; + typeBadge.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex; + idLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex; + metaLabel.style.display = isHeader ? DisplayStyle.None : DisplayStyle.Flex; + statusBadge.text = entry.StatusLabel; + statusBadge.style.display = isHeader || string.IsNullOrEmpty(entry.StatusLabel) ? DisplayStyle.None : DisplayStyle.Flex; + flowDot.style.display = !isHeader && entry.AccentColor.HasValue ? DisplayStyle.Flex : DisplayStyle.None; + if (entry.AccentColor.HasValue) + { + flowDot.style.backgroundColor = FrameAnimationFlowColorUtility.CreatePalette(entry.AccentColor.Value).Stroke; + } + typeBadge.EnableInClassList("fa-badge--blue", entry.Kind == FrameAnimationBrowserEntryKind.Clip); + typeBadge.EnableInClassList("fa-badge--purple", entry.Kind == FrameAnimationBrowserEntryKind.Flow); + typeBadge.EnableInClassList("fa-badge--green", entry.Kind == FrameAnimationBrowserEntryKind.Source); + statusBadge.EnableInClassList("fa-badge--red", entry.IssueState == FrameAnimationBrowserIssueState.Error); + statusBadge.EnableInClassList("fa-badge--yellow", entry.IssueState == FrameAnimationBrowserIssueState.Warning); + } + } + + internal sealed class FrameAnimationFrameRowElement : VisualElement + { + private readonly ObjectField spriteField; + private readonly IntegerField durationField; + private readonly Label frameNameLabel; + private readonly Label sourceIndexLabel; + private FrameClip clip; + private int index; + private bool readOnly; + private Action changed; + + internal FrameAnimationFrameRowElement() + { + AddToClassList("fa-frame-row"); + spriteField = new ObjectField("Sprite") { objectType = typeof(Sprite), allowSceneObjects = false }; + durationField = new IntegerField("Duration Ms"); + var readOnlyRow = new VisualElement(); + readOnlyRow.AddToClassList("fa-frame-row__readonly"); + frameNameLabel = new Label(); + sourceIndexLabel = new Label(); + readOnlyRow.Add(frameNameLabel); + readOnlyRow.Add(sourceIndexLabel); + Add(spriteField); + Add(durationField); + Add(readOnlyRow); + spriteField.RegisterValueChangedCallback(OnSpriteChanged); + durationField.RegisterValueChangedCallback(OnDurationChanged); + } + + internal void Bind(FrameClip value, int itemIndex, bool isReadOnly, Action onChanged) + { + clip = value; + index = itemIndex; + readOnly = isReadOnly; + changed = onChanged; + userData = itemIndex; + var frame = clip != null && index >= 0 && index < clip.Frames.Count ? clip.Frames[index] : null; + spriteField.SetValueWithoutNotify(frame?.Sprite); + durationField.SetValueWithoutNotify(frame?.DurationMs ?? 100); + frameNameLabel.text = "Frame " + (frame?.FrameName ?? string.Empty); + sourceIndexLabel.text = "Source " + (frame?.SourceIndex.ToString() ?? "-1"); + spriteField.SetEnabled(!readOnly); + durationField.SetEnabled(!readOnly); + } + + private void OnSpriteChanged(ChangeEvent evt) + { + if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return; + var serialized = new SerializedObject(clip); + serialized.Update(); + var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index); + element.FindPropertyRelative("sprite").objectReferenceValue = evt.newValue; + serialized.ApplyModifiedProperties(); + EditorUtility.SetDirty(clip); + changed?.Invoke(); + } + + private void OnDurationChanged(ChangeEvent evt) + { + if (readOnly || clip == null || index < 0 || index >= clip.Frames.Count) return; + var serialized = new SerializedObject(clip); + serialized.Update(); + var element = serialized.FindProperty("frames").GetArrayElementAtIndex(index); + element.FindPropertyRelative("durationMs").intValue = Mathf.Max(1, evt.newValue); + serialized.ApplyModifiedProperties(); + durationField.SetValueWithoutNotify(Mathf.Max(1, evt.newValue)); + EditorUtility.SetDirty(clip); + changed?.Invoke(); + } + } +} diff --git a/Assets/Editor/FrameAnimation/FrameAnimationEditorElements.cs.meta b/Assets/Editor/FrameAnimation/FrameAnimationEditorElements.cs.meta new file mode 100644 index 000000000..dc0efdba2 --- /dev/null +++ b/Assets/Editor/FrameAnimation/FrameAnimationEditorElements.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 876191b33b10eef44b133cc291ebc912 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/FrameAnimation/FrameAnimationEditorServices.cs b/Assets/Editor/FrameAnimation/FrameAnimationEditorServices.cs index 53ca236e8..d9434ca82 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationEditorServices.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationEditorServices.cs @@ -122,7 +122,7 @@ namespace AibisDream.FrameAnimation.Editor { if (clip == null || !clip.IsImported) { - return "Manual"; + return clip != null && clip.HasStandaloneImportSource ? "Standalone Source" : "Manual"; } var sourceId = clip.ImportInfo?.ImportSourceId; if (sourceNames != null && !string.IsNullOrEmpty(sourceId) && @@ -232,7 +232,7 @@ namespace AibisDream.FrameAnimation.Editor if (clip.IsImported || AssetDatabase.IsSubAsset(clip) || string.IsNullOrEmpty(AssetDatabase.GetAssetPath(clip))) { - error = "只能添加独立 .asset 形式的 Manual Clip。"; + error = "只能添加独立 .asset 形式、且不由 Graph ImportSource 管理的 Clip。"; return false; } if (graph.Clips.Contains(clip)) diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphAuthoringServices.cs b/Assets/Editor/FrameAnimation/FrameAnimationGraphAuthoringServices.cs index aa2216769..764892ae4 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationGraphAuthoringServices.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphAuthoringServices.cs @@ -403,7 +403,7 @@ namespace AibisDream.FrameAnimation.Editor public static string MakeUniqueFlowId(FrameAnimationGraph graph, AnimationNode node) { - var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "Flow"; + var root = (string.IsNullOrWhiteSpace(node?.DisplayName) ? "Animation" : node.DisplayName) + "_Flow"; var candidate = root; var suffix = 2; while (!FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, candidate)) diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uss b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uss index 0bfb16281..b4494ddff 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uss +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uss @@ -1,3 +1,642 @@ +:root { + --fa-bg: rgb(27, 29, 32); + --fa-canvas: rgb(30, 32, 35); + --fa-panel: rgb(36, 39, 43); + --fa-panel-raised: rgb(43, 46, 51); + --fa-panel-hover: rgb(48, 52, 58); + --fa-field: rgb(28, 31, 34); + --fa-line: rgb(17, 19, 21); + --fa-line-soft: rgb(62, 67, 74); + --fa-text: rgb(215, 217, 220); + --fa-muted: rgb(143, 150, 158); + --fa-accent: rgb(61, 145, 212); + --fa-accent-soft: rgb(36, 79, 112); + --fa-success: rgb(86, 185, 123); + --fa-warning: rgb(224, 184, 88); + --fa-error: rgb(223, 106, 106); +} + +.fa-workbench { + flex-grow: 1; + flex-direction: column; + background-color: var(--fa-bg); + color: var(--fa-text); + font-size: 12px; +} + +.fa-global-toolbar-host, +.fa-main-host, +.fa-bottom-host { + flex-shrink: 0; +} + +.fa-main-host { + flex-grow: 1; + min-height: 0; +} + +.fa-workbench .unity-toolbar { + min-height: 32px; + padding-left: 6px; + padding-right: 6px; + background-color: var(--fa-panel-raised); + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-workbench .unity-toolbar-button, +.fa-workbench .unity-button { + min-height: 24px; + margin-left: 2px; + margin-right: 2px; + padding-left: 8px; + padding-right: 8px; + border-left-width: 1px; + border-right-width: 1px; + border-top-width: 1px; + border-bottom-width: 1px; + border-left-color: var(--fa-line); + border-right-color: var(--fa-line); + border-top-color: var(--fa-line); + border-bottom-color: var(--fa-line); + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + background-color: rgb(54, 58, 63); + color: var(--fa-text); +} + +.fa-workbench .unity-toolbar-button:hover, +.fa-workbench .unity-button:hover { + background-color: rgb(70, 75, 81); +} + +.fa-button--primary { + background-color: rgb(42, 105, 150); + border-left-color: rgb(25, 74, 104); + border-right-color: rgb(25, 74, 104); + border-top-color: rgb(25, 74, 104); + border-bottom-color: rgb(25, 74, 104); +} + +.fa-button--danger { + background-color: rgb(87, 54, 56); + color: rgb(244, 205, 207); +} + +.fa-toolbar-group { + flex-direction: row; + align-items: center; + flex-shrink: 0; +} + +.fa-toolbar-separator { + width: 1px; + height: 21px; + margin-left: 5px; + margin-right: 5px; + background-color: var(--fa-line); +} + +.fa-panel { + flex-direction: column; + flex-shrink: 0; + min-height: 0; + background-color: var(--fa-panel); +} + +.fa-panel-header { + height: 28px; + min-height: 28px; + padding-left: 9px; + padding-right: 9px; + -unity-text-align: middle-left; + -unity-font-style: bold; + background-color: rgb(44, 47, 51); + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-resizer--vertical { + width: 4px; + background-color: rgb(24, 26, 29); +} + +.fa-resizer--horizontal { + height: 4px; + background-color: rgb(24, 26, 29); +} + +.fa-resizer--vertical:hover, +.fa-resizer--horizontal:hover { + background-color: var(--fa-accent); +} + +.fa-resource-tabs { + min-height: 34px; + padding-left: 4px; + padding-right: 4px; + padding-top: 3px; + background-color: rgb(31, 34, 38); +} + +.fa-resource-tabs .unity-toolbar-button, +.fa-bottom-tabs .unity-toolbar-button { + flex-grow: 1; + flex-basis: 0; + min-height: 28px; + margin-left: 1px; + margin-right: 1px; + border-left-width: 0; + border-right-width: 0; + border-top-width: 0; + border-bottom-width: 2px; + border-bottom-color: rgba(0, 0, 0, 0); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-color: rgba(0, 0, 0, 0); + color: rgb(169, 175, 182); +} + +.fa-resource-tabs .unity-toolbar-button:hover, +.fa-bottom-tabs .unity-toolbar-button:hover { + color: var(--fa-text); + background-color: rgb(48, 52, 57); +} + +.fa-resource-tabs .unity-toolbar-button.fa-tab--active, +.fa-bottom-tabs .unity-toolbar-button.fa-tab--active { + color: white; + background-color: var(--fa-panel-raised); + border-bottom-color: var(--fa-accent); + -unity-font-style: bold; +} + +.fa-resource-tools { + padding-left: 7px; + padding-right: 7px; + padding-top: 7px; + padding-bottom: 7px; + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-resource-list { + flex-grow: 1; + min-height: 0; + background-color: var(--fa-panel); +} + +.fa-resource-row { + height: 48px; + padding-left: 9px; + padding-right: 7px; + padding-top: 5px; + padding-bottom: 4px; + border-left-width: 3px; + border-left-color: rgba(0, 0, 0, 0); + border-bottom-width: 1px; + border-bottom-color: rgb(32, 35, 39); +} + +.fa-resource-row:hover { + background-color: var(--fa-panel-hover); +} + +.fa-resource-row--selected { + background-color: rgb(37, 75, 103); + border-left-color: rgb(98, 178, 234); +} + +.fa-resource-row--error { + border-left-color: var(--fa-error); +} + +.fa-resource-row--warning { + border-left-color: var(--fa-warning); +} + +.fa-resource-row__main, +.fa-resource-row__meta, +.fa-badge-row { + flex-direction: row; + align-items: center; +} + +.fa-resource-row__main { + height: 20px; +} + +.fa-resource-row__name { + flex-grow: 1; + min-width: 0; + -unity-font-style: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fa-resource-row__id { + max-width: 115px; + margin-left: 5px; + color: rgb(156, 163, 171); + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.fa-resource-row__meta { + height: 17px; + color: var(--fa-muted); + font-size: 10px; +} + +.fa-resource-group { + height: 27px; + padding-left: 8px; + padding-right: 8px; + -unity-text-align: middle-left; + -unity-font-style: bold; + color: rgb(184, 189, 195); + background-color: rgb(41, 44, 49); + border-top-width: 1px; + border-top-color: rgb(52, 56, 61); + border-bottom-width: 1px; + border-bottom-color: rgb(26, 28, 31); +} + +.fa-badge { + min-height: 16px; + margin-right: 4px; + padding-left: 5px; + padding-right: 5px; + -unity-text-align: middle-center; + font-size: 9px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + background-color: rgb(52, 56, 61); + color: rgb(174, 180, 187); +} + +.fa-badge--blue { background-color: rgb(37, 63, 85); color: rgb(143, 202, 239); } +.fa-badge--green { background-color: rgb(35, 63, 49); color: rgb(131, 209, 160); } +.fa-badge--yellow { background-color: rgb(74, 62, 37); color: rgb(240, 200, 111); } +.fa-badge--red { background-color: rgb(75, 41, 44); color: rgb(239, 141, 147); } +.fa-badge--purple { background-color: rgb(59, 48, 75); color: rgb(195, 168, 232); } + +.fa-flow-dot { + width: 8px; + height: 8px; + margin-right: 6px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.fa-canvas-panel { + flex-grow: 1; + min-width: 280px; + min-height: 0; + flex-direction: column; + background-color: var(--fa-canvas); +} + +.fa-canvas-toolbar { + min-height: 32px; +} + +.fa-toolbar-label { + margin-left: 2px; + margin-right: 4px; + color: var(--fa-muted); + -unity-text-align: middle-left; +} + +.fa-canvas-toolbar #flow-focus-field { + flex-shrink: 1; +} + +.fa-flow-focus { + min-width: 110px; + max-width: 240px; + padding-left: 7px; + padding-right: 7px; + -unity-font-style: bold; + -unity-text-align: middle-left; + border-left-width: 3px; + border-left-color: rgba(0, 0, 0, 0); +} + +.fa-empty-state { + color: var(--fa-muted); +} + +.fa-node { + min-width: 224px; + width: 224px; + max-width: 224px; + min-height: 164px; + border-left-width: 1px; + border-right-width: 1px; + border-top-width: 1px; + border-bottom-width: 1px; + border-left-color: var(--fa-line); + border-right-color: var(--fa-line); + border-top-color: var(--fa-line); + border-bottom-color: var(--fa-line); + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + background-color: rgb(43, 47, 52); +} + +.fa-node .title { + min-height: 31px; + padding-left: 7px; + padding-right: 7px; + background-color: rgb(46, 111, 155); + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-node .title-label { + -unity-font-style: bold; +} + +.fa-node .input, +.fa-node .output { + min-width: 12px; +} + +.fa-node__meta { + flex-direction: row; + justify-content: space-between; + margin-left: 8px; + margin-right: 8px; + margin-top: 4px; + margin-bottom: 4px; + color: rgb(175, 181, 188); + font-size: 10px; +} + +.fa-node__clip-id { + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.fa-node__behavior { + margin-left: 5px; + color: rgb(201, 205, 210); +} + +.fa-node__badges { + min-height: 21px; + margin-left: 8px; + margin-right: 8px; + margin-top: 5px; + flex-direction: row; + align-items: center; + overflow: hidden; +} + +.fa-node__entry { + min-width: 16px; + height: 16px; + margin-right: 5px; + padding-left: 3px; + padding-right: 3px; + -unity-text-align: middle-center; + -unity-font-style: bold; + font-size: 9px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + +.fa-node__preview { + height: 78px; + margin-left: 8px; + margin-right: 8px; + margin-top: 1px; +} + +.fa-node--focused { + border-left-width: 2px; +} + +.fa-node.selected { + border-left-width: 2px; + border-right-width: 2px; + border-top-width: 2px; + border-bottom-width: 2px; + border-left-color: rgb(105, 185, 237); + border-right-color: rgb(105, 185, 237); + border-top-color: rgb(105, 185, 237); + border-bottom-color: rgb(105, 185, 237); +} + +.fa-node--preview-current { + border-left-color: rgb(55, 174, 240); + border-right-color: rgb(55, 174, 240); + border-top-color: rgb(55, 174, 240); + border-bottom-color: rgb(55, 174, 240); +} + +.fa-node--preview-passed { border-left-color: rgb(83, 154, 113); } +.fa-node--preview-upcoming { opacity: 0.72; } + +.fa-inspector-scroll, +.fa-bottom-scroll { + flex-grow: 1; + min-height: 0; + background-color: var(--fa-panel); +} + +.fa-inspector-title { + padding-left: 11px; + padding-right: 11px; + padding-top: 10px; + padding-bottom: 8px; + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-inspector-title__name { + font-size: 15px; + -unity-font-style: bold; +} + +.fa-inspector-title__id { + margin-top: 3px; + color: var(--fa-muted); + font-size: 10px; +} + +.fa-section { + padding-bottom: 8px; + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-section__title { + height: 27px; + padding-left: 10px; + -unity-text-align: middle-left; + -unity-font-style: bold; + background-color: rgb(41, 44, 48); +} + +.fa-section__body { + padding-left: 10px; + padding-right: 10px; + padding-top: 7px; +} + +.fa-section__body > .unity-base-field, +.fa-section__body > .unity-button, +.fa-section__body > .fa-readonly-row, +.fa-section__body > .fa-callout { + margin-top: 2px; + margin-bottom: 2px; +} + +.fa-readonly-row { + min-height: 22px; + flex-direction: row; + align-items: center; +} + +.fa-readonly-row__label { + width: 118px; + color: rgb(173, 179, 186); +} + +.fa-readonly-row__value { + flex-grow: 1; + min-width: 0; + color: rgb(197, 202, 208); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.fa-callout { + padding-left: 9px; + padding-right: 9px; + padding-top: 7px; + padding-bottom: 7px; + white-space: normal; + border-left-width: 3px; + border-left-color: var(--fa-accent); + background-color: rgb(34, 48, 58); + color: rgb(188, 209, 223); +} + +.fa-callout--warning { border-left-color: var(--fa-warning); background-color: rgb(57, 51, 33); color: rgb(228, 208, 161); } +.fa-callout--error { border-left-color: var(--fa-error); background-color: rgb(63, 39, 42); color: rgb(239, 184, 188); } + +.fa-frame-row { + min-height: 72px; + padding-left: 5px; + padding-right: 5px; + padding-top: 4px; + padding-bottom: 4px; + border-bottom-width: 1px; + border-bottom-color: rgb(48, 51, 55); +} + +.fa-frame-row__readonly { + flex-direction: row; +} + +.fa-frame-row__readonly > .unity-label { + flex-grow: 1; + color: var(--fa-muted); + font-size: 10px; +} + +.fa-bottom-panel { + flex-direction: column; + min-height: 28px; + background-color: rgb(32, 35, 39); + border-top-width: 1px; + border-top-color: var(--fa-line); +} + +.fa-bottom-toolbar { + min-height: 31px; + padding-left: 4px; + padding-right: 4px; +} + +.fa-bottom-tabs { + min-width: 230px; + height: 30px; + flex-direction: row; + align-items: stretch; +} + +.fa-bottom-tabs .unity-toolbar-button { + min-width: 108px; + padding-left: 14px; + padding-right: 14px; +} + +.fa-bottom-toolbar .fa-bottom-collapse { + width: 28px; + min-width: 28px; + padding-left: 0; + padding-right: 0; + font-size: 14px; +} + +.fa-result-controls { + min-height: 31px; + flex-direction: row; + align-items: center; + padding-left: 8px; + padding-right: 8px; + border-bottom-width: 1px; + border-bottom-color: var(--fa-line); +} + +.fa-result-header, +.fa-result-row { + min-height: 29px; + flex-direction: row; + align-items: center; + padding-left: 10px; + padding-right: 10px; + border-bottom-width: 1px; + border-bottom-color: rgb(41, 44, 48); +} + +.fa-result-header { + min-height: 26px; + color: var(--fa-muted); + background-color: rgb(40, 43, 48); +} + +.fa-result-row:hover { background-color: var(--fa-panel-hover); } +.fa-result-severity { width: 82px; -unity-font-style: bold; } +.fa-result-kind { width: 135px; } +.fa-result-object { width: 180px; } +.fa-result-message { flex-grow: 1; min-width: 260px; white-space: normal; } +.fa-severity--error { color: var(--fa-error); } +.fa-severity--warning { color: var(--fa-warning); } +.fa-severity--info { color: rgb(114, 189, 232); } + .fa-preview { position: relative; overflow: hidden; @@ -25,26 +664,6 @@ color: rgb(190, 190, 190); } -.fa-node__preview { - height: 82px; - margin-top: 5px; -} - -.fa-node--preview-current { - border-left-color: rgb(55, 174, 240); - border-right-color: rgb(55, 174, 240); - border-top-color: rgb(55, 174, 240); - border-bottom-color: rgb(55, 174, 240); -} - -.fa-node--preview-passed { - border-left-color: rgb(83, 154, 113); -} - -.fa-node--preview-upcoming { - opacity: 0.72; -} - .fa-clip-preview-panel { flex-shrink: 0; padding-left: 6px; @@ -55,6 +674,26 @@ border-bottom-color: rgb(28, 28, 28); } +.fa-selection-preview-header { + min-height: 34px; + flex-direction: column; + justify-content: center; +} + +.fa-selection-preview-title { + color: var(--fa-text); + -unity-font-style: bold; +} + +.fa-selection-preview-target { + margin-top: 1px; + color: var(--fa-muted); + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .fa-clip-preview-panel .fa-preview { height: 190px; margin-bottom: 4px; @@ -63,10 +702,82 @@ .fa-preview-controls { flex-direction: row; align-items: center; + flex-shrink: 0; + min-height: 27px; + padding-left: 0; + padding-right: 0; +} + +.fa-selection-preview-panel .fa-preview-controls .fa-preview-button { + width: 24px; + min-width: 24px; + max-width: 24px; + height: 24px; + min-height: 24px; + max-height: 24px; + padding-left: 0; + padding-right: 0; + padding-top: 0; + padding-bottom: 0; + margin-left: 1px; + margin-right: 1px; + flex-shrink: 0; + align-items: center; + justify-content: center; +} + +.fa-preview-button__icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.fa-preview-timeline { + min-width: 28px; + flex-grow: 1; + flex-shrink: 1; + margin-left: 5px; + margin-right: 5px; +} + +.fa-preview-time { + width: 82px; + min-width: 82px; + flex-shrink: 0; + white-space: nowrap; + -unity-text-align: middle-right; +} + +.fa-preview-settings { + min-height: 27px; + flex-direction: row; + align-items: center; +} + +.fa-preview-setting { + flex-grow: 1; + flex-basis: 0; + min-width: 0; + margin-right: 5px; + flex-direction: row; + align-items: center; +} + +.fa-preview-setting > .unity-label { + width: 64px; + min-width: 64px; + color: var(--fa-muted); + font-size: 10px; +} + +.fa-preview-setting__field { + flex-grow: 1; + min-width: 0; } .fa-preview-status { font-size: 10px; color: rgb(185, 185, 185); margin-top: 2px; + white-space: normal; } diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uxml b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uxml new file mode 100644 index 000000000..508ef28cc --- /dev/null +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uxml.meta b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uxml.meta new file mode 100644 index 000000000..f4b6d3311 --- /dev/null +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditor.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 3286d11c482fe8640a72c689bed6780c +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs index e9244f119..59344eb67 100644 --- a/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs +++ b/Assets/Editor/FrameAnimation/FrameAnimationGraphEditorWindow.cs @@ -18,13 +18,6 @@ namespace AibisDream.FrameAnimation.Editor private enum BottomTab { ImportDiff, Validation } private enum ValidationKindFilter { All, Graph, Clip, Flow, Source, Node, Edge } - private sealed class BrowserEntry - { - public bool IsHeader; - public string Label; - public FrameAnimationEditorSelection Selection; - } - private const string LastGraphGuidKey = "AibisDream.FrameAnimation.Workspace.LastGraphGuid"; [SerializeField] private FrameAnimationGraph graph; @@ -35,23 +28,26 @@ namespace AibisDream.FrameAnimation.Editor private Label canvasLabel; private FrameAnimationGraphView graphView; private PopupField flowFocusField; - private Label flowFocusStatusLabel; - private ToolbarButton exitFlowFocusButton; private string focusedFlowId = string.Empty; private IReadOnlyList multiSelectedNodes = Array.Empty(); private AnimationNode lastSelectedNode; private Vector2 browserDragStart; private bool browserDragPending; private ListView resourceList; - private readonly List browserEntries = new List(); + private readonly List browserEntries = new List(); private ToolbarSearchField searchField; private EnumField filterField; private EnumField sortField; - private IMGUIContainer propertyContainer; - private IMGUIContainer bottomContainer; + private VisualElement propertyContainer; + private VisualElement bottomContainer; private VisualElement leftPanel; private VisualElement rightPanel; private VisualElement bottomPanel; + private VisualElement globalToolbarHost; + private VisualElement mainHost; + private VisualElement bottomHost; + private readonly List resourceTabButtons = new List(); + private readonly List bottomTabButtons = new List(); private ResourceTab resourceTab; private BottomTab bottomTab; private FrameAnimationClipFilter clipFilter; @@ -68,19 +64,23 @@ namespace AibisDream.FrameAnimation.Editor private SerializedObject clipSerializedObject; private FrameClip frameListClip; private ReorderableList frameList; + private bool frameListEditable; private readonly FrameAnimationPreviewCoordinator previewCoordinator = new FrameAnimationPreviewCoordinator(); private readonly List previewTimelineSliders = new List(); private readonly List