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:
+110
-32
@@ -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 = [¤t]() -> 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user