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:
2026-07-05 06:47:12 +08:00
parent b95b567b75
commit 42fd355c53
4 changed files with 173 additions and 19 deletions
+84 -1
View File
@@ -1,5 +1,7 @@
#include "MaterialManager.h"
#include <pxr/usd/sdr/registry.h>
#include <pxr/usd/sdr/shaderProperty.h>
#include <pxr/base/tf/stringUtils.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdShade/material.h>
#include <pxr/usd/usdShade/shader.h>
@@ -9,10 +11,84 @@
#include <pxr/base/tf/token.h>
#include <pxr/base/vt/value.h>
#include <algorithm>
#include <cctype>
#include <set>
namespace UsdLayerManager {
namespace {
// Coarse Hypershade-style bucket for the create-node list. Sdr metadata is
// parser-dependent (glslfx vs MaterialX vs Arnold vs Cycles fill different
// fields), so classify from the strongest signal available: context first,
// then terminal output types, then role, then name keywords.
std::string DeriveCategory(pxr::SdrShaderNodeConstPtr node) {
const std::string context = pxr::TfStringify(node->GetContext());
if (context == "surface" || context == "volume" || context == "displacement")
return "Material";
if (context == "light" || context == "lightFilter")
return "Light";
// MaterialX/Arnold terminals often lack a context but type their outputs
// as surfaceshader/volumeshader/displacementshader/closure.
for (const pxr::TfToken& outName : node->GetShaderOutputNames()) {
if (const auto* prop = node->GetShaderOutput(outName)) {
const std::string outType = pxr::TfStringify(prop->GetType());
if (outType.find("shader") != std::string::npos ||
outType == "terminal" || outType == "closure")
return "Material";
}
}
const std::string role = pxr::TfStringify(node->GetRole());
if (role == "texture") return "Texture";
if (role == "primvar") return "Geometry";
if (role == "math") return "Utility";
const std::string name = pxr::TfStringToLower(
node->GetIdentifier().GetString() + " " + node->GetName());
for (const char* kw : {"texture", "image", "checker", "noise", "ramp",
"fractal", "worley", "voronoi", "triplanar"})
if (name.find(kw) != std::string::npos) return "Texture";
for (const char* kw : {"primvar", "texcoord", "geomprop", "position",
"normal", "tangent", "bitangent", "uv_"})
if (name.find(kw) != std::string::npos) return "Geometry";
return "Utility";
}
// Fixed display order for the known buckets; anything unexpected sorts after.
int CategoryRank(const std::string& category) {
if (category == "Material") return 0;
if (category == "Texture") return 1;
if (category == "Geometry") return 2;
if (category == "Utility") return 3;
if (category == "Light") return 4;
return 5;
}
// Top-level group from the Sdr source type. glslfx and OSL collapse into one
// "USD" group — the core usd* nodes register under both parsers and would
// otherwise show up twice under different headers.
std::string DeriveSource(pxr::SdrShaderNodeConstPtr node) {
std::string st = pxr::TfStringToLower(pxr::TfStringify(node->GetSourceType()));
if (st == "glslfx" || st == "osl") return "USD";
if (st == "mtlx") return "MaterialX";
if (st == "arnold") return "Arnold";
if (st == "cycles") return "Cycles";
if (st.empty()) return "Other";
st[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(st[0])));
return st;
}
int SourceRank(const std::string& source) {
if (source == "USD") return 0;
if (source == "MaterialX") return 1;
return 2; // renderers and anything else, alphabetical among themselves
}
} // namespace
const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes() {
if (m_shaderNodeCacheBuilt)
return m_shaderNodeCache;
@@ -24,12 +100,19 @@ const std::vector<ShaderNodeTypeInfo>& MaterialManager::GetAvailableShaderNodes(
info.identifier = node->GetIdentifier().GetString();
info.label = !node->GetLabel().IsEmpty() ? node->GetLabel().GetString() : node->GetName();
info.family = node->GetFamily().GetString();
info.category = DeriveCategory(node);
info.source = DeriveSource(node);
m_shaderNodeCache.push_back(std::move(info));
}
std::sort(m_shaderNodeCache.begin(), m_shaderNodeCache.end(),
[](const ShaderNodeTypeInfo& a, const ShaderNodeTypeInfo& b) {
if (a.family != b.family) return a.family < b.family;
const int sa = SourceRank(a.source), sb = SourceRank(b.source);
if (sa != sb) return sa < sb;
if (a.source != b.source) return a.source < b.source;
const int ra = CategoryRank(a.category), rb = CategoryRank(b.category);
if (ra != rb) return ra < rb;
if (a.category != b.category) return a.category < b.category;
return a.label < b.label;
});
+9 -1
View File
@@ -15,7 +15,15 @@ namespace UsdLayerManager {
struct ShaderNodeTypeInfo {
std::string identifier; ///< Sdr identifier, authored as info:id
std::string label; ///< display label for the create-node menu
std::string family; ///< grouping for the create-node menu (may be empty)
std::string family; ///< Sdr family (parser-provided, may be empty)
/// Coarse Hypershade-style bucket ("Material", "Texture", "Geometry",
/// "Utility", "Light") derived from Sdr context/role/output types; the
/// create-node list groups by it. Never empty.
std::string category;
/// Top-level create-list group derived from the Sdr source type:
/// "USD" (glslfx/OSL core nodes), "MaterialX", "Arnold", "Cycles", or a
/// capitalized raw source type. Never empty.
std::string source;
};
/// One input or output pin on a shader node, with its USD value type so