Style material editor node properties like PropertyPanel

Same table anatomy as the attribute inspector: authored-state (a)
mini-button with path/type tooltip and copy-path popup, fixed name
column, stretch value column with row striping. Widgets switch to the
same vocabulary (InputFloat/InputScalarN %.4f, InputInt, default
ColorEdit swatches) committing on deactivation, and connected inputs
render as '-> node.output'. Preview column widens to 320 to fit; the
shader ball centers in it. Per-edit undo commands are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 23:09:05 +08:00
parent 09819091c4
commit b08bb6b233
+110 -32
View File
@@ -237,7 +237,9 @@ void MaterialEditorPanel::Render() {
ImGui::SameLine();
ImGui::BeginChild("MaterialCanvasRegion", ImVec2(-220.0f, 0.0f), 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);
RenderNodeGraphCanvas();
ImGui::EndChild();
@@ -274,6 +276,8 @@ void MaterialEditorPanel::RenderPreviewPanel() {
const float size = 200.0f;
uint32_t texId = m_preview.Render(static_cast<int>(size), static_cast<int>(size));
ImGui::SetCursorPosX(std::max(0.0f, (ImGui::GetContentRegionAvail().x - size) * 0.5f)
+ ImGui::GetCursorPosX());
if (texId != 0) {
ImGui::Image(ImTextureID(static_cast<ImU64>(texId)), ImVec2(size, size), ImVec2(0, 1), ImVec2(1, 0));
if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
@@ -314,6 +318,30 @@ static pxr::VtValue ReadInputValue(const pxr::UsdPrim& prim, const std::string&
return value;
}
// Mirrors PropertyPanel's file-local mini-button (usdtweak-style "(a)" dot):
// coloured yellow when authored, grey otherwise; tooltip shows path + type,
// left-click offers "Copy path".
static void DrawInputMiniButton(const char* label, bool authored,
const char* pathForCopy, const char* typeHint) {
ImVec4 btnColor = authored
? ImVec4(1.f, 0.85f, 0.4f, 1.f) // yellow authored
: ImVec4(0.5f, 0.5f, 0.5f, 1.f); // grey Sdr default / no opinion
ImGui::PushStyleColor(ImGuiCol_Text, btnColor);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::AlignTextToFramePadding();
ImGui::SmallButton(label);
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s\n%s", pathForCopy, typeHint);
if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonLeft)) {
if (ImGui::MenuItem("Copy path"))
ImGui::SetClipboardText(pathForCopy);
ImGui::EndPopup();
}
}
void MaterialEditorPanel::RenderSelectedNodeProperties() {
if (!m_stage || m_selectedNodePath.IsEmpty()) {
ImGui::TextDisabled("No node selected");
@@ -333,15 +361,51 @@ void MaterialEditorPanel::RenderSelectedNodeProperties() {
ImGui::TextDisabled("%s", node->shaderId.c_str());
ImGui::Spacing();
// Same table anatomy as PropertyPanel's RenderAttrRelSubTable: authored
// dot / fixed name column / stretch value column, with row striping.
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##nodePropTable", 3, kFlags)) return;
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f);
ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, 110.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
const float rowH = ImGui::GetFrameHeight();
for (const auto& input : node->inputs) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
ImGui::PushID(input.name.c_str());
pxr::UsdAttribute attr = prim.GetAttribute(pxr::TfToken("inputs:" + input.name));
ImGui::TableSetColumnIndex(0);
DrawInputMiniButton("(a)", attr && attr.IsAuthored(),
(node->path.GetString() + ".inputs:" + input.name).c_str(),
input.typeName.GetAsToken().GetText());
ImGui::TableSetColumnIndex(1);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(input.name.c_str());
if (IsPinLinked(node->path, input.name, false)) {
ImGui::TextDisabled(" (connected)");
continue;
if (ImGui::IsItemHovered()) // 110px column truncates long mtlx names
ImGui::SetTooltip("%s", input.name.c_str());
ImGui::TableSetColumnIndex(2);
const ShaderGraphLink* link = nullptr;
for (const auto& l : m_graph.links)
if (l.destNode == node->path && l.destInput == input.name) { link = &l; break; }
if (link) {
// Connected input: source display, like PropertyPanel's "-> path".
ImGui::AlignTextToFramePadding();
std::string src = link->sourceNode.GetName() + "." + link->sourceOutput;
ImGui::TextDisabled("-> %s", src.c_str());
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s.outputs:%s", link->sourceNode.GetText(), link->sourceOutput.c_str());
} else {
ImGui::PushItemWidth(-FLT_MIN);
RenderInputValueWidget(*node, input, prim);
ImGui::PopItemWidth();
}
ImGui::SetNextItemWidth(-FLT_MIN);
RenderInputValueWidget(*node, input, prim);
ImGui::PopID();
}
ImGui::EndTable();
}
void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
@@ -354,11 +418,12 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
bool authored = false;
pxr::VtValue current = ReadInputValue(prim, node.shaderId, input, &authored);
// Convention for every widget below: apply the value live while the
// widget is being edited (no command — a drag would flood undo), stash
// the pre-edit state on activation, push one undoable command when the
// edit ends (IsItemDeactivatedAfterEdit).
pxr::VtValue newValue;
// Widget vocabulary mirrors PropertyPanel's DrawVtValueWidget (Input*
// fields with %.4f, applied on IsItemDeactivatedAfterEdit; ColorEdit
// applied live) — plus this panel's undo convention: pre-edit state is
// stashed on activation and one undoable command is pushed per edit.
pxr::VtValue newValue; // applied live (colors, checkbox) as it changes
pxr::VtValue finalValue; // applied + committed on deactivate (Input* fields)
auto scalarAsFloat = [&current]() -> float {
if (current.IsHolding<float>()) return current.UncheckedGet<float>();
@@ -371,37 +436,44 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
if (type == tn->Color3f) {
pxr::GfVec3f v = current.IsHolding<pxr::GfVec3f>() ? current.UncheckedGet<pxr::GfVec3f>()
: pxr::GfVec3f(0.0f);
if (ImGui::ColorEdit3(widgetId.c_str(), v.data(), ImGuiColorEditFlags_Float))
if (ImGui::ColorEdit3(widgetId.c_str(), v.data()))
newValue = v;
} else if (type == tn->Color4f) {
pxr::GfVec4f v = current.IsHolding<pxr::GfVec4f>() ? current.UncheckedGet<pxr::GfVec4f>()
: pxr::GfVec4f(0.0f);
if (ImGui::ColorEdit4(widgetId.c_str(), v.data()))
newValue = v;
} else if (type == tn->Float3 || type == tn->Vector3f || type == tn->Normal3f || type == tn->Point3f) {
pxr::GfVec3f v = current.IsHolding<pxr::GfVec3f>() ? current.UncheckedGet<pxr::GfVec3f>()
: pxr::GfVec3f(0.0f);
if (ImGui::DragFloat3(widgetId.c_str(), v.data(), 0.01f))
newValue = v;
ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 3, nullptr, nullptr, "%.4f");
if (ImGui::IsItemDeactivatedAfterEdit())
finalValue = v;
} else if (type == tn->Float2 || type == tn->TexCoord2f) {
pxr::GfVec2f v = current.IsHolding<pxr::GfVec2f>() ? current.UncheckedGet<pxr::GfVec2f>()
: pxr::GfVec2f(0.0f);
if (ImGui::DragFloat2(widgetId.c_str(), v.data(), 0.01f))
newValue = v;
} else if (type == tn->Float4 || type == tn->Color4f) {
ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 2, nullptr, nullptr, "%.4f");
if (ImGui::IsItemDeactivatedAfterEdit())
finalValue = v;
} else if (type == tn->Float4) {
pxr::GfVec4f v = current.IsHolding<pxr::GfVec4f>() ? current.UncheckedGet<pxr::GfVec4f>()
: pxr::GfVec4f(0.0f);
bool changed = (type == tn->Color4f)
? ImGui::ColorEdit4(widgetId.c_str(), v.data(), ImGuiColorEditFlags_Float)
: ImGui::DragFloat4(widgetId.c_str(), v.data(), 0.01f);
if (changed)
newValue = v;
ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 4, nullptr, nullptr, "%.4f");
if (ImGui::IsItemDeactivatedAfterEdit())
finalValue = v;
} else if (type == tn->Float || type == tn->Double || type == tn->Half) {
float f = scalarAsFloat();
if (ImGui::DragFloat(widgetId.c_str(), &f, 0.01f)) {
if (type == tn->Double) newValue = static_cast<double>(f);
else if (type == tn->Half) newValue = pxr::GfHalf(f);
else newValue = f;
ImGui::InputFloat(widgetId.c_str(), &f, 0.f, 0.f, "%.4f");
if (ImGui::IsItemDeactivatedAfterEdit()) {
if (type == tn->Double) finalValue = static_cast<double>(f);
else if (type == tn->Half) finalValue = pxr::GfHalf(f);
else finalValue = f;
}
} else if (type == tn->Int) {
int v = current.IsHolding<int>() ? current.UncheckedGet<int>() : 0;
if (ImGui::DragInt(widgetId.c_str(), &v))
newValue = v;
ImGui::InputInt(widgetId.c_str(), &v);
if (ImGui::IsItemDeactivatedAfterEdit())
finalValue = v;
} else if (type == tn->Bool) {
bool v = current.IsHolding<bool>() && current.UncheckedGet<bool>();
if (ImGui::Checkbox(widgetId.c_str(), &v))
@@ -419,7 +491,8 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
else newValue = pxr::SdfAssetPath(buf);
}
} else {
ImGui::TextDisabled(" %s (unsupported)", type.GetAsToken().GetText());
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("%s", type.GetAsToken().GetText());
return;
}
@@ -427,11 +500,16 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
m_preEditValue = current;
m_preEditWasAuthored = authored;
}
if (!newValue.IsEmpty()) {
auto setValue = [&](const pxr::VtValue& v) {
pxr::UsdShadeShader shader(prim);
if (shader)
shader.CreateInput(pxr::TfToken(input.name), input.typeName).Set(newValue);
}
shader.CreateInput(pxr::TfToken(input.name), input.typeName).Set(v);
};
if (!newValue.IsEmpty())
setValue(newValue);
if (!finalValue.IsEmpty())
setValue(finalValue);
if (ImGui::IsItemDeactivatedAfterEdit())
CommitInputEdit(node, input);
}