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:
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user