Group create-node list by source and category; make editor columns resizable
The create-node list is now a two-level tree: a header per node source (USD, MaterialX, Arnold, Cycles - from the Sdr sourceType, with glslfx and OSL merged into USD so core nodes don't appear twice) containing Hypershade-style category sub-trees (Material / Texture / Geometry / Utility / Light) derived from Sdr context, terminal output types, role, and name keywords. Duplicate collapsing is per (source, label) so same-named nodes from different providers both stay; searching forces all sections open. The TAB popup shows [Source/Category] per entry. The browser and preview columns get drag splitters (viewport-divider styling, clamped widths); the canvas absorbs the remainder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,8 +122,9 @@ size_t ComputeGraphRevision(const pxr::UsdStageRefPtr& stage, const ShaderGraphS
|
||||
|
||||
// Case-insensitive substring filter over the Sdr node-type list for the
|
||||
// searchable create-node UIs (TAB popup, browser list); matches label or
|
||||
// identifier, and collapses duplicate labels (one per source-type parser
|
||||
// variant) to a single entry.
|
||||
// identifier, and collapses duplicate (source, label) pairs — parser
|
||||
// variants within one source group — to a single entry. The same label under
|
||||
// different sources (e.g. an "add" in both MaterialX and Arnold) stays.
|
||||
std::vector<const ShaderNodeTypeInfo*> FilterShaderNodeTypes(
|
||||
const std::vector<ShaderNodeTypeInfo>& nodeTypes, const char* rawQuery) {
|
||||
auto toLower = [](std::string s) {
|
||||
@@ -140,13 +141,37 @@ std::vector<const ShaderNodeTypeInfo*> FilterShaderNodeTypes(
|
||||
toLower(info.label).find(query) == std::string::npos &&
|
||||
toLower(info.identifier).find(query) == std::string::npos)
|
||||
continue;
|
||||
if (!seenLabels.insert(info.label).second)
|
||||
if (!seenLabels.insert(info.source + "|" + info.label).second)
|
||||
continue;
|
||||
matches.push_back(&info);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Vertical drag-splitter between the material editor's columns. Adjusts
|
||||
// *width by the mouse drag (negated for a right-hand column, which grows
|
||||
// when dragged left); colours match the viewport's split dividers.
|
||||
void VerticalSplitter(const char* id, float* width, float minWidth, float maxWidth,
|
||||
bool rightSideColumn) {
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
const float height = std::max(1.0f, ImGui::GetContentRegionAvail().y);
|
||||
ImGui::InvisibleButton(id, ImVec2(6.0f, height));
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
const bool active = ImGui::IsItemActive();
|
||||
if (hovered || active)
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
|
||||
if (active) {
|
||||
const float delta = ImGui::GetIO().MouseDelta.x * (rightSideColumn ? -1.0f : 1.0f);
|
||||
*width = std::clamp(*width + delta, minWidth, maxWidth);
|
||||
}
|
||||
ImVec2 rectMin = ImGui::GetItemRectMin();
|
||||
ImVec2 rectMax = ImGui::GetItemRectMax();
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||
ImVec2(rectMin.x + 2.0f, rectMin.y), ImVec2(rectMax.x - 2.0f, rectMax.y),
|
||||
(hovered || active) ? IM_COL32(250, 150, 66, 200) : IM_COL32(80, 80, 80, 180));
|
||||
ImGui::SameLine(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -231,19 +256,20 @@ void MaterialEditorPanel::Render() {
|
||||
|
||||
// Hypershade-style three-column layout: material browser on the left,
|
||||
// node-graph work area in the middle, shader-ball viewer on the right.
|
||||
ImGui::BeginChild("MaterialBrowserRegion", ImVec2(220.0f, 0.0f), true);
|
||||
// The outer columns are user-resizable via the splitters between them.
|
||||
ImGui::BeginChild("MaterialBrowserRegion", ImVec2(m_browserWidth, 0.0f), true);
|
||||
RenderMaterialBrowser();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
VerticalSplitter("##MaterialSplitL", &m_browserWidth, 140.0f, 500.0f,
|
||||
/*rightSideColumn=*/false);
|
||||
|
||||
// Preview column is 320 wide so the PropertyPanel-style attribute table
|
||||
// (dot + name + value columns) fits below the shader ball.
|
||||
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-320.0f, 0.0f), false);
|
||||
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-(m_previewWidth + 6.0f), 0.0f), false);
|
||||
RenderNodeGraphCanvas();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
VerticalSplitter("##MaterialSplitR", &m_previewWidth, 220.0f, 640.0f,
|
||||
/*rightSideColumn=*/true);
|
||||
|
||||
ImGui::BeginChild("MaterialPreviewRegion", ImVec2(0.0f, 0.0f), true);
|
||||
RenderPreviewPanel();
|
||||
@@ -633,21 +659,55 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
|
||||
if (m_materialPath.IsEmpty()) {
|
||||
ImGui::TextDisabled("Open a material first");
|
||||
} else {
|
||||
const bool searching = (m_browserNodeSearchBuf[0] != '\0');
|
||||
const auto matches =
|
||||
FilterShaderNodeTypes(m_materialManager->GetAvailableShaderNodes(), m_browserNodeSearchBuf);
|
||||
if (matches.empty())
|
||||
ImGui::TextDisabled("No matching nodes");
|
||||
// Matches arrive sorted by (source, category, label) — a collapsing
|
||||
// header per source (USD / MaterialX / Arnold / ...) with a category
|
||||
// sub-tree (Material / Texture / Geometry / Utility ...) inside,
|
||||
// Hypershade-style. While searching, everything is forced open so a
|
||||
// collapsed section can never hide a hit.
|
||||
std::string currentSource;
|
||||
std::string currentCategory;
|
||||
bool sourceOpen = false;
|
||||
bool categoryOpen = false;
|
||||
auto closeCategory = [&]() {
|
||||
if (categoryOpen) ImGui::TreePop();
|
||||
categoryOpen = false;
|
||||
};
|
||||
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
||||
const ShaderNodeTypeInfo* info = matches[i];
|
||||
std::string itemLabel = info->label;
|
||||
if (!info->family.empty())
|
||||
itemLabel += " [" + info->family + "]";
|
||||
itemLabel += "##" + std::to_string(i);
|
||||
if (info->source != currentSource) {
|
||||
closeCategory();
|
||||
currentSource = info->source;
|
||||
currentCategory = std::string();
|
||||
if (searching)
|
||||
ImGui::SetNextItemOpen(true);
|
||||
sourceOpen = ImGui::CollapsingHeader(currentSource.c_str(),
|
||||
ImGuiTreeNodeFlags_DefaultOpen);
|
||||
}
|
||||
if (!sourceOpen)
|
||||
continue;
|
||||
if (info->category != currentCategory) {
|
||||
closeCategory();
|
||||
currentCategory = info->category;
|
||||
if (searching)
|
||||
ImGui::SetNextItemOpen(true);
|
||||
// "##source" keeps IDs unique — category names repeat per source.
|
||||
std::string catLabel = currentCategory + "##" + currentSource;
|
||||
categoryOpen = ImGui::TreeNodeEx(catLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen);
|
||||
}
|
||||
if (!categoryOpen)
|
||||
continue;
|
||||
std::string itemLabel = info->label + "##" + std::to_string(i);
|
||||
if (ImGui::Selectable(itemLabel.c_str(), false))
|
||||
m_pendingCreateShaderId = info->identifier; // created at view center next canvas pass
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", info->identifier.c_str());
|
||||
}
|
||||
closeCategory();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
@@ -1041,10 +1101,8 @@ void MaterialEditorPanel::RenderNodeSearchMenu(const ImVec2& canvasPos) {
|
||||
ImGui::TextDisabled("No matching nodes");
|
||||
for (int i = 0; i < static_cast<int>(matches.size()); ++i) {
|
||||
const ShaderNodeTypeInfo* info = matches[i];
|
||||
std::string itemLabel = info->label;
|
||||
if (!info->family.empty())
|
||||
itemLabel += " [" + info->family + "]";
|
||||
itemLabel += "##" + std::to_string(i);
|
||||
std::string itemLabel =
|
||||
info->label + " [" + info->source + "/" + info->category + "]##" + std::to_string(i);
|
||||
const bool selected = (i == m_nodeSearchSelected);
|
||||
if (ImGui::Selectable(itemLabel.c_str(), selected))
|
||||
chosen = info;
|
||||
|
||||
@@ -133,6 +133,11 @@ private:
|
||||
char m_nodeSearchBuf[128] = "";
|
||||
int m_nodeSearchSelected = 0;
|
||||
|
||||
/// 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;
|
||||
float m_previewWidth = 320.0f;
|
||||
|
||||
/// 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
|
||||
|
||||
Reference in New Issue
Block a user