From 450751e43fe4d9627da938715b3930e9d25ebe10 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:36 +0200 Subject: [PATCH] DT material editor, supporting components --- .../Materials/ConstantEditors.cs | 71 +++++++ .../Materials/MaterialTemplatePickers.cs | 177 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs new file mode 100644 index 00000000..690580df --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs @@ -0,0 +1,71 @@ +using System.Collections.Frozen; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public static class ConstantEditors +{ + public static readonly IEditor DefaultFloat = Editors.DefaultFloat.AsByteEditor(); + public static readonly IEditor DefaultInt = Editors.DefaultInt.AsByteEditor(); + public static readonly IEditor DefaultIntAsFloat = Editors.DefaultInt.IntAsFloatEditor().AsByteEditor(); + public static readonly IEditor DefaultColor = ColorEditor.HighDynamicRange.Reinterpreting(); + + /// + /// Material constants known to be encoded as native s. + /// + /// A editor is nonfunctional for them, as typical values for these constants would fall into the IEEE 754 denormalized number range. + /// + private static readonly FrozenSet KnownIntConstants; + + static ConstantEditors() + { + IReadOnlyList knownIntConstants = [ + "g_ToonIndex", + "g_ToonSpecIndex", + ]; + + KnownIntConstants = knownIntConstants.ToFrozenSet(); + } + + public static IEditor DefaultFor(Name name, MaterialTemplatePickers? materialTemplatePickers = null) + { + if (materialTemplatePickers != null) + { + if (name == Names.SphereMapIndexConstantName) + return materialTemplatePickers.SphereMapIndexPicker; + else if (name == Names.TileIndexConstantName) + return materialTemplatePickers.TileIndexPicker; + } + + if (name.Value != null && name.Value.EndsWith("Color")) + return DefaultColor; + + if (KnownIntConstants.Contains(name)) + return DefaultInt; + + return DefaultFloat; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor AsByteEditor(this IEditor inner) where T : unmanaged + => inner.Reinterpreting(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor IntAsFloatEditor(this IEditor inner) + => inner.Converting(value => int.CreateSaturating(MathF.Round(value)), value => value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithExponent(this IEditor inner, T exponent) + where T : unmanaged, IPowerFunctions, IComparisonOperators + => exponent == T.MultiplicativeIdentity + ? inner + : inner.Converting(value => value < T.Zero ? -T.Pow(-value, T.MultiplicativeIdentity / exponent) : T.Pow(value, T.MultiplicativeIdentity / exponent), value => value < T.Zero ? -T.Pow(-value, exponent) : T.Pow(value, exponent)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithFactorAndBias(this IEditor inner, T factor, T bias) + where T : unmanaged, IMultiplicativeIdentity, IAdditiveIdentity, IMultiplyOperators, IAdditionOperators, ISubtractionOperators, IDivisionOperators, IEqualityOperators + => factor == T.MultiplicativeIdentity && bias == T.AdditiveIdentity + ? inner + : inner.Converting(value => (value - bias) / factor, value => value * factor + bias); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs new file mode 100644 index 00000000..6ffd1f88 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -0,0 +1,177 @@ +using Dalamud.Interface; +using FFXIVClientStructs.Interop; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed unsafe class MaterialTemplatePickers : IUiService +{ + private const float MaximumTextureSize = 64.0f; + + private readonly TextureArraySlicer _textureArraySlicer; + private readonly CharacterUtility _characterUtility; + + public readonly IEditor TileIndexPicker; + public readonly IEditor SphereMapIndexPicker; + + public MaterialTemplatePickers(TextureArraySlicer textureArraySlicer, CharacterUtility characterUtility) + { + _textureArraySlicer = textureArraySlicer; + _characterUtility = characterUtility; + + TileIndexPicker = new Editor(DrawTileIndexPicker).AsByteEditor(); + SphereMapIndexPicker = new Editor(DrawSphereMapIndexPicker).AsByteEditor(); + } + + public bool DrawTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->TileOrbArrayTexResource, + _characterUtility.Address->TileNormArrayTexResource, + ]); + + public bool DrawSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->SphereDArrayTexResource, + ]); + + public bool DrawTextureArrayIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact, ReadOnlySpan> textureRHs) + { + TextureResourceHandle* firstNonNullTextureRH = null; + foreach (var texture in textureRHs) + { + if (texture.Value != null && texture.Value->CsHandle.Texture != null) + { + firstNonNullTextureRH = texture; + break; + } + } + var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null; + + var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; + var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0; + + var ret = false; + + var framePadding = ImGui.GetStyle().FramePadding; + var itemSpacing = ImGui.GetStyle().ItemSpacing; + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + { + var spaceSize = ImUtf8.CalcTextSize(" "u8).X; + var spaces = (int)((ImGui.CalcItemWidth() - framePadding.X * 2.0f - (compact ? 0.0f : (textureSize.X + itemSpacing.X) * textureRHs.Length)) / spaceSize); + using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, framePadding + new Vector2(0.0f, Math.Max(textureSize.Y - ImGui.GetFrameHeight() + itemSpacing.Y, 0.0f) * 0.5f), !compact); + using var combo = ImUtf8.Combo(label, (value == ushort.MaxValue ? "-" : value.ToString()).PadLeft(spaces), ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge); + if (combo.Success && firstNonNullTextureRH != null) + { + var lineHeight = Math.Max(ImGui.GetTextLineHeightWithSpacing(), framePadding.Y * 2.0f + textureSize.Y); + var itemWidth = Math.Max(ImGui.GetContentRegionAvail().X, ImUtf8.CalcTextSize("MMM"u8).X + (itemSpacing.X + textureSize.X) * textureRHs.Length + framePadding.X * 2.0f); + using var center = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); + using var clipper = ImUtf8.ListClipper(count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd && i < count; i++) + { + if (ImUtf8.Selectable($"{i,3}", i == value, size: new(itemWidth, lineHeight))) + { + ret = value != i; + value = (ushort)i; + } + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var textureRegionStart = new Vector2( + rectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), + rectMin.Y + framePadding.Y); + var maxSize = new Vector2(textureSize.X, rectMax.Y - framePadding.Y - textureRegionStart.Y); + DrawTextureSlices(textureRegionStart, maxSize, itemSpacing.X, textureRHs, (byte)i); + } + } + } + } + if (!compact && value != ushort.MaxValue) + { + var cbRectMin = ImGui.GetItemRectMin(); + var cbRectMax = ImGui.GetItemRectMax(); + var cbTextureRegionStart = new Vector2(cbRectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), cbRectMin.Y + framePadding.Y); + var cbMaxSize = new Vector2(textureSize.X, cbRectMax.Y - framePadding.Y - cbTextureRegionStart.Y); + DrawTextureSlices(cbTextureRegionStart, cbMaxSize, itemSpacing.X, textureRHs, (byte)value); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && (description.Length > 0 || compact && value != ushort.MaxValue)) + { + using var disabled = ImRaii.Enabled(); + using var tt = ImUtf8.Tooltip(); + if (description.Length > 0) + ImUtf8.Text(description); + if (compact && value != ushort.MaxValue) + { + ImGui.Dummy(new Vector2(textureSize.X * textureRHs.Length + itemSpacing.X * (textureRHs.Length - 1), textureSize.Y)); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + DrawTextureSlices(rectMin, textureSize, itemSpacing.X, textureRHs, (byte)value); + } + } + + return ret; + } + + public void DrawTextureSlices(Vector2 regionStart, Vector2 itemSize, float itemSpacing, ReadOnlySpan> textureRHs, byte sliceIndex) + { + for (var j = 0; j < textureRHs.Length; ++j) + { + if (textureRHs[j].Value == null) + continue; + var texture = textureRHs[j].Value->CsHandle.Texture; + if (texture == null) + continue; + var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); + if (handle == 0) + continue; + + var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; + var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + position += (itemSize - size) * 0.5f; + ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, + new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + } + } + + private delegate bool DrawEditor(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact); + + private sealed class Editor(DrawEditor draw) : IEditor + { + public bool Draw(Span values, bool disabled) + { + var helper = Editors.PrepareMultiComponent(values.Length); + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + helper.SetupComponent(valueIdx); + + var value = ushort.CreateSaturating(MathF.Round(values[valueIdx])); + if (disabled) + { + using var _ = ImRaii.Disabled(); + draw(helper.Id, default, ref value, true); + } + else + { + if (draw(helper.Id, default, ref value, true)) + { + values[valueIdx] = value; + ret = true; + } + } + } + + return ret; + } + } +}