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:
+167
-18
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user