Icon toolbar and resizable browser lists in material editor; mute debug logs

Toolbar: every action is now an icon button (tooltip carries the label,
text fallback with no icon set) via a local iconBtn helper matching the
StageEditor/Timeline convention — Create Material, Presets, Create + Bind,
Bind Current. Icons fill the standard button height (GetFrameHeight, zero
frame padding) so the row stays uniform.

Browser: the materials list and the create-node list are now split by a
draggable HorizontalSplitter (sibling to the column VerticalSplitter)
instead of a fixed 45% / separator; m_browserListRatio holds the list's
share (clamped 0.15-0.85). Create-node entries get a per-category glyph
(Material/Texture/Geometry/Light/Utility) sized to the text line height.

Silence the temporary node-editor drag-regression tracing: the
[NodeGraph] event-trace block is #if 0'd, its PersistNodePosition logs
commented, and the [ed] g_AxNodeEditorDebugLog hook install is commented
out (the vendored editor's dumps are guarded on that hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:16:04 +08:00
parent 038450a1ca
commit 2ddeeed061
2 changed files with 92 additions and 30 deletions
+83 -26
View File
@@ -182,6 +182,37 @@ void VerticalSplitter(const char* id, float* width, float minWidth, float maxWid
ImGui::SameLine(0.0f, 0.0f); ImGui::SameLine(0.0f, 0.0f);
} }
// Horizontal drag-splitter stacked between two vertically-arranged panels.
// Adjusts *ratio (fraction of totalHeight given to the panel above) by the
// mouse drag; colours match VerticalSplitter.
void HorizontalSplitter(const char* id, float* ratio, float totalHeight,
float minRatio, float maxRatio) {
const float width = std::max(1.0f, ImGui::GetContentRegionAvail().x);
ImGui::InvisibleButton(id, ImVec2(width, 6.0f));
const bool hovered = ImGui::IsItemHovered();
const bool active = ImGui::IsItemActive();
if (hovered || active)
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
if (active && totalHeight > 1.0f)
*ratio = std::clamp(*ratio + ImGui::GetIO().MouseDelta.y / totalHeight, minRatio, maxRatio);
ImVec2 rectMin = ImGui::GetItemRectMin();
ImVec2 rectMax = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddRectFilled(
ImVec2(rectMin.x, rectMin.y + 2.0f), ImVec2(rectMax.x, rectMax.y - 2.0f),
(hovered || active) ? IM_COL32(250, 150, 66, 200) : IM_COL32(80, 80, 80, 180));
}
// Small glyph for a create-node list entry, keyed by the derived category
// (see MaterialManager::DeriveCategory: Material/Texture/Geometry/Light/Utility).
Icon IconForNodeCategory(const std::string& category) {
if (category == "Material") return Icon::Swatchbook;
if (category == "Texture") return Icon::Grid;
if (category == "Geometry") return Icon::Cube;
if (category == "Light") return Icon::Lightbulb;
if (category == "Utility") return Icon::Code;
return Icon::CircleDot;
}
// Networks referenced from .mtlx (or authored elsewhere) carry no uiPosition // 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 // 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 // layered left-to-right positions instead: each node sits one column left of
@@ -227,8 +258,9 @@ void ApplyFallbackLayout(ShaderGraphSnapshot& graph) {
MaterialEditorPanel::MaterialEditorPanel() { MaterialEditorPanel::MaterialEditorPanel() {
// Route the vendored editor's click-time hit-test dump into our log so // Route the vendored editor's click-time hit-test dump into our log so
// it interleaves with the [NodeGraph] event trace. // it interleaves with the [NodeGraph] event trace. Disabled: uncomment to
g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); }; // re-enable the [ed] trace while diagnosing the drag regression.
// g_AxNodeEditorDebugLog = [](const char* msg) { LOG_INFO(std::string(msg)); };
NE::Config config; NE::Config config;
config.SettingsFile = nullptr; // layout persists as USD uiPosition custom data, not an on-disk .json config.SettingsFile = nullptr; // layout persists as USD uiPosition custom data, not an on-disk .json
@@ -612,25 +644,34 @@ void MaterialEditorPanel::CommitInputEdit(const ShaderGraphNode& node, const Sha
} }
void MaterialEditorPanel::RenderToolbar() { void MaterialEditorPanel::RenderToolbar() {
// Create Material icon button // Every toolbar action is an icon button; the tooltip carries the label.
bool createClicked; // The icon fills the full button height (standard frame height, zero frame
// padding) so the buttons stay a uniform, standard-row-height square. Falls
// back to a text button only when no icon set is loaded.
const float btnH = ImGui::GetFrameHeight();
auto iconBtn = [&](const char* id, Icon icon, const char* fallback, const char* tip) -> bool {
bool clicked;
if (m_iconManager) { if (m_iconManager) {
createClicked = ImGui::ImageButton("##createMaterial", ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.f, 0.f));
ImTextureRef(m_iconManager->Get(Icon::FilePlus)), clicked = ImGui::ImageButton(id, ImTextureRef(m_iconManager->Get(icon)),
ImVec2(18.f, 18.f)); ImVec2(btnH, btnH));
ImGui::PopStyleVar();
} else { } else {
createClicked = ImGui::Button("Create Material"); clicked = ImGui::Button(fallback);
} }
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Create Material"); ImGui::SetTooltip("%s", tip);
if (createClicked) return clicked;
};
if (iconBtn("##createMaterial", Icon::FilePlus, "Create Material", "Create Material"))
CreateNewMaterial(); CreateNewMaterial();
// Presets menu: entries appear per node source when the required shader // Presets menu: entries appear per node source when the required shader
// definitions are registered, and need an open material to apply to. // definitions are registered, and need an open material to apply to.
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(m_materialPath.IsEmpty()); ImGui::BeginDisabled(m_materialPath.IsEmpty());
if (ImGui::Button("Presets")) if (iconBtn("##presets", Icon::Swatchbook, "Presets", "Presets"))
ImGui::OpenPopup("MaterialPresets"); ImGui::OpenPopup("MaterialPresets");
ImGui::EndDisabled(); ImGui::EndDisabled();
if (ImGui::BeginPopup("MaterialPresets")) { if (ImGui::BeginPopup("MaterialPresets")) {
@@ -657,11 +698,13 @@ void MaterialEditorPanel::RenderToolbar() {
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("Selected: %s", m_targetPrimPath.GetText()); ImGui::Text("Selected: %s", m_targetPrimPath.GetText());
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Create + Bind Material")) if (iconBtn("##createBind", Icon::ObjectGroup, "Create + Bind",
"Create + Bind Material to Selected"))
CreateAndBindMaterialForTarget(); CreateAndBindMaterialForTarget();
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(m_materialPath.IsEmpty()); ImGui::BeginDisabled(m_materialPath.IsEmpty());
if (ImGui::Button("Bind Current Material to Selected")) if (iconBtn("##bindCurrent", Icon::Link, "Bind Current",
"Bind Current Material to Selected"))
BindMaterialToTarget(m_materialPath, m_targetPrimPath); BindMaterialToTarget(m_materialPath, m_targetPrimPath);
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
@@ -698,9 +741,12 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
std::find(materials.begin(), materials.end(), m_browserSelection) == materials.end()) std::find(materials.begin(), materials.end(), m_browserSelection) == materials.end())
m_browserSelection = pxr::SdfPath(); m_browserSelection = pxr::SdfPath();
// Top ~45%: materials list. Below: persistent searchable create-node // Materials list (top) and the persistent searchable create-node list
// list (Hypershade's create bar, with Nuke-style filtering). // (bottom, Hypershade's create bar with Nuke-style filtering), split by a
ImGui::BeginChild("MaterialBrowserList", ImVec2(0.0f, ImGui::GetContentRegionAvail().y * 0.45f), false); // draggable horizontal splitter; m_browserListRatio is the list's share.
const float browserAvail = ImGui::GetContentRegionAvail().y;
const float listHeight = std::max(1.0f, browserAvail * m_browserListRatio);
ImGui::BeginChild("MaterialBrowserList", ImVec2(0.0f, listHeight), false);
for (const pxr::SdfPath& path : materials) { for (const pxr::SdfPath& path : materials) {
const bool isSelected = (path == m_browserSelection); const bool isSelected = (path == m_browserSelection);
const bool isOpen = (path == m_materialPath); const bool isOpen = (path == m_materialPath);
@@ -721,7 +767,8 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
} }
ImGui::EndChild(); ImGui::EndChild();
ImGui::Separator(); HorizontalSplitter("##BrowserListSplitter", &m_browserListRatio, browserAvail, 0.15f, 0.85f);
ImGui::TextUnformatted("Create Node"); ImGui::TextUnformatted("Create Node");
ImGui::SetNextItemWidth(-FLT_MIN); ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputTextWithHint("##BrowserNodeSearch", "search...", ImGui::InputTextWithHint("##BrowserNodeSearch", "search...",
@@ -773,6 +820,12 @@ void MaterialEditorPanel::RenderMaterialBrowser() {
} }
if (!categoryOpen) if (!categoryOpen)
continue; continue;
if (m_iconManager) {
const float h = ImGui::GetTextLineHeight();
ImGui::Image(ImTextureRef(m_iconManager->Get(IconForNodeCategory(info->category))),
ImVec2(h, h));
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
}
std::string itemLabel = info->label + "##" + std::to_string(i); std::string itemLabel = info->label + "##" + std::to_string(i);
if (ImGui::Selectable(itemLabel.c_str(), false)) if (ImGui::Selectable(itemLabel.c_str(), false))
m_pendingCreateShaderId = info->identifier; // created at view center next canvas pass m_pendingCreateShaderId = info->identifier; // created at view center next canvas pass
@@ -1225,6 +1278,9 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
// event logs once, not per frame. Event order in the editor is: // event logs once, not per frame. Event order in the editor is:
// LMB press -> node becomes active (and is brought to front) -> drag // LMB press -> node becomes active (and is brought to front) -> drag
// moves it -> release -> "click" -> selection updates -> position save. // moves it -> release -> "click" -> selection updates -> position save.
// Disabled: flip to #if 1 to re-enable the [NodeGraph] trace while
// diagnosing the node-editor drag regression.
#if 0
{ {
// Logs the mouse position and every node's editor rect with a hit // Logs the mouse position and every node's editor rect with a hit
// verdict, and returns the hit paths. Topmost is unknown to us, but // verdict, and returns the hit paths. Topmost is unknown to us, but
@@ -1378,6 +1434,7 @@ void MaterialEditorPanel::RenderNodeGraphCanvas() {
LOG_INFO(msg); LOG_INFO(msg);
} }
} }
#endif
// Mirror the editor's node selection (first selected node) for the // Mirror the editor's node selection (first selected node) for the
// properties section under the preview. // properties section under the preview.
@@ -1611,8 +1668,8 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) {
for (const auto& node : m_graph.nodes) { for (const auto& node : m_graph.nodes) {
if (node.path == path) { if (node.path == path) {
if (node.uiPosition == newValue) { if (node.uiPosition == newValue) {
LOG_INFO("[NodeGraph] position save for " + path.GetString() + // LOG_INFO("[NodeGraph] position save for " + path.GetString() +
" ignored (echo of snapshot seed)"); // " ignored (echo of snapshot seed)");
return; return;
} }
break; break;
@@ -1625,16 +1682,16 @@ void MaterialEditorPanel::PersistNodePosition(NE::NodeId nodeId) {
: pxr::GfVec2f(0.0f, 0.0f); : pxr::GfVec2f(0.0f, 0.0f);
if (oldValue == newValue) { if (oldValue == newValue) {
LOG_INFO("[NodeGraph] position save for " + path.GetString() + // LOG_INFO("[NodeGraph] position save for " + path.GetString() +
" ignored (customData already matches)"); // " ignored (customData already matches)");
return; return;
} }
char moveMsg[256]; // char moveMsg[256];
std::snprintf(moveMsg, sizeof(moveMsg), // std::snprintf(moveMsg, sizeof(moveMsg),
"[NodeGraph] move committed: %s (%.1f, %.1f) -> (%.1f, %.1f)", // "[NodeGraph] move committed: %s (%.1f, %.1f) -> (%.1f, %.1f)",
path.GetText(), oldValue[0], oldValue[1], newValue[0], newValue[1]); // path.GetText(), oldValue[0], oldValue[1], newValue[0], newValue[1]);
LOG_INFO(std::string(moveMsg)); // LOG_INFO(std::string(moveMsg));
pxr::UsdStageRefPtr stage = m_stage; pxr::UsdStageRefPtr stage = m_stage;
m_commandHistory->Push(std::make_unique<AttributeSetCommand>( m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
+5
View File
@@ -157,6 +157,11 @@ private:
float m_browserWidth = 220.0f; float m_browserWidth = 220.0f;
float m_previewWidth = 320.0f; float m_previewWidth = 320.0f;
/// Fraction of the browser column's height given to the materials list;
/// the create-node list takes the rest. User-adjustable via the horizontal
/// splitter between them.
float m_browserListRatio = 0.45f;
/// Search text of the browser's persistent create-node list. /// Search text of the browser's persistent create-node list.
char m_browserNodeSearchBuf[128] = ""; char m_browserNodeSearchBuf[128] = "";
/// Shader id clicked in the browser's create-node list; created at the /// Shader id clicked in the browser's create-node list; created at the