Hypershade-style pin display modes for material graph nodes

Each node gets an All/Conn toggle: connected-only mode hides
unconnected pins behind collapsed connector nubs on the title row;
dragging a link onto (or from) a nub opens a menu to pick which hidden
pin to wire, and the pin appears once connected. The toggle is drawn
and hit-tested geometrically, and its tooltip is deferred to after
NE::End() so it lands at the cursor. A new Preferences > Material
checkbox opts into persisting the per-node mode as USD customData
(uiShowAllPins); otherwise it stays session-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 23:02:56 +08:00
parent 22406fbe63
commit c7edd145df
6 changed files with 331 additions and 7 deletions
+5
View File
@@ -138,6 +138,7 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
if (!materialPrim.IsValid()) return snapshot; if (!materialPrim.IsValid()) return snapshot;
static const pxr::TfToken kUiPositionKey("uiPosition"); static const pxr::TfToken kUiPositionKey("uiPosition");
static const pxr::TfToken kUiShowAllPinsKey("uiShowAllPins");
// Seed with every shader under the material, recursing into nested node // Seed with every shader under the material, recursing into nested node
// graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph // graphs (usdMtlx nests a material's nodes inside UsdShadeNodeGraph
@@ -179,6 +180,10 @@ ShaderGraphSnapshot MaterialManager::GetShaderGraph(const pxr::SdfPath& material
node.hasAuthoredPosition = true; node.hasAuthoredPosition = true;
} }
pxr::VtValue showAllValue = child.GetCustomDataByKey(kUiShowAllPinsKey);
if (showAllValue.IsHolding<bool>())
node.showAllPins = showAllValue.UncheckedGet<bool>();
// Prefer the full Sdr-defined pin set (so unauthored pins can still be // Prefer the full Sdr-defined pin set (so unauthored pins can still be
// dragged to create a connection); fall back to authored attributes // dragged to create a connection); fall back to authored attributes
// only for shader types the registry doesn't know about. // only for shader types the registry doesn't know about.
+6
View File
@@ -48,6 +48,12 @@ struct ShaderGraphNode {
/// False when no uiPosition custom data is authored (typical for networks /// False when no uiPosition custom data is authored (typical for networks
/// referenced from .mtlx) — the editor auto-lays such nodes out instead. /// referenced from .mtlx) — the editor auto-lays such nodes out instead.
bool hasAuthoredPosition = false; 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<ShaderPinInfo> inputs; std::vector<ShaderPinInfo> inputs;
std::vector<ShaderPinInfo> outputs; std::vector<ShaderPinInfo> outputs;
}; };
+23 -1
View File
@@ -429,11 +429,15 @@ void Application::LoadPreferences()
m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }(); m_prefs.materialBrowserWidth = [&]{ try { return std::stof(val); } catch(...){ return 220.0f; } }();
else if (key == "MaterialPreviewWidth") else if (key == "MaterialPreviewWidth")
m_prefs.materialPreviewWidth = [&]{ try { return std::stof(val); } catch(...){ return 320.0f; } }(); 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_materialEditorPanel->SetColumnWidths(m_prefs.materialBrowserWidth,
m_prefs.materialPreviewWidth); m_prefs.materialPreviewWidth);
m_materialEditorPanel->SetKeepGraphNodeViewSettingsInUsd(m_prefs.keepGraphNodeViewSettingsInUsd);
}
} }
void Application::SavePreferences() void Application::SavePreferences()
@@ -455,6 +459,7 @@ void Application::SavePreferences()
f << "OcioLook=" << m_prefs.ocioLook << "\n"; f << "OcioLook=" << m_prefs.ocioLook << "\n";
f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n"; f << "MaterialBrowserWidth=" << m_prefs.materialBrowserWidth << "\n";
f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n"; f << "MaterialPreviewWidth=" << m_prefs.materialPreviewWidth << "\n";
f << "KeepGraphNodeViewSettingsInUsd=" << (m_prefs.keepGraphNodeViewSettingsInUsd ? 1 : 0) << "\n";
} }
void Application::ApplyPrefsToAllViewports() void Application::ApplyPrefsToAllViewports()
@@ -505,6 +510,23 @@ void Application::RenderPreferencesDialog()
ImGui::EndTabItem(); 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 ──────────────────────────────────────────────────────── // ── Viewport ────────────────────────────────────────────────────────
if (ImGui::BeginTabItem("Viewport")) { if (ImGui::BeginTabItem("Viewport")) {
ImGui::Spacing(); ImGui::Spacing();
+5
View File
@@ -30,6 +30,11 @@ struct AppPreferences {
std::string ocioLook; std::string ocioLook;
float materialBrowserWidth = 220.0f; ///< Material Editor left-column width float materialBrowserWidth = 220.0f; ///< Material Editor left-column width
float materialPreviewWidth = 320.0f; ///< Material Editor right-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 { class Application {
+221 -6
View File
@@ -306,7 +306,7 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) {
MaterialEditorPanel::MaterialEditorPanel() { MaterialEditorPanel::MaterialEditorPanel() {
// Route the vendored editor's click-time hit-test dump into our log so // Route the vendored editor's click-time hit-test dump into our log so
// it interleaves with the [NodeGraph] event trace. Disabled: uncomment to // 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)); }; // g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); };
NE::Config config; NE::Config config;
@@ -1311,6 +1311,16 @@ void MaterialEditorPanel::SyncFromUsd() {
} }
m_graph = m_materialManager->GetShaderGraph(m_materialPath); m_graph = m_materialManager->GetShaderGraph(m_materialPath);
ApplyFallbackLayout(m_graph); 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() { void MaterialEditorPanel::RenderNodeGraphCanvas() {
@@ -1365,6 +1375,12 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// One whole-graph revision hash drives every material thumbnail this frame. // One whole-graph revision hash drives every material thumbnail this frame.
const size_t graphRevision = ComputeGraphRevision(m_stage, m_graph); 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) { for (const auto& node : m_graph.nodes) {
uintptr_t nodeIdValue = HashId(node.path.GetString()); uintptr_t nodeIdValue = HashId(node.path.GetString());
NE::NodeId nodeId(nodeIdValue); NE::NodeId nodeId(nodeIdValue);
@@ -1414,18 +1430,103 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// ImGui fork this project doesn't use) — approximate the classic // ImGui fork this project doesn't use) — approximate the classic
// "outputs hug the right edge" look by right-aligning each output row // "outputs hug the right edge" look by right-aligning each output row
// within the widest row measured across the whole node. // 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; const float rowSpacing = ImGui::GetStyle().ItemSpacing.x;
float contentWidth = ImGui::CalcTextSize(title.c_str()).x; float contentWidth = ImGui::CalcTextSize(title.c_str()).x;
for (const auto& input : node.inputs) 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) 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) if (wantsThumb)
contentWidth = std::max(contentWidth, static_cast<float>(NodeThumbnailCache::kThumbPx)); contentWidth = std::max(contentWidth, static_cast<float>(NodeThumbnailCache::kThumbPx));
NE::BeginNode(nodeId); NE::BeginNode(nodeId);
ImGui::PushID(node.path.GetText());
ImVec2 headerTop = ImGui::GetCursorScreenPos(); 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()); 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(); ImVec2 headerBottom = ImGui::GetItemRectMax();
ImGui::Dummy(ImVec2(0.0f, 4.0f)); // breathing room between title and pins 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); uintptr_t pinIdValue = HashId(node.path.GetString() + ":in:" + input.name);
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName}; m_pinIdToInfo[pinIdValue] = PinInfo{node.path, input.name, false, input.typeName};
bool linked = IsPinLinked(node.path, input.name, false); bool linked = IsPinLinked(node.path, input.name, false);
if (!IsPinVisible(node, input.name, false)) continue;
NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input); NE::BeginPin(NE::PinId(pinIdValue), NE::PinKind::Input);
// Default pivot is the center of the whole icon+text row, which // 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 // 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); uintptr_t pinIdValue = HashId(node.path.GetString() + ":out:" + output.name);
m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName}; m_pinIdToInfo[pinIdValue] = PinInfo{node.path, output.name, true, output.typeName};
bool linked = IsPinLinked(node.path, output.name, true); 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; float rowWidth = ImGui::CalcTextSize(output.name.c_str()).x + rowSpacing + pinIconSize.x;
if (contentWidth > rowWidth) if (contentWidth > rowWidth)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (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))); ax::Widgets::Icon(pinIconSize, ax::Widgets::IconType::Circle, linked, ImColor(GetPinColor(output.typeName)));
NE::EndPin(); NE::EndPin();
} }
ImGui::PopID();
NE::EndNode(); NE::EndNode();
// Colored header strip drawn on the node's background draw list, // 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); RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin);
ImGui::EndPopup(); ImGui::EndPopup();
} }
if (m_openPinPickMenu) {
ImGui::OpenPopup("PinPickMenu");
m_openPinPickMenu = false;
}
if (ImGui::BeginPopup("PinPickMenu")) {
RenderPinPickMenu();
ImGui::EndPopup();
}
NE::Resume(); NE::Resume();
NE::End(); 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 // --- Debug tracing of node interaction events. Edge-triggered so each
// event logs once, not per frame. Event order in the editor is: // event logs once, not per frame. Event order in the editor is:
// LMB press -> node becomes active (and is brought to front) -> drag // 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& b = endIt->second;
const PinInfo& outPin = a.isOutput ? a : b; const PinInfo& outPin = a.isOutput ? a : b;
const PinInfo& inPin = a.isOutput ? b : a; 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 { } else {
NE::RejectNewItem(); NE::RejectNewItem();
@@ -1735,10 +1862,13 @@ void MaterialEditorPanel::HandleCreateAndDelete() {
// and pop the create-node menu (deferred to the suspended block below, // 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 // since OpenPopup must not run mid-editor); the chosen node is wired to
// this pin. // 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; NE::PinId newNodePinId;
if (NE::QueryNewNode(&newNodePinId)) { if (NE::QueryNewNode(&newNodePinId)) {
auto it = m_pinIdToInfo.find(newNodePinId.Get()); 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_linkDragSourcePin = it->second;
m_openLinkDragMenu = true; m_openLinkDragMenu = true;
m_pendingCreateNodePos = ImGui::GetMousePos(); m_pendingCreateNodePos = ImGui::GetMousePos();
@@ -1779,6 +1909,62 @@ bool MaterialEditorPanel::IsPinLinked(const pxr::SdfPath& nodePath, const std::s
return false; 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<ShaderPinInfo>& 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) { void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource) {
if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) { if (!m_materialManager || !m_stage || m_materialPath.IsEmpty()) {
ImGui::TextDisabled("Open or create a material first"); 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<bool>() ? oldShowAllVt.UncheckedGet<bool>() : true;
if (oldShowAll == newShowAll)
return;
pxr::UsdStageRefPtr stage = m_stage;
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"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*/, bool MaterialEditorPanel::SaveNodeSettingsCallback(NE::NodeId nodeId, const char* /*data*/, size_t /*size*/,
NE::SaveReasonFlags reason, void* userPointer) { NE::SaveReasonFlags reason, void* userPointer) {
auto* self = static_cast<MaterialEditorPanel*>(userPointer); auto* self = static_cast<MaterialEditorPanel*>(userPointer);
+71
View File
@@ -54,6 +54,12 @@ public:
} }
void SetColumnWidths(float browserWidth, float previewWidth); 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(); void Render();
private: private:
@@ -63,6 +69,12 @@ private:
std::string name; std::string name;
bool isOutput = false; bool isOutput = false;
pxr::SdfValueTypeName typeName; 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. /// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
struct LinkInfo { struct LinkInfo {
@@ -70,6 +82,24 @@ private:
std::string destInput; 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(); void RenderToolbar();
/// Property editor for the node selected in the graph, shown below the /// Property editor for the node selected in the graph, shown below the
/// shader-ball preview. Values apply live while a widget is being /// shader-ball preview. Values apply live while a widget is being
@@ -122,6 +152,35 @@ private:
void PersistNodePosition(ax::NodeEditor::NodeId nodeId); void PersistNodePosition(ax::NodeEditor::NodeId nodeId);
bool IsPinLinked(const pxr::SdfPath& nodePath, const std::string& pinName, bool isOutput) const; 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, static bool SaveNodeSettingsCallback(ax::NodeEditor::NodeId nodeId,
const char* data, size_t size, const char* data, size_t size,
ax::NodeEditor::SaveReasonFlags reason, ax::NodeEditor::SaveReasonFlags reason,
@@ -170,6 +229,18 @@ private:
bool m_openLinkDragMenu = false; bool m_openLinkDragMenu = false;
PinInfo m_linkDragSourcePin; 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<std::string, PinDisplayOverride> 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, /// Column widths of the browser (left) and preview (right) sections,
/// user-adjustable via the splitters between them and the canvas. /// user-adjustable via the splitters between them and the canvas.
float m_browserWidth = 220.0f; float m_browserWidth = 220.0f;