Drag-to-create-and-connect nodes; double-click to rename materials

Drag a link off a pin onto empty canvas to open a create-node menu at
the drop point (the editor's QueryNewNode path), filtered to nodes with a
type-connectable pin; the chosen node is created and auto-wired to the
source pin. Direction is handled both ways — drag from an output makes a
downstream consumer, from an input an upstream producer. Type matching is
lenient (exact, or same scalar/2-/3-/4-vector class), mirroring what USD/
MaterialX networks already tolerate; the target pin is the first exact
match else the first connectable. RenderNodeSearchMenu gained an optional
linkSource arg so the TAB popup and this share one implementation, and
CreateShaderNode now returns the created path. Create and connect are two
undo steps.

Materials list: double-click a row starts an inline rename (mirrors
SceneHierarchyPanel — Enter commits, focus-loss/Esc cancels) through
RenamePrimCommand, which fixes up bindings/arcs via UsdNamespaceEditor.
Selection and the open graph follow the rename. This repurposes the row's
double-click, which previously opened the graph; opening stays on the
Show Graph button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 22:13:36 +08:00
parent 2ddeeed061
commit 7d214821f5
2 changed files with 189 additions and 22 deletions
+167 -18
View File
@@ -5,11 +5,14 @@
#include "../core/commands/ConnectShaderAttrsCommand.h"
#include "../core/commands/DisconnectShaderAttrCommand.h"
#include "../core/commands/AttributeSetCommand.h"
#include "../core/commands/RenamePrimCommand.h"
#include "../utils/Logger.h"
#include <pxr/usd/usdShade/shader.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/sdr/registry.h>
#include <pxr/usd/sdr/shaderNode.h>
#include <pxr/usd/sdr/shaderProperty.h>
#include <pxr/usd/sdf/types.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/base/gf/vec2f.h>
@@ -213,6 +216,49 @@ Icon IconForNodeCategory(const std::string& category) {
return Icon::CircleDot;
}
// Coarse connectability class for lenient shader-pin type matching (0 = only
// matches its own exact type; 1 = scalar; 2/3/4 = 2/3/4-component vector).
// Mirrors the leniency USD/MaterialX shading networks already tolerate.
int PinTypeClass(const pxr::SdfValueTypeName& t) {
const auto& tn = pxr::SdfValueTypeNames;
if (t == tn->Float || t == tn->Half || t == tn->Double || t == tn->Int) return 1;
if (t == tn->Float2) return 2;
if (t == tn->Float3 || t == tn->Color3f || t == tn->Vector3f ||
t == tn->Normal3f || t == tn->Point3f) return 3;
if (t == tn->Float4 || t == tn->Color4f) return 4;
return 0;
}
bool PinTypesConnectable(const pxr::SdfValueTypeName& a, const pxr::SdfValueTypeName& b) {
if (a == b) return true;
const int ca = PinTypeClass(a);
return ca != 0 && ca == PinTypeClass(b);
}
// Best pin of the requested direction on shaderId to receive a connection from
// a pin of type wantType: first exact type match, else first type-connectable
// pin. Returns false when the node has no connectable pin of that direction.
bool BestConnectablePin(const std::string& shaderId, bool wantOutput,
const pxr::SdfValueTypeName& wantType,
std::string& outName, pxr::SdfValueTypeName& outType) {
pxr::SdrShaderNodeConstPtr node =
pxr::SdrRegistry::GetInstance().GetShaderNodeByIdentifier(pxr::TfToken(shaderId));
if (!node) return false;
bool haveConnectable = false;
for (const pxr::TfToken& n : wantOutput ? node->GetShaderOutputNames()
: node->GetShaderInputNames()) {
const auto* prop = wantOutput ? node->GetShaderOutput(n) : node->GetShaderInput(n);
if (!prop) continue;
const pxr::SdfValueTypeName t = prop->GetTypeAsSdfType().GetSdfType();
if (t == wantType) { outName = n.GetString(); outType = t; return true; }
if (!haveConnectable && PinTypesConnectable(wantType, t)) {
outName = n.GetString();
outType = t;
haveConnectable = true;
}
}
return haveConnectable;
}
// Networks referenced from .mtlx (or authored elsewhere) carry no uiPosition
// custom data, so every node would seed at (0,0) in a pile. Give those nodes
// layered left-to-right positions instead: each node sits one column left of
@@ -748,22 +794,68 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
const float listHeight = std::max(1.0f, browserAvail * m_browserListRatio);
ImGui::BeginChild("MaterialBrowserList", ImVec2(0.0f, listHeight), false);
for (const pxr::SdfPath& path : materials) {
// PushID(full path) keeps IDs unique for same-named materials in
// different scopes without an explicit ## suffix on the label.
ImGui::PushID(path.GetText());
const bool isSelected = (path == m_browserSelection);
const bool isOpen = (path == m_materialPath);
if (isOpen)
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 210, 90, 255));
// Name for the label (Hypershade lists swatch names); ## suffix keeps
// the ImGui ID unique for same-named materials in different scopes.
std::string label = path.GetName() + "##" + path.GetString();
if (ImGui::Selectable(label.c_str(), isSelected, ImGuiSelectableFlags_AllowDoubleClick)) {
m_browserSelection = path;
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
OpenOrCreateMaterial(path.GetString());
const bool isRenaming = (path == m_renamingMaterial);
if (isRenaming) {
// Inline rename (mirrors SceneHierarchyPanel): commit on Enter,
// cancel on focus loss. firstFrame guards against IsItemDeactivated
// firing spuriously on the frame SetKeyboardFocusHere activates it.
ImGui::SetNextItemWidth(-FLT_MIN);
const bool firstFrame = m_materialRenameJustStarted;
if (m_materialRenameJustStarted) {
ImGui::SetKeyboardFocusHere();
m_materialRenameJustStarted = false;
}
const bool commit = ImGui::InputText("##matrename", m_materialRenameBuf,
sizeof(m_materialRenameBuf),
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_AutoSelectAll);
const bool canceled = !firstFrame && ImGui::IsItemDeactivated() && !commit;
if (commit && m_materialRenameBuf[0] != '\0') {
const std::string newName = SanitizeUsdName(m_materialRenameBuf);
m_renamingMaterial = pxr::SdfPath();
if (!newName.empty() && newName != path.GetName() && m_commandHistory) {
const pxr::SdfPath newPath =
path.GetParentPath().AppendChild(pxr::TfToken(newName));
m_commandHistory->Push(std::make_unique<RenamePrimCommand>(
m_stage, path, newName));
// Follow the rename with the selection and (if it was the
// open one) the loaded graph. UsdNamespaceEditor fixes up
// bindings/arcs, so bound prims keep pointing at it.
if (m_browserSelection == path) m_browserSelection = newPath;
if (m_materialPath == path) OpenOrCreateMaterial(newPath.GetString());
ImGui::PopID();
break; // 'materials' now holds stale paths — rebuilt next frame
}
} else if (canceled) {
m_renamingMaterial = pxr::SdfPath();
}
} else {
if (isOpen)
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 210, 90, 255));
if (ImGui::Selectable(path.GetName().c_str(), isSelected,
ImGuiSelectableFlags_AllowDoubleClick)) {
m_browserSelection = path;
// Double-click starts an inline rename (single-click selects;
// "Show Graph" opens the material into the canvas).
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
m_renamingMaterial = path;
std::snprintf(m_materialRenameBuf, sizeof(m_materialRenameBuf), "%s",
path.GetName().c_str());
m_materialRenameJustStarted = true;
}
}
if (isOpen)
ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", path.GetText());
}
if (isOpen)
ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", path.GetText());
ImGui::PopID();
}
ImGui::EndChild();
@@ -1270,6 +1362,14 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos));
ImGui::EndPopup();
}
if (m_openLinkDragMenu) {
ImGui::OpenPopup("LinkDragCreateNode");
m_openLinkDragMenu = false;
}
if (ImGui::BeginPopup("LinkDragCreateNode")) {
RenderNodeSearchMenu(NE::ScreenToCanvas(m_pendingCreateNodePos), &m_linkDragSourcePin);
ImGui::EndPopup();
}
NE::Resume();
NE::End();
@@ -1470,6 +1570,21 @@ void MaterialEditorPanel::HandleCreateAndDelete() {
NE::RejectNewItem();
}
}
// Nuke-style drag-to-create: a link dragged onto empty canvas (rather
// than another pin) surfaces here. On release, remember the source pin
// 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.
NE::PinId newNodePinId;
if (NE::QueryNewNode(&newNodePinId)) {
auto it = m_pinIdToInfo.find(newNodePinId.Get());
if (it != m_pinIdToInfo.end() && NE::AcceptNewItem()) {
m_linkDragSourcePin = it->second;
m_openLinkDragMenu = true;
m_pendingCreateNodePos = ImGui::GetMousePos();
}
}
NE::EndCreate();
}
@@ -1505,12 +1620,20 @@ bool MaterialEditorPanel::IsPinLinked(const pxr::SdfPath& nodePath, const std::s
return false;
}
void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
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");
return;
}
// The new node must expose a pin of the opposite direction to the pin the
// link was dragged from.
const bool wantOutput = linkSource && !linkSource->isOutput;
if (linkSource) {
ImGui::TextDisabled("Connect %s %s to a new node:",
linkSource->isOutput ? "output" : "input", linkSource->name.c_str());
}
if (ImGui::IsWindowAppearing()) {
m_nodeSearchBuf[0] = '\0';
m_nodeSearchSelected = 0;
@@ -1522,8 +1645,18 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
if (ImGui::IsItemEdited())
m_nodeSearchSelected = 0;
const auto matches =
auto matches =
FilterShaderNodeTypes(m_materialManager->GetAvailableShaderNodes(), m_nodeSearchBuf);
if (linkSource) {
// Keep only nodes with a pin type-connectable to the dragged pin.
std::string pn; pxr::SdfValueTypeName pt;
matches.erase(std::remove_if(matches.begin(), matches.end(),
[&](const ShaderNodeTypeInfo* info) {
return !BestConnectablePin(info->identifier, wantOutput,
linkSource->typeName, pn, pt);
}),
matches.end());
}
const bool movedDown = ImGui::IsKeyPressed(ImGuiKey_DownArrow);
const bool movedUp = ImGui::IsKeyPressed(ImGuiKey_UpArrow);
@@ -1554,7 +1687,22 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
ImGui::EndChild();
if (chosen) {
CreateShaderNode(chosen->identifier, canvasPos);
if (linkSource) {
const pxr::SdfPath newPath = CreateShaderNode(chosen->identifier, canvasPos);
std::string pinName; pxr::SdfValueTypeName pinType;
if (!newPath.IsEmpty() &&
BestConnectablePin(chosen->identifier, wantOutput, linkSource->typeName,
pinName, pinType)) {
const PinInfo newPin{newPath, pinName, wantOutput, pinType};
// dest is the input pin, source the output pin.
if (linkSource->isOutput)
CreateConnection(newPin, *linkSource);
else
CreateConnection(*linkSource, newPin);
}
} else {
CreateShaderNode(chosen->identifier, canvasPos);
}
ImGui::CloseCurrentPopup();
}
}
@@ -1607,8 +1755,8 @@ ImVec2 MaterialEditorPanel::FindFreeCanvasSpot(ImVec2 desired, const std::string
return desired;
}
void MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos) {
if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return;
pxr::SdfPath MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos) {
if (!m_stage || !m_commandHistory || m_materialPath.IsEmpty()) return pxr::SdfPath();
std::string baseName = SanitizeUsdName(shaderId);
if (baseName.empty()) baseName = "Shader";
@@ -1624,6 +1772,7 @@ void MaterialEditorPanel::CreateShaderNode(const std::string& shaderId, const Im
m_commandHistory->Push(std::make_unique<CreateShaderNodeCommand>(m_stage, path, shaderId, pos));
SyncFromUsd();
return path;
}
void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) {
+22 -4
View File
@@ -60,7 +60,7 @@ private:
struct PinInfo {
pxr::SdfPath nodePath;
std::string name;
bool isOutput;
bool isOutput = false;
pxr::SdfValueTypeName typeName;
};
/// Resolved endpoint of a rendered link, keyed by its ax::NodeEditor LinkId.
@@ -92,9 +92,13 @@ private:
void RenderPreviewPanel();
void HandleCreateAndDelete();
/// Nuke-style TAB popup: type-to-filter shader node list; Enter or click
/// creates the highlighted node at canvasPos.
void RenderNodeSearchMenu(const ImVec2& canvasPos);
void CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos);
/// creates the highlighted node at canvasPos. When linkSource is non-null
/// (a connection was dragged onto empty canvas), the list is filtered to
/// nodes with a type-connectable pin and the chosen node is auto-wired to
/// linkSource.
void RenderNodeSearchMenu(const ImVec2& canvasPos, const PinInfo* linkSource = nullptr);
/// Creates the node and returns its prim path (empty on failure).
pxr::SdfPath CreateShaderNode(const std::string& shaderId, const ImVec2& canvasPos);
/// Nudges a creation point right until the new node's estimated rectangle
/// clears every existing node's rendered rectangle. Overlapping nodes
/// fight over the editor's hit test: the buried one becomes unmovable and
@@ -152,6 +156,13 @@ private:
char m_nodeSearchBuf[128] = "";
int m_nodeSearchSelected = 0;
/// Nuke-style drag-to-create: set when a connection was dragged from a pin
/// onto empty canvas. m_openLinkDragMenu requests the create-node popup on
/// the next frame (deferred so OpenPopup runs while the editor is
/// suspended); m_linkDragSourcePin is the pin to auto-wire the new node to.
bool m_openLinkDragMenu = false;
PinInfo m_linkDragSourcePin;
/// 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;
@@ -162,6 +173,13 @@ private:
/// splitter between them.
float m_browserListRatio = 0.45f;
/// Inline-rename state for the materials list (double-click a row to start;
/// mirrors SceneHierarchyPanel): the material being renamed (empty = none),
/// the edit buffer, and a one-frame flag to focus the InputText on start.
pxr::SdfPath m_renamingMaterial;
char m_materialRenameBuf[128] = "";
bool m_materialRenameJustStarted = false;
/// Search text of the browser's persistent create-node list.
char m_browserNodeSearchBuf[128] = "";
/// Shader id clicked in the browser's create-node list; created at the