diff --git a/src/core/MaterialManager.cpp b/src/core/MaterialManager.cpp index 160b040..e3fcb10 100644 --- a/src/core/MaterialManager.cpp +++ b/src/core/MaterialManager.cpp @@ -138,6 +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"); // Seed with every shader under the material, recursing into nested node // graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph @@ -179,6 +180,10 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material node.hasAuthoredPosition = true; } + pxr::VtValue showAllValue = child.GetCustomDataByKey(kUiShowAllPinsKey); + if (showAllValue.IsHolding()) + node.showAllPins = showAllValue.UncheckedGet(); + // Prefer the full Sdr-defined pin set (so unauthored pins can still be // dragged to create a connection); fall back to authored attributes // only for shader types the registry doesn't know about. diff --git a/src/core/MaterialManager.h b/src/core/MaterialManager.h index 42d855a..8bf6af5 100644 --- a/src/core/MaterialManager.h +++ b/src/core/MaterialManager.h @@ -48,6 +48,12 @@ 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; std::vector inputs; std::vector outputs; }; diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index dd87dae..bbd7417 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -429,11 +429,15 @@ void Application::LoadPreferences() m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }(); else if (key == "MaterialPreviewWidth") m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }(); + else if (key == "KeepGraphNodeViewSettingsInUsd") + m_prefs.keepGraphNodeViewSettingsInUsd = (val == "1"); } - if (m_materialEditorPanel) + if (m_materialEditorPanel) { m_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth, m_prefs.materialPreviewWidth); + m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(m_prefs.keepGraphNodeViewSettingsInUsd); + } } void Application::SavePreferences() @@ -455,6 +459,7 @@ void Application::SavePreferences() f << "OcioLook=" << m_prefs.ocioLook << "\n"; f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n"; f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n"; + f << "KeepGraphNodeViewSettingsInUsd=" << (m_prefs.keepGraphNodeViewSettingsInUsd ? 1 : 0) << "\n"; } void Application::ApplyPrefsToAllViewports() @@ -505,6 +510,23 @@ void Application::RenderPreferencesDialog() ImGui::EndTabItem(); } + // ── Material ──────────────────────────────────────────────────────── + if (ImGui::BeginTabItem("Material")) { + ImGui::Spacing(); + if (ImGui::Checkbox("Keep graph node view settings in USD customData", + &m_prefs.keepGraphNodeViewSettingsInUsd)) { + if (m_materialEditorPanel) + m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd( + m_prefs.keepGraphNodeViewSettingsInUsd); + } + ImGui::TextWrapped( + "When enabled, each node's pin display mode (all pins vs. " + "connected only) and any manually revealed pins are saved into " + "the material file and restored when it's reopened. When " + "disabled, this is a session-only view setting."); + ImGui::EndTabItem(); + } + // ── Viewport ──────────────────────────────────────────────────────── if (ImGui::BeginTabItem("Viewport")) { ImGui::Spacing(); diff --git a/src/ui/Application.h b/src/ui/Application.h index 810f457..25ed9f0 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -30,6 +30,11 @@ struct AppPreferences { std::string ocioLook; float materialBrowserWidth = 220.0f; ///< Material Editor left-column width float materialPreviewWidth = 320.0f; ///< Material Editor right-column width + /// When true, per-node pin display mode (Hypershade-style "show all" vs + /// "show connected only") and revealed hidden pins are authored into USD + /// customData so they survive reopening the material; when false, that + /// state is session-only. + bool keepGraphNodeViewSettingsInUsd = false; }; class Application { diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index c4312b9..dae414d 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -306,7 +306,7 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) { MaterialEditorPanel::MaterialEditorPanel() { // Route the vendored editor's click-time hit-test dump into our log so // it interleaves with the [NodeGraph] event trace. Disabled: uncomment to - // re-enable the [ed] trace while diagnosing the drag regression. + // re-enable the [ed] trace when diagnosing canvas interaction issues. // g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); }; NE::Config config; @@ -1311,6 +1311,16 @@ void MaterialEditorPanel::SyncFromUsd() { } m_graph = m_materialManager->GetShaderGraph(m_materialPath); ApplyFallbackLayout(m_graph); + SeedPinDisplayOverrides(); +} + +void MaterialEditorPanel::SeedPinDisplayOverrides() { + for (const auto& node : m_graph.nodes) { + const std::string key = node.path.GetString(); + if (m_pinDisplayOverrides.find(key) != m_pinDisplayOverrides.end()) + continue; + m_pinDisplayOverrides.emplace(key, PinDisplayOverride{node.showAllPins}); + } } void MaterialEditorPanel::RenderNodeGraphCanvas() { @@ -1365,6 +1375,12 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { // One whole-graph revision hash drives every material thumbnail this frame. const size_t graphRevision = ComputeGraphRevision(m_stage, m_graph); + // Tooltip requested by a hovered in-canvas widget this frame. Shown after + // NE::End(): inside the canvas io.MousePos is in canvas space, so a + // tooltip opened there is positioned at canvas coordinates misread as + // screen coordinates — deferring restores correct placement at the cursor. + const char* pendingTooltip = nullptr; + for (const auto& node : m_graph.nodes) { uintptr_t nodeIdValue = HashId(node.path.GetString()); NE::NodeId nodeId(nodeIdValue); @@ -1414,18 +1430,103 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { // ImGui fork this project doesn't use) — approximate the classic // "outputs hug the right edge" look by right-aligning each output row // 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. + bool hasHiddenInput = false, hasHiddenOutput = false; + if (!pinDisplay.showAllPins) { + 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; } + } + const float rowSpacing = ImGui::GetStyle().ItemSpacing.x; float contentWidth = ImGui::CalcTextSize(title.c_str()).x; for (const auto& input : node.inputs) - contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(input.name.c_str()).x); + if (IsPinVisible(node, input.name, false)) + contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(input.name.c_str()).x); for (const auto& output : node.outputs) - contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(output.name.c_str()).x); + if (IsPinVisible(node, output.name, true)) + contentWidth = std::max(contentWidth, pinIconSize.x + rowSpacing + ImGui::CalcTextSize(output.name.c_str()).x); if (wantsThumb) contentWidth = std::max(contentWidth, static_cast(NodeThumbnailCache::kThumbPx)); NE::BeginNode(nodeId); + 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 + // patches work around for pins and node drags — the hover race is + // lost to the editor's background item), so an ImGui::SmallButton + // here never highlights or clicks on most nodes. Hit-test the rect + // geometrically instead, like those patches do. io.MousePos is in + // canvas space inside NE::Begin/End, the same space as the cursor + // position, so the comparison is direct; canvasHovered (screen-space, + // 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); + const ImVec2 btnMin = ImGui::GetCursorScreenPos(); + const ImVec2 btnMax(btnMin.x + labelSize.x + style.FramePadding.x * 2.0f, + btnMin.y + labelSize.y); + ImGui::Dummy(ImVec2(btnMax.x - btnMin.x, btnMax.y - btnMin.y)); + + // IsMouseHoveringRect is pure geometry (no hover-id arbitration) + // and clips against the canvas view, so off-view parts don't hit. + const bool btnHovered = canvasHovered && ImGui::IsMouseHoveringRect(btnMin, btnMax); + const ImU32 frameColor = ImGui::GetColorU32( + btnHovered ? (ImGui::IsMouseDown(ImGuiMouseButton_Left) ? ImGuiCol_ButtonActive + : ImGuiCol_ButtonHovered) + : 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); + + if (btnHovered) + pendingTooltip = pinDisplay.showAllPins + ? "Showing all pins - click to show connected pins only" + : "Showing connected pins only - click to show all pins"; + if (btnHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + TogglePinDisplayMode(node.path); + } + ImVec2 headerBottom = ImGui::GetItemRectMax(); ImGui::Dummy(ImVec2(0.0f, 4.0f)); // breathing room between title and pins @@ -1451,6 +1552,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { uintptr_t pinIdValue = HashId(node.path.GetString() + ":in:" + input.name); m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName}; bool linked = IsPinLinked(node.path, input.name, false); + if (!IsPinVisible(node, input.name, false)) continue; NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input); // Default pivot is the center of the whole icon+text row, which // makes links land mid-label instead of at the icon. Pin it to @@ -1466,6 +1568,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { uintptr_t pinIdValue = HashId(node.path.GetString() + ":out:" + output.name); m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName}; bool linked = IsPinLinked(node.path, output.name, true); + if (!IsPinVisible(node, output.name, true)) continue; float rowWidth = ImGui::CalcTextSize(output.name.c_str()).x + rowSpacing + pinIconSize.x; if (contentWidth > rowWidth) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (contentWidth - rowWidth)); @@ -1478,7 +1581,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, linked, ImColor(GetPinColor(output.typeName))); NE::EndPin(); } - + ImGui::PopID(); NE::EndNode(); // Colored header strip drawn on the node's background draw list, @@ -1529,10 +1632,22 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() { RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin); ImGui::EndPopup(); } + if (m_openPinPickMenu) { + ImGui::OpenPopup("PinPickMenu"); + m_openPinPickMenu = false; + } + if (ImGui::BeginPopup("PinPickMenu")) { + RenderPinPickMenu(); + ImGui::EndPopup(); + } NE::Resume(); NE::End(); + // Deferred from the node loop — see pendingTooltip's declaration. + if (pendingTooltip) + ImGui::SetTooltip("%s", pendingTooltip); + // --- Debug tracing of node interaction events. Edge-triggered so each // event logs once, not per frame. Event order in the editor is: // LMB press -> node becomes active (and is brought to front) -> drag @@ -1723,7 +1838,19 @@ void MaterialEditorPanel::HandleCreateAndDelete() { const PinInfo& b = endIt->second; const PinInfo& outPin = a.isOutput ? a : b; const PinInfo& inPin = a.isOutput ? b : a; - CreateConnection(inPin, outPin); + if (outPin.isMeta || inPin.isMeta) { + // Dropped on (or dragged from) a collapsed "more pins" + // nub: defer to RenderPinPickMenu() to choose which + // actual pin it wires to, instead of connecting now. + const PinInfo& metaSide = outPin.isMeta ? outPin : inPin; + const PinInfo& otherSide = outPin.isMeta ? inPin : outPin; + m_pendingPinPick.nodePath = metaSide.nodePath; + m_pendingPinPick.isOutput = metaSide.isOutput; + m_pendingPinPick.otherPin = otherSide; + m_openPinPickMenu = true; + } else { + CreateConnection(inPin, outPin); + } } } else { NE::RejectNewItem(); @@ -1735,10 +1862,13 @@ void MaterialEditorPanel::HandleCreateAndDelete() { // and pop the create-node menu (deferred to the suspended block below, // since OpenPopup must not run mid-editor); the chosen node is wired to // this pin. + // Dragging from a collapsed meta pin onto empty canvas is not + // supported (there's no single concrete pin/type to auto-wire the + // new node to) — left un-accepted here so the library cancels it. NE::PinId newNodePinId; if (NE::QueryNewNode(&newNodePinId)) { auto it = m_pinIdToInfo.find(newNodePinId.Get()); - if (it != m_pinIdToInfo.end() && NE::AcceptNewItem()) { + if (it != m_pinIdToInfo.end() && !it->second.isMeta && NE::AcceptNewItem()) { m_linkDragSourcePin = it->second; m_openLinkDragMenu = true; m_pendingCreateNodePos = ImGui::GetMousePos(); @@ -1779,6 +1909,62 @@ bool MaterialEditorPanel::IsPinLinked(const pxr::SdfPath& nodePath, const std::s return false; } +MaterialEditorPanel::PinDisplayOverride& MaterialEditorPanel::GetPinDisplayOverride(const pxr::SdfPath& nodePath) { + return m_pinDisplayOverrides[nodePath.GetString()]; +} + +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); +} + +void MaterialEditorPanel::TogglePinDisplayMode(const pxr::SdfPath& nodePath) { + PinDisplayOverride& ov = GetPinDisplayOverride(nodePath); + ov.showAllPins = !ov.showAllPins; + PersistPinDisplayState(nodePath); +} + +void MaterialEditorPanel::RenderPinPickMenu() { + const ShaderGraphNode* node = nullptr; + for (const auto& n : m_graph.nodes) + if (n.path == m_pendingPinPick.nodePath) { node = &n; break; } + if (!node) return; + + const bool isOutput = m_pendingPinPick.isOutput; + const std::vector& pins = isOutput ? node->outputs : node->inputs; + + ImGui::TextDisabled(isOutput ? "Connect output" : "Connect input"); + ImGui::Separator(); + for (const auto& pin : pins) { + if (IsPinLinked(node->path, pin.name, isOutput)) continue; // already wired elsewhere + if (ImGui::MenuItem(pin.name.c_str())) { + CompletePinPick(PinInfo{node->path, pin.name, isOutput, pin.typeName, false}); + ImGui::CloseCurrentPopup(); + break; + } + } +} + +void MaterialEditorPanel::CompletePinPick(const PinInfo& chosen) { + if (m_pendingPinPick.otherPin.isMeta) { + // Dragged directly between two collapsed nubs: this pick resolved + // one side to a real pin, now chain into a second pick for the far + // side (deferred, since we're inside the already-open popup). + PendingPinPick next; + next.nodePath = m_pendingPinPick.otherPin.nodePath; + next.isOutput = m_pendingPinPick.otherPin.isOutput; + next.otherPin = chosen; + m_pendingPinPick = next; + m_openPinPickMenu = true; + return; + } + const PinInfo& outPin = chosen.isOutput ? chosen : m_pendingPinPick.otherPin; + const PinInfo& inPin = chosen.isOutput ? m_pendingPinPick.otherPin : chosen; + CreateConnection(inPin, outPin); +} + void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource) { if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) { ImGui::TextDisabled("Open or create a material first"); @@ -2038,6 +2224,35 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) { })); } +void MaterialEditorPanel::PersistPinDisplayState(const pxr::SdfPath& nodePath) { + if (!m_keepGraphNodeViewSettingsInUsd || !m_stage || !m_commandHistory) return; + + pxr::UsdPrim prim = m_stage->GetPrimAtPath(nodePath); + if (!prim.IsValid()) return; + + static const pxr::TfToken kShowAllKey("uiShowAllPins"); + + const bool newShowAll = GetPinDisplayOverride(nodePath).showAllPins; + + pxr::VtValue oldShowAllVt = prim.GetCustomDataByKey(kShowAllKey); + const bool oldShowAll = oldShowAllVt.IsHolding() ? oldShowAllVt.UncheckedGet() : true; + + if (oldShowAll == newShowAll) + return; + + pxr::UsdStageRefPtr stage = m_stage; + m_commandHistory->Push(std::make_unique( + "Change pin display for " + nodePath.GetName(), + [stage, nodePath, newShowAll]() { + pxr::UsdPrim p = stage->GetPrimAtPath(nodePath); + if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiShowAllPins"), pxr::VtValue(newShowAll)); + }, + [stage, nodePath, oldShowAll]() { + pxr::UsdPrim p = stage->GetPrimAtPath(nodePath); + if (p.IsValid()) p.SetCustomDataByKey(pxr::TfToken("uiShowAllPins"), pxr::VtValue(oldShowAll)); + })); +} + bool MaterialEditorPanel::SaveNodeSettingsCallback(NE::NodeId nodeId, const char* /*data*/, size_t /*size*/, NE::SaveReasonFlags reason, void* userPointer) { auto* self = static_cast(userPointer); diff --git a/src/ui/MaterialEditorPanel.h b/src/ui/MaterialEditorPanel.h index 24aa180..1913e3a 100644 --- a/src/ui/MaterialEditorPanel.h +++ b/src/ui/MaterialEditorPanel.h @@ -54,6 +54,12 @@ public: } void SetColumnWidths(float browserWidth, float previewWidth); + /// Whether toggling a node's pin display mode / revealing a hidden pin + /// authors USD customData (persisted) or stays session-only. Pushed in by + /// Application from AppPreferences, live-updated when the user flips the + /// checkbox in Preferences -> Material. + void SetKeepGraphNodeViewSettingsInUsd(bool value) { m_keepGraphNodeViewSettingsInUsd = value; } + void Render(); private: @@ -63,6 +69,12 @@ private: std::string name; bool isOutput = false; pxr::SdfValueTypeName typeName; + /// True for the collapsed "more pins" connector nub rendered on a + /// node's title row when it's hiding unconnected pins (Hypershade + /// style) — name/typeName are unset. Connecting to/from it opens + /// RenderPinPickMenu() to choose which actual pin to wire, instead of + /// creating the connection immediately. See HandleCreateAndDelete(). + bool isMeta = false; }; /// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId. struct LinkInfo { @@ -70,6 +82,24 @@ private: std::string destInput; }; + /// Hypershade-style per-node pin display state. Always the render-time + /// source of truth (never resync-clobbered); optionally mirrored into USD + /// customData when m_keepGraphNodeViewSettingsInUsd is set. See + /// GetPinDisplayOverride()/IsPinVisible(). + struct PinDisplayOverride { + bool showAllPins = true; + }; + /// A connection dragged onto/from a collapsed meta pin, awaiting the user + /// picking which actual pin on nodePath/isOutput to wire up. otherPin is + /// the already-resolved endpoint on the far side of the link (itself meta + /// if the user dragged directly between two collapsed nubs, in which case + /// picking chains into a second RenderPinPickMenu() for that side). + struct PendingPinPick { + pxr::SdfPath nodePath; + bool isOutput = false; + PinInfo otherPin; + }; + void RenderToolbar(); /// Property editor for the node selected in the graph, shown below the /// shader-ball preview. Values apply live while a widget is being @@ -122,6 +152,35 @@ private: void PersistNodePosition(ax::NodeEditor::NodeId nodeId); bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const; + /// 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. + void SeedPinDisplayOverrides(); + /// Creates the entry (defaulted to "show 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(). + 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); + /// 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); + /// 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 + /// be called from within the NE::Suspend()/Resume() block, like the + /// other popups. + void RenderPinPickMenu(); + /// Finishes a pick: creates the real connection to/from chosen, unless + /// the far side of the pending pick was itself a meta pin, in which case + /// this chains into a second pick for that side instead. + void CompletePinPick(const PinInfo& chosen); + static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId, const char* data, size_t size, ax::NodeEditor::SaveReasonFlags reason, @@ -170,6 +229,18 @@ private: bool m_openLinkDragMenu = false; PinInfo m_linkDragSourcePin; + /// Hypershade-style pin display state, keyed by node path string. Always + /// the render-time source of truth; see PinDisplayOverride. + std::unordered_map m_pinDisplayOverrides; + /// Whether node toggles author USD customData (see + /// SetKeepGraphNodeViewSettingsInUsd). + bool m_keepGraphNodeViewSettingsInUsd = false; + /// Deferred "choose pin" popup request: set from HandleCreateAndDelete() + /// when a link is dropped on/dragged from a collapsed meta pin, actually + /// opened after NE::Suspend(), like m_openLinkDragMenu. + bool m_openPinPickMenu = false; + PendingPinPick m_pendingPinPick; + /// 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;