From a877e8973cd030b6449692996649d1fc3b2dde1b Mon Sep 17 00:00:00 2001 From: indigo Date: Sat, 11 Jul 2026 11:07:57 +0800 Subject: [PATCH] Add thumbnail toggle and set-all pin-display toolbar icons Material editor toolbar gets an eye icon to hide/show node thumbnails (skips the reserved space and decode/render work, not just the visual) and a 3-dot icon that broadcasts a pin-display mode (All / Connected / Collapsed) to every node in the open material at once. The dot icon is shared with the per-node toggle via a parameterized DrawPinDisplayModeIcon, with dot radius/spacing scaled to fit the toolbar's fixed icon-button size instead of overflowing it. Co-Authored-By: Claude Sonnet 5 --- src/ui/MaterialEditorPanel.cpp | 110 ++++++++++++++++++++++++++------- src/ui/MaterialEditorPanel.h | 11 ++++ 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index ae04cd2..a85e5af 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -45,6 +45,29 @@ uintptr_t HashId(const std::string& s) { return static_cast(std::hash{}(s)); } +// Draws the 3-dot pin-display-mode icon (filled-dot count = 3 - mode, so it +// visually "funnels down" as pins collapse further), centered within +// [min, max]. Shared by the per-node toggle in RenderNodeGraphCanvas (which +// sizes its button around the default dot geometry) and the toolbar's +// "set all nodes" button in RenderToolbar (which instead fits the dots into +// its fixed, already-square icon-button size via smaller dotRadius/dotSpacing). +void DrawPinDisplayModeIcon(ImDrawList* dl, const ImVec2& min, const ImVec2& max, + PinDisplayMode mode, ImU32 filledColor, ImU32 hollowColor, + float dotRadius = 3.0f, float dotSpacing = 9.0f) { + const int dotCount = 3; + const int filledDots = 3 - static_cast(mode); + const float iconWidth = dotSpacing * (dotCount - 1) + dotRadius * 2.0f; + ImVec2 dotCenter(min.x + (max.x - min.x - iconWidth) * 0.5f + dotRadius, + min.y + (max.y - min.y) * 0.5f); + for (int i = 0; i < dotCount; ++i) { + if (i < filledDots) + dl->AddCircleFilled(dotCenter, dotRadius, filledColor); + else + dl->AddCircle(dotCenter, dotRadius, hollowColor, 0, 1.2f); + dotCenter.x += dotSpacing; + } +} + // Mirrors the file-local sanitizers in Application.cpp/SceneHierarchyPanel.cpp. std::string SanitizeUsdName(const std::string& raw) { std::string result; @@ -788,6 +811,50 @@ void MaterialEditorPanel::RenderToolbar() { ImGui::EndPopup(); } + ImGui::SameLine(); + if (iconBtn("##toggleThumbs", m_showNodeThumbnails ? Icon::Eye : Icon::EyeSlash, + m_showNodeThumbnails ? "Hide Thumbnails" : "Show Thumbnails", + m_showNodeThumbnails ? "Hide node thumbnails" : "Show node thumbnails")) + m_showNodeThumbnails = !m_showNodeThumbnails; + + // "Set all nodes" pin-display broadcast: same 3-dot icon language as the + // per-node toggle (DrawPinDisplayModeIcon), but hit-tested the normal + // ImGui way — this button lives in the toolbar window, not inside the + // node canvas, so none of RenderNodeGraphCanvas's geometric hit-test + // workarounds are needed here. + ImGui::SameLine(); + ImGui::BeginDisabled(m_materialPath.IsEmpty()); + { + ImGui::PushID("##allPinMode"); + const ImVec2 pos = ImGui::GetCursorScreenPos(); + const ImVec2 posMax(pos.x + btnH, pos.y + btnH); + const bool clicked = ImGui::InvisibleButton("##btn", ImVec2(btnH, btnH)); + const bool hovered = ImGui::IsItemHovered(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImU32 bg = ImGui::GetColorU32( + hovered ? (ImGui::IsItemActive() ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered) + : ImGuiCol_Button); + dl->AddRectFilled(pos, posMax, bg, ImGui::GetStyle().FrameRounding); + // Dot geometry scaled to btnH (unlike the per-node toggle's fixed + // size) so the icon fits inside this fixed, already-square button + // instead of overflowing it. + DrawPinDisplayModeIcon(dl, pos, posMax, m_toolbarPinDisplayMode, + ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled), + btnH * 0.09f, btnH * 0.30f); + static const char* kTips[3] = { + "All nodes: showing all pins - click to show connected pins only on every node", + "All nodes: showing connected pins only - click to collapse every node to one pin", + "All nodes: collapsed to one pin per side - click to show all pins on every node", + }; + if (hovered) ImGui::SetTooltip("%s", kTips[static_cast(m_toolbarPinDisplayMode)]); + if (clicked) { + m_toolbarPinDisplayMode = static_cast((static_cast(m_toolbarPinDisplayMode) + 1) % 3); + ApplyPinDisplayModeToAllNodes(m_toolbarPinDisplayMode); + } + ImGui::PopID(); + } + ImGui::EndDisabled(); + if (!m_targetPrimPath.IsEmpty()) { ImGui::SameLine(); ImGui::Text("Selected: %s", m_targetPrimPath.GetText()); @@ -1396,9 +1463,11 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { // Thumbnail below the title: the source image for Texture nodes, a // shader-ball render for Material nodes. thumb is 0 until ready; the // space is still reserved so the node doesn't jump when it appears. + // Toggled off entirely (no reserved space, no decode/render work) via + // the toolbar's thumbnail eye icon. ImTextureID thumb = 0; bool wantsThumb = false; - if (node.category == "Texture") { + if (m_showNodeThumbnails && node.category == "Texture") { // Any texture node that exposes a file/asset input reserves a // thumbnail slot. It stays black until a valid, on-disk image is // decoded, so an unset or unresolvable path reads as a black chip. @@ -1411,7 +1480,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { if (!file.empty()) thumb = m_thumbnails.GetTextureThumbnail(node.path, file); } - } else if (node.category == "Material") { + } else if (m_showNodeThumbnails && node.category == "Material") { wantsThumb = true; // Preview the terminal (token-typed) output as the surface; fall // back to the first output routed into diffuseColor. @@ -1479,14 +1548,11 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { // or popups, since the canvas-space transform is unclamped. { const ImGuiStyle& style = ImGui::GetStyle(); - // 3-dot icon: filled-dot count encodes how much is visible — - // All shows all 3 dots filled, Connected 2, Collapsed 1 — so the - // icon visually "funnels down" as the node collapses further. - const float dotRadius = 3.0f; - const float dotSpacing = 9.0f; - const int dotCount = 3; - const int filledDots = 3 - static_cast(pinDisplay.mode); - const ImVec2 iconSize(dotSpacing * (dotCount - 1) + dotRadius * 2.0f, dotRadius * 2.0f); + // Icon area sized to match the 3-dot icon's fixed geometry (see + // DrawPinDisplayModeIcon); filled-dot count visually "funnels + // down" as the node collapses further (All=3, Connected=2, + // Collapsed=1). + const ImVec2 iconSize(9.0f * 2.0f + 3.0f * 2.0f, 3.0f * 2.0f); const ImVec2 btnMin = ImGui::GetCursorScreenPos(); const ImVec2 btnMax(btnMin.x + iconSize.x + style.FramePadding.x * 2.0f, btnMin.y + iconSize.y + style.FramePadding.y * 2.0f); @@ -1501,18 +1567,8 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { : ImGuiCol_Button); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(btnMin, btnMax, frameColor, style.FrameRounding); - - const ImU32 dotColor = ImGui::GetColorU32(ImGuiCol_Text); - const ImU32 dotHollowColor = ImGui::GetColorU32(ImGuiCol_TextDisabled); - ImVec2 dotCenter(btnMin.x + style.FramePadding.x + dotRadius, - btnMin.y + style.FramePadding.y + dotRadius); - for (int i = 0; i < dotCount; ++i) { - if (i < filledDots) - dl->AddCircleFilled(dotCenter, dotRadius, dotColor); - else - dl->AddCircle(dotCenter, dotRadius, dotHollowColor, 0, 1.2f); - dotCenter.x += dotSpacing; - } + DrawPinDisplayModeIcon(dl, btnMin, btnMax, pinDisplay.mode, + ImGui::GetColorU32(ImGuiCol_Text), ImGui::GetColorU32(ImGuiCol_TextDisabled)); static const char* kTooltips[3] = { "Showing all pins - click to show connected pins only", @@ -1971,6 +2027,16 @@ void MaterialEditorPanel::CyclePinDisplayMode(const pxr::SdfPath& nodePath) { PersistPinDisplayState(nodePath); } +void MaterialEditorPanel::ApplyPinDisplayModeToAllNodes(PinDisplayMode mode) { + for (const auto& node : m_graph.nodes) { + PinDisplayOverride& ov = GetPinDisplayOverride(node.path); + if (ov.mode != mode) { + ov.mode = mode; + PersistPinDisplayState(node.path); + } + } +} + uintptr_t MaterialEditorPanel::EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const { auto it = m_pinDisplayOverrides.find(nodePath.GetString()); if (it != m_pinDisplayOverrides.end() && it->second.mode == PinDisplayMode::Collapsed) diff --git a/src/ui/MaterialEditorPanel.h b/src/ui/MaterialEditorPanel.h index 9ec94a2..c308c31 100644 --- a/src/ui/MaterialEditorPanel.h +++ b/src/ui/MaterialEditorPanel.h @@ -169,6 +169,8 @@ private: bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const; /// Cycles a node's display mode: All -> Connected -> Collapsed -> All. void CyclePinDisplayMode(const pxr::SdfPath& nodePath); + /// Sets every node currently in m_graph to mode (toolbar "set all" button). + void ApplyPinDisplayModeToAllNodes(PinDisplayMode mode); /// Authors the node's current PinDisplayOverride into USD customData via /// an undoable command, mirroring PersistNodePosition. No-op unless /// m_keepGraphNodeViewSettingsInUsd is set. @@ -249,6 +251,15 @@ private: bool m_openPinPickMenu = false; PendingPinPick m_pendingPinPick; + /// Toolbar toggle: whether node thumbnails (texture images / material + /// shader-ball renders) are shown at all. Off skips the reserved space + /// and the decode/render work entirely, not just the visual. + bool m_showNodeThumbnails = true; + /// Toolbar "set all nodes" pin-display button: the mode last broadcast + /// to every node (and the icon/tooltip shown), independent of what any + /// individual node is actually in after the user tweaks one by hand. + PinDisplayMode m_toolbarPinDisplayMode = PinDisplayMode::All; + /// Column widths of the browser (left) and preview (right) sections, /// user-adjustable via the splitters between them and the canvas. float m_browserWidth = 220.0f;