diff --git a/src/ui/MaterialEditorPanel.cpp b/src/ui/MaterialEditorPanel.cpp index 6173c33..909d7f4 100644 --- a/src/ui/MaterialEditorPanel.cpp +++ b/src/ui/MaterialEditorPanel.cpp @@ -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 #include #include #include +#include +#include #include #include #include @@ -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( + 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(m_stage, path, shaderId, pos)); SyncFromUsd(); + return path; } void MaterialEditorPanel::CreateConnection(const PinInfo& destInput, const PinInfo& sourceOutput) { diff --git a/src/ui/MaterialEditorPanel.h b/src/ui/MaterialEditorPanel.h index 59d727f..93f328d 100644 --- a/src/ui/MaterialEditorPanel.h +++ b/src/ui/MaterialEditorPanel.h @@ -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