using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Component.GUI; using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags; using static Dalamud.Bindings.ImGui.ImGuiTableFlags; using static Dalamud.Bindings.ImGui.ImGuiTreeNodeFlags; using static Dalamud.Interface.ColorHelpers; using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui; using static Dalamud.Utility.Util; using static FFXIVClientStructs.FFXIV.Component.GUI.TextureType; namespace Dalamud.Interface.Internal.UiDebug2.Browsing; /// /// A tree for an that can be printed and browsed via ImGui. /// internal unsafe partial class ImageNodeTree : ResNodeTree { /// /// Initializes a new instance of the class. /// /// The node to create a tree for. /// The tree representing the containing addon. internal ImageNodeTree(AtkResNode* node, AddonTree addonTree) : base(node, addonTree) { } /// /// Gets the part ID that this node uses. /// private protected virtual uint PartId => this.ImgNode->PartId; /// /// Gets the parts list that this node uses. /// private protected virtual AtkUldPartsList* PartsList => this.ImgNode->PartsList; /// /// Gets or sets a summary of pertinent data about this 's texture. Updated each time is called. /// private protected TextureData TexData { get; set; } private AtkImageNode* ImgNode => (AtkImageNode*)this.Node; /// /// Draws the texture inside the window, in either of two styles.

/// Full Image (0)presents the texture in full as a spritesheet.
/// Parts List (1)presents the individual parts as rows in a table. ///
private protected void DrawTextureAndParts() { this.TexData = new TextureData(this.PartsList, this.PartId); if (this.TexData.Texture == null) { return; } using var tree = ImRaii.TreeNode($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", SpanFullWidth); if (tree.Success) { PrintFieldValuePairs( ("Texture Type", $"{this.TexData.TexType}"), ("Part ID", $"{this.TexData.PartId}"), ("Part Count", $"{this.TexData.PartCount}")); if (this.TexData.Path != null) { PrintFieldValuePairs(("Texture Path", this.TexData.Path)); } if (ImGui.RadioButton("Full Image##textureDisplayStyle0"u8, TexDisplayStyle == 0)) { TexDisplayStyle = 0; } ImGui.SameLine(); if (ImGui.RadioButton("Parts List##textureDisplayStyle1"u8, TexDisplayStyle == 1)) { TexDisplayStyle = 1; } ImGui.NewLine(); if (TexDisplayStyle == 1) { this.PrintPartsTable(); } else { this.DrawFullTexture(); } } } /// /// Draws an outline of a given part within the texture. /// /// The part ID. /// The absolute position of the cursor onscreen. /// The relative position of the cursor within the window. /// The color of the outline. /// Whether this outline requires the user to mouse over it. private protected virtual void DrawPartOutline(uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false) { var part = this.TexData.PartsList->Parts[partId]; var hrFactor = this.TexData.HiRes ? 2f : 1f; var uv = new Vector2(part.U, part.V) * hrFactor; var wh = new Vector2(part.Width, part.Height) * hrFactor; var partBegin = cursorScreenPos + uv; var partEnd = partBegin + wh; if (reqHover && !ImGui.IsMouseHoveringRect(partBegin, partEnd)) { return; } var savePos = ImGui.GetCursorPos(); ImGui.GetWindowDrawList().AddRect(partBegin, partEnd, RgbaVector4ToUint(col)); ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20)); ImGuiHelpers.SafeTextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}"); ImGui.SetCursorPos(savePos); } /// private protected override void PrintNodeObject() => ShowStruct(this.ImgNode); /// private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) { PrintFieldValuePairs( ("Wrap", $"{this.ImgNode->WrapMode}"), ("Image Flags", $"0x{this.ImgNode->Flags:X}")); this.DrawTextureAndParts(); } private static void PrintPartCoords(float u, float v, float w, float h, bool asFloat = false, bool lineBreak = false) { ImGui.TextDisabled($"{u}, {v},{(lineBreak ? "\n" : " ")}{w}, {h}"); if (ImGui.IsItemHovered()) { ImGui.SetTooltip("Click to copy as Vector2\nShift-click to copy as Vector4"u8); } var suffix = asFloat ? "f" : string.Empty; if (ImGui.IsItemClicked()) { ImGui.SetClipboardText( ImGui.IsKeyDown(ImGuiKey.ModShift) ? $"new Vector4({u}{suffix}, {v}{suffix}, {w}{suffix}, {h}{suffix})" : $"new Vector2({u}{suffix}, {v}{suffix});\nnew Vector2({w}{suffix}, {h}{suffix})"); } } private void DrawFullTexture() { var cursorScreenPos = ImGui.GetCursorScreenPos(); var cursorLocalPos = ImGui.GetCursorPos(); ImGui.Image(new(this.TexData.Texture->D3D11ShaderResourceView), new(this.TexData.Texture->ActualWidth, this.TexData.Texture->ActualHeight)); for (uint p = 0; p < this.TexData.PartsList->PartCount; p++) { if (p == this.TexData.PartId) { continue; } this.DrawPartOutline(p, cursorScreenPos, cursorLocalPos, new(0.6f, 0.6f, 0.6f, 1), true); } this.DrawPartOutline(this.TexData.PartId, cursorScreenPos, cursorLocalPos, new(0, 0.85F, 1, 1)); } private void PrintPartsTable() { using var tbl = ImRaii.Table($"partsTable##{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", 3, Borders | RowBg | Reorderable); if (tbl.Success) { ImGui.TableSetupColumn("Part ID"u8, WidthFixed); ImGui.TableSetupColumn("Part Texture"u8, WidthFixed); ImGui.TableSetupColumn("Coordinates"u8, WidthFixed); ImGui.TableHeadersRow(); var tWidth = this.TexData.Texture->ActualWidth; var tHeight = this.TexData.Texture->ActualHeight; var textureSize = new Vector2(tWidth, tHeight); for (ushort i = 0; i < this.TexData.PartCount; i++) { ImGui.TableNextColumn(); var col = i == this.TexData.PartId ? new Vector4(0, 0.85F, 1, 1) : new(1); ImGuiHelpers.SafeTextColored(col, $"#{i.ToString().PadLeft(this.TexData.PartCount.ToString().Length, '0')}"); ImGui.TableNextColumn(); var part = this.TexData.PartsList->Parts[i]; var hiRes = this.TexData.HiRes; var u = hiRes ? part.U * 2f : part.U; var v = hiRes ? part.V * 2f : part.V; var width = hiRes ? part.Width * 2f : part.Width; var height = hiRes ? part.Height * 2f : part.Height; ImGui.Image( new(this.TexData.Texture->D3D11ShaderResourceView), new(width, height), new Vector2(u, v) / textureSize, new Vector2(u + width, v + height) / textureSize); ImGui.TableNextColumn(); ImGuiHelpers.SafeTextColored(!hiRes ? new Vector4(1) : new(0.6f, 0.6f, 0.6f, 1), "Standard:\t"); ImGui.SameLine(); var cursX = ImGui.GetCursorPosX(); PrintPartCoords(u / 2f, v / 2f, width / 2f, height / 2f); ImGuiHelpers.SafeTextColored(hiRes ? new Vector4(1) : new(0.6f, 0.6f, 0.6f, 1), "Hi-Res:\t"); ImGui.SameLine(); ImGui.SetCursorPosX(cursX); PrintPartCoords(u, v, width, height); ImGui.TextUnformatted("UV:\t"u8); ImGui.SameLine(); ImGui.SetCursorPosX(cursX); PrintPartCoords(u / tWidth, v / tWidth, (u + width) / tWidth, (v + height) / tHeight, true, true); } } } /// /// A summary of pertinent data about a node's texture. /// protected struct TextureData { /// The texture's partslist. public AtkUldPartsList* PartsList; /// The number of parts in the texture. public uint PartCount; /// The part ID the node is using. public uint PartId; /// The texture itself. public Texture* Texture = null; /// The type of texture. public TextureType TexType = 0; /// The texture's file path (if , otherwise this value is null). public string? Path = null; /// Whether this is a high-resolution texture. public bool HiRes = false; /// /// Initializes a new instance of the struct. /// /// The texture's parts list. /// The part ID being used by the node. public TextureData(AtkUldPartsList* partsList, uint partId) { this.PartsList = partsList; this.PartCount = this.PartsList->PartCount; this.PartId = partId >= this.PartCount ? 0 : partId; if (this.PartsList == null) { return; } var asset = this.PartsList->Parts[this.PartId].UldAsset; if (asset == null) { return; } this.TexType = asset->AtkTexture.TextureType; if (this.TexType == Resource) { var resource = asset->AtkTexture.Resource; this.Texture = resource->KernelTextureObject; this.Path = Marshal.PtrToStringAnsi(new(resource->TexFileResourceHandle->ResourceHandle.FileName.BufferPtr)); } else { this.Texture = this.TexType == KernelTexture ? asset->AtkTexture.KernelTexture : null; this.Path = null; } this.HiRes = this.Path?.Contains("_hr1") ?? false; } } }