diff --git a/src/core/MaterialManager.cpp b/src/core/MaterialManager.cpp index e3fcb10..d02957a 100644 --- a/src/core/MaterialManager.cpp +++ b/src/core/MaterialManager.cpp @@ -138,7 +138,7 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material if (!materialPrim.IsValid()) return snapshot; static const pxr::TfToken kUiPositionKey("uiPosition"); - static const pxr::TfToken kUiShowAllPinsKey("uiShowAllPins"); + static const pxr::TfToken kUiPinDisplayModeKey("uiPinDisplayMode"); // Seed with every shader under the material, recursing into nested node // graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph @@ -180,9 +180,12 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material node.hasAuthoredPosition = true; } - pxr::VtValue showAllValue = child.GetCustomDataByKey(kUiShowAllPinsKey); - if (showAllValue.IsHolding()) - node.showAllPins = showAllValue.UncheckedGet(); + pxr::VtValue pinModeValue = child.GetCustomDataByKey(kUiPinDisplayModeKey); + if (pinModeValue.IsHolding()) { + int raw = pinModeValue.UncheckedGet(); + if (raw >= static_cast(PinDisplayMode::All) && raw <= static_cast(PinDisplayMode::Collapsed)) + node.pinDisplayMode = static_cast(raw); + } // Prefer the full Sdr-defined pin set (so unauthored pins can still be // dragged to create a connection); fall back to authored attributes diff --git a/src/core/MaterialManager.h b/src/core/MaterialManager.h index 8bf6af5..34d4368 100644 --- a/src/core/MaterialManager.h +++ b/src/core/MaterialManager.h @@ -33,6 +33,17 @@ struct ShaderPinInfo { pxr::SdfValueTypeName typeName; }; +/// Hypershade-style pin display mode for a node in the graph canvas. +/// All: every Sdr-defined pin shown (today's default). Connected: only +/// linked pins shown, the rest collapsed behind a single connectable nub per +/// side. Collapsed: every pin — linked or not — collapses behind that one +/// nub per side, so existing connections visually converge on it too. +enum class PinDisplayMode { + All = 0, + Connected = 1, + Collapsed = 2, +}; + /// One UsdShadeShader prim read back from an existing material network. /// inputs/outputs list the full Sdr-defined pin set (not just authored /// attributes) so unauthored pins can still be dragged to create a connection. @@ -48,12 +59,9 @@ struct ShaderGraphNode { /// False when no uiPosition custom data is authored (typical for networks /// referenced from .mtlx) — the editor auto-lays such nodes out instead. bool hasAuthoredPosition = false; - /// Hypershade-style pin display mode, read from customData when the - /// "keep graph node view settings in USD" preference authored it; true - /// (show every pin) when unauthored. False hides unconnected pins, - /// collapsing them into a single connectable "more pins" nub per side - /// (see MaterialEditorPanel) until a new connection reveals one by name. - bool showAllPins = true; + /// Pin display mode, read from customData when the "keep graph node view + /// settings in USD" preference authored it; All when unauthored. + PinDisplayMode pinDisplayMode = PinDisplayMode::All; std::vector inputs; std::vector outputs; }; diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index dae414d..ae04cd2 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -1319,7 +1319,7 @@ void MaterialEditorPanel::SeedPinDisplayOverrides() { const std::string key = node.path.GetString(); if (m_pinDisplayOverrides.find(key) != m_pinDisplayOverrides.end()) continue; - m_pinDisplayOverrides.emplace(key, PinDisplayOverride{node.showAllPins}); + m_pinDisplayOverrides.emplace(key, PinDisplayOverride{node.pinDisplayMode}); } } @@ -1432,15 +1432,21 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { // within the widest row measured across the whole node. PinDisplayOverride& pinDisplay = GetPinDisplayOverride(node.path); - // Connected-only mode collapses every unconnected pin on a side into - // one connectable "more pins" nub on the title row (Maya Hypershade - // style) — present only while that side actually has something hidden. + // Connected mode collapses every unconnected pin on a side into one + // connectable "more pins" nub (Maya Hypershade style), present only + // while that side actually has something hidden. Collapsed mode + // collapses every pin on a side — linked or not — into that nub + // whenever the side has any pins at all (existing links redirect to + // it too; see EffectivePinId()). bool hasHiddenInput = false, hasHiddenOutput = false; - if (!pinDisplay.showAllPins) { + if (pinDisplay.mode == PinDisplayMode::Connected) { for (const auto& input : node.inputs) if (!IsPinLinked(node.path, input.name, false)) { hasHiddenInput = true; break; } for (const auto& output : node.outputs) if (!IsPinLinked(node.path, output.name, true)) { hasHiddenOutput = true; break; } + } else if (pinDisplay.mode == PinDisplayMode::Collapsed) { + hasHiddenInput = !node.inputs.empty(); + hasHiddenOutput = !node.outputs.empty(); } const float rowSpacing = ImGui::GetStyle().ItemSpacing.x; @@ -1458,35 +1464,8 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { ImGui::PushID(node.path.GetText()); ImVec2 headerTop = ImGui::GetCursorScreenPos(); - if (hasHiddenInput) { - // Collapsed input nub: dropping a connection here (or dragging - // one out of it) opens RenderPinPickMenu() to choose which - // hidden input it actually wires to. - uintptr_t metaInId = HashId(node.path.GetString() + ":metaIn"); - m_pinIdToInfo[metaInId] = PinInfo{node.path, "", false, pxr::SdfValueTypeName(), true}; - NE::BeginPin(NE::PinId(metaInId), NE::PinKind::Input); - NE::PinPivotAlignment(ImVec2(0.0f, 0.5f)); - NE::PinPivotSize(ImVec2(0.0f, 0.0f)); - ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255)); - NE::EndPin(); - if (ImGui::IsItemHovered()) pendingTooltip = "Connect to a hidden input"; - ImGui::SameLine(); - } - ImGui::TextUnformatted(title.c_str()); - if (hasHiddenOutput) { - ImGui::SameLine(); - uintptr_t metaOutId = HashId(node.path.GetString() + ":metaOut"); - m_pinIdToInfo[metaOutId] = PinInfo{node.path, "", true, pxr::SdfValueTypeName(), true}; - NE::BeginPin(NE::PinId(metaOutId), NE::PinKind::Output); - NE::PinPivotAlignment(ImVec2(1.0f, 0.5f)); - NE::PinPivotSize(ImVec2(0.0f, 0.0f)); - ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255)); - NE::EndPin(); - if (ImGui::IsItemHovered()) pendingTooltip = "Connect a hidden output"; - } - // Mode toggle, drawn and hit-tested manually. ImGui's own hover // attribution is unreliable for widgets inside the canvas (the same // mis-attribution the vendored editor's FindPinAt()/m_PressedNode @@ -1499,12 +1478,18 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { // captured before NE::Begin) gates out clicks landing on other panels // or popups, since the canvas-space transform is unclamped. { - const char* label = pinDisplay.showAllPins ? "All" : "Conn"; const ImGuiStyle& style = ImGui::GetStyle(); - const ImVec2 labelSize = ImGui::CalcTextSize(label); + // 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); const ImVec2 btnMin = ImGui::GetCursorScreenPos(); - const ImVec2 btnMax(btnMin.x + labelSize.x + style.FramePadding.x * 2.0f, - btnMin.y + labelSize.y); + const ImVec2 btnMax(btnMin.x + iconSize.x + style.FramePadding.x * 2.0f, + btnMin.y + iconSize.y + style.FramePadding.y * 2.0f); ImGui::Dummy(ImVec2(btnMax.x - btnMin.x, btnMax.y - btnMin.y)); // IsMouseHoveringRect is pure geometry (no hover-id arbitration) @@ -1516,15 +1501,28 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { : ImGuiCol_Button); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(btnMin, btnMax, frameColor, style.FrameRounding); - dl->AddText(ImVec2(btnMin.x + style.FramePadding.x, btnMin.y), - ImGui::GetColorU32(ImGuiCol_Text), label); + 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; + } + + static const char* kTooltips[3] = { + "Showing all pins - click to show connected pins only", + "Showing connected pins only - click to collapse into one pin", + "Collapsed to one pin per side - click to show all pins", + }; if (btnHovered) - pendingTooltip = pinDisplay.showAllPins - ? "Showing all pins - click to show connected pins only" - : "Showing connected pins only - click to show all pins"; + pendingTooltip = kTooltips[static_cast(pinDisplay.mode)]; if (btnHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - TogglePinDisplayMode(node.path); + CyclePinDisplayMode(node.path); } ImVec2 headerBottom = ImGui::GetItemRectMax(); @@ -1581,6 +1579,38 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, linked, ImColor(GetPinColor(output.typeName))); NE::EndPin(); } + + // Collapsed connector nubs on the node's bottom row (Connected or + // Collapsed mode): input nub at the left edge, output nub hugging the + // right edge. Dropping a connection on (or dragging one out of) a + // nub opens RenderPinPickMenu() to choose which pin it wires to. + if (hasHiddenInput || hasHiddenOutput) { + ImGui::Dummy(ImVec2(0.0f, 2.0f)); + const float nubRowStartX = ImGui::GetCursorPosX(); + if (hasHiddenInput) { + uintptr_t metaInId = HashId(node.path.GetString() + ":metaIn"); + m_pinIdToInfo[metaInId] = PinInfo{node.path, "", false, pxr::SdfValueTypeName(), true}; + NE::BeginPin(NE::PinId(metaInId), NE::PinKind::Input); + NE::PinPivotAlignment(ImVec2(0.0f, 0.5f)); + NE::PinPivotSize(ImVec2(0.0f, 0.0f)); + ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255)); + NE::EndPin(); + if (ImGui::IsItemHovered()) pendingTooltip = "Connect to a hidden input"; + } + if (hasHiddenOutput) { + if (hasHiddenInput) ImGui::SameLine(); + ImGui::SetCursorPosX(nubRowStartX + contentWidth - pinIconSize.x); + uintptr_t metaOutId = HashId(node.path.GetString() + ":metaOut"); + m_pinIdToInfo[metaOutId] = PinInfo{node.path, "", true, pxr::SdfValueTypeName(), true}; + NE::BeginPin(NE::PinId(metaOutId), NE::PinKind::Output); + NE::PinPivotAlignment(ImVec2(1.0f, 0.5f)); + NE::PinPivotSize(ImVec2(0.0f, 0.0f)); + ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, false, ImColor(180, 180, 180, 255)); + NE::EndPin(); + if (ImGui::IsItemHovered()) pendingTooltip = "Connect a hidden output"; + } + } + ImGui::PopID(); NE::EndNode(); @@ -1601,8 +1631,9 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { for (const auto& link : m_graph.links) { uintptr_t linkIdValue = HashId(link.destNode.GetString() + ":" + link.destInput); + // The pin's own id, always populated in m_pinIdToInfo regardless of + // visibility (used here purely to color the link by the real type). uintptr_t startPinId = HashId(link.sourceNode.GetString() + ":out:" + link.sourceOutput); - uintptr_t endPinId = HashId(link.destNode.GetString() + ":in:" + link.destInput); m_linkIdToInfo[linkIdValue] = LinkInfo{link.destNode, link.destInput}; ImU32 linkColor = IM_COL32(200, 200, 200, 255); @@ -1610,7 +1641,13 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { if (pinIt != m_pinIdToInfo.end()) linkColor = GetPinColor(pinIt->second.typeName); - NE::Link(NE::LinkId(linkIdValue), NE::PinId(startPinId), NE::PinId(endPinId), ImColor(linkColor)); + // The id actually referenced by the drawn link: redirected to a + // node's collapsed meta nub when that node is in Collapsed mode + // (the real pin isn't drawn there, so its id can't be referenced). + uintptr_t linkStartPinId = EffectivePinId(link.sourceNode, link.sourceOutput, true); + uintptr_t linkEndPinId = EffectivePinId(link.destNode, link.destInput, false); + + NE::Link(NE::LinkId(linkIdValue), NE::PinId(linkStartPinId), NE::PinId(linkEndPinId), ImColor(linkColor)); } HandleCreateAndDelete(); @@ -1915,17 +1952,32 @@ MaterialEditorPanel::PinDisplayOverride& MaterialEditorPanel::GetPinDisplayOverr bool MaterialEditorPanel::IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const { auto it = m_pinDisplayOverrides.find(node.path.GetString()); - if (it == m_pinDisplayOverrides.end() || it->second.showAllPins) - return true; - return IsPinLinked(node.path, pinName, isOutput); + PinDisplayMode mode = (it != m_pinDisplayOverrides.end()) ? it->second.mode : PinDisplayMode::All; + switch (mode) { + case PinDisplayMode::All: return true; + case PinDisplayMode::Connected: return IsPinLinked(node.path, pinName, isOutput); + case PinDisplayMode::Collapsed: return false; + } + return true; } -void MaterialEditorPanel::TogglePinDisplayMode(const pxr::SdfPath& nodePath) { +void MaterialEditorPanel::CyclePinDisplayMode(const pxr::SdfPath& nodePath) { PinDisplayOverride& ov = GetPinDisplayOverride(nodePath); - ov.showAllPins = !ov.showAllPins; + switch (ov.mode) { + case PinDisplayMode::All: ov.mode = PinDisplayMode::Connected; break; + case PinDisplayMode::Connected: ov.mode = PinDisplayMode::Collapsed; break; + case PinDisplayMode::Collapsed: ov.mode = PinDisplayMode::All; break; + } PersistPinDisplayState(nodePath); } +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) + return HashId(nodePath.GetString() + (isOutput ? ":metaOut" : ":metaIn")); + return HashId(nodePath.GetString() + (isOutput ? ":out:" : ":in:") + pinName); +} + void MaterialEditorPanel::RenderPinPickMenu() { const ShaderGraphNode* node = nullptr; for (const auto& n : m_graph.nodes) @@ -2230,26 +2282,26 @@ void MaterialEditorPanel::PersistPinDisplayState(const pxr::SdfPath& nodePath) { pxr::UsdPrim prim = m_stage->GetPrimAtPath(nodePath); if (!prim.IsValid()) return; - static const pxr::TfToken kShowAllKey("uiShowAllPins"); + static const pxr::TfToken kPinModeKey("uiPinDisplayMode"); - const bool newShowAll = GetPinDisplayOverride(nodePath).showAllPins; + const int newMode = static_cast(GetPinDisplayOverride(nodePath).mode); - pxr::VtValue oldShowAllVt = prim.GetCustomDataByKey(kShowAllKey); - const bool oldShowAll = oldShowAllVt.IsHolding() ? oldShowAllVt.UncheckedGet() : true; + pxr::VtValue oldModeVt = prim.GetCustomDataByKey(kPinModeKey); + const int oldMode = oldModeVt.IsHolding() ? oldModeVt.UncheckedGet() : static_cast(PinDisplayMode::All); - if (oldShowAll == newShowAll) + if (oldMode == newMode) return; pxr::UsdStageRefPtr stage = m_stage; m_commandHistory->Push(std::make_unique( "Change pin display for " + nodePath.GetName(), - [stage, nodePath, newShowAll]() { + [stage, nodePath, newMode]() { pxr::UsdPrim p = stage->GetPrimAtPath(nodePath); - if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiShowAllPins"), pxr::VtValue(newShowAll)); + if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiPinDisplayMode"), pxr::VtValue(newMode)); }, - [stage, nodePath, oldShowAll]() { + [stage, nodePath, oldMode]() { pxr::UsdPrim p = stage->GetPrimAtPath(nodePath); - if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiShowAllPins"), pxr::VtValue(oldShowAll)); + if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiPinDisplayMode"), pxr::VtValue(oldMode)); })); } diff --git a/src/ui/MaterialEditorPanel.h b/src/ui/MaterialEditorPanel.h index 1913e3a..9ec94a2 100644 --- a/src/ui/MaterialEditorPanel.h +++ b/src/ui/MaterialEditorPanel.h @@ -87,7 +87,7 @@ private: /// customData when m_keepGraphNodeViewSettingsInUsd is set. See /// GetPinDisplayOverride()/IsPinVisible(). struct PinDisplayOverride { - bool showAllPins = true; + PinDisplayMode mode = PinDisplayMode::All; }; /// A connection dragged onto/from a collapsed meta pin, awaiting the user /// picking which actual pin on nodePath/isOutput to wire up. otherPin is @@ -154,22 +154,30 @@ private: /// Seeds m_pinDisplayOverrides for any node path not already present /// (i.e. not touched yet this session) from the just-synced m_graph's - /// showAllPins (customData-backed). Never overwrites an existing entry, - /// so in-session toggles survive resyncs. + /// pinDisplayMode (customData-backed). Never overwrites an existing + /// entry, so in-session toggles survive resyncs. void SeedPinDisplayOverrides(); - /// Creates the entry (defaulted to "show all") on first access. + /// Creates the entry (defaulted to All) on first access. PinDisplayOverride& GetPinDisplayOverride(const pxr::SdfPath& nodePath); - /// True if pin should be drawn this frame: node is in "show all" mode, or - /// the pin is linked (linked pins are never hidden — Link() unconditionally - /// references pin IDs that must have been drawn). A hidden pin becomes - /// visible the moment it's connected, via RenderPinPickMenu(). + /// True if pin should be drawn this frame: All mode shows everything; + /// Connected mode shows only linked pins (linked pins are never hidden — + /// Link() unconditionally references pin IDs that must have been drawn — + /// so a hidden pin becomes visible the moment it's connected, via + /// RenderPinPickMenu()); Collapsed mode hides every real pin, linked or + /// not, in favor of the single connector nub per side (see + /// EffectivePinId(), which redirects existing links to that nub). bool IsPinVisible(const ShaderGraphNode& node, const std::string& pinName, bool isOutput) const; - /// Flips a node between "show all" and "show connected only". - void TogglePinDisplayMode(const pxr::SdfPath& nodePath); + /// Cycles a node's display mode: All -> Connected -> Collapsed -> All. + void CyclePinDisplayMode(const pxr::SdfPath& nodePath); /// Authors the node's current PinDisplayOverride into USD customData via /// an undoable command, mirroring PersistNodePosition. No-op unless /// m_keepGraphNodeViewSettingsInUsd is set. void PersistPinDisplayState(const pxr::SdfPath& nodePath); + /// PinId to use when drawing a link touching (nodePath, pinName, + /// isOutput): the pin's own id normally, or that node's collapsed meta + /// nub id when the node is in Collapsed mode (so existing links visually + /// converge on the nub instead of referencing an undrawn real pin). + uintptr_t EffectivePinId(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const; /// Renders the deferred "choose pin" popup for m_pendingPinPick (Maya /// Hypershade style): a connection was dragged onto/from a collapsed meta /// pin, and the user now picks which actual attribute it wires to. Must