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::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();
|
RenderNodeGraphCanvas();
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
@@ -274,6 +276,8 @@ void MaterialEditorPanel::RenderPreviewPanel() {
|
|||||||
|
|
||||||
const float size = 200.0f;
|
const float size = 200.0f;
|
||||||
uint32_t texId = m_preview.Render(static_cast<int>(size), static_cast<int>(size));
|
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) {
|
if (texId != 0) {
|
||||||
ImGui::Image(ImTextureID(static_cast<ImU64>(texId)), ImVec2(size, size), ImVec2(0, 1), ImVec2(1, 0));
|
ImGui::Image(ImTextureID(static_cast<ImU64>(texId)), ImVec2(size, size), ImVec2(0, 1), ImVec2(1, 0));
|
||||||
if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
||||||
@@ -314,6 +318,30 @@ static pxr::VtValue ReadInputValue(const pxr::UsdPrim& prim, const std::string&
|
|||||||
return value;
|
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() {
|
void MaterialEditorPanel::RenderSelectedNodeProperties() {
|
||||||
if (!m_stage || m_selectedNodePath.IsEmpty()) {
|
if (!m_stage || m_selectedNodePath.IsEmpty()) {
|
||||||
ImGui::TextDisabled("No node selected");
|
ImGui::TextDisabled("No node selected");
|
||||||
@@ -333,15 +361,51 @@ void MaterialEditorPanel::RenderSelectedNodeProperties() {
|
|||||||
ImGui::TextDisabled("%s", node->shaderId.c_str());
|
ImGui::TextDisabled("%s", node->shaderId.c_str());
|
||||||
ImGui::Spacing();
|
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) {
|
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());
|
ImGui::TextUnformatted(input.name.c_str());
|
||||||
if (IsPinLinked(node->path, input.name, false)) {
|
if (ImGui::IsItemHovered()) // 110px column truncates long mtlx names
|
||||||
ImGui::TextDisabled(" (connected)");
|
ImGui::SetTooltip("%s", input.name.c_str());
|
||||||
continue;
|
|
||||||
|
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);
|
ImGui::PopID();
|
||||||
RenderInputValueWidget(*node, input, prim);
|
|
||||||
}
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
||||||
@@ -354,11 +418,12 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
|||||||
bool authored = false;
|
bool authored = false;
|
||||||
pxr::VtValue current = ReadInputValue(prim, node.shaderId, input, &authored);
|
pxr::VtValue current = ReadInputValue(prim, node.shaderId, input, &authored);
|
||||||
|
|
||||||
// Convention for every widget below: apply the value live while the
|
// Widget vocabulary mirrors PropertyPanel's DrawVtValueWidget (Input*
|
||||||
// widget is being edited (no command — a drag would flood undo), stash
|
// fields with %.4f, applied on IsItemDeactivatedAfterEdit; ColorEdit
|
||||||
// the pre-edit state on activation, push one undoable command when the
|
// applied live) — plus this panel's undo convention: pre-edit state is
|
||||||
// edit ends (IsItemDeactivatedAfterEdit).
|
// stashed on activation and one undoable command is pushed per edit.
|
||||||
pxr::VtValue newValue;
|
pxr::VtValue newValue; // applied live (colors, checkbox) as it changes
|
||||||
|
pxr::VtValue finalValue; // applied + committed on deactivate (Input* fields)
|
||||||
|
|
||||||
auto scalarAsFloat = [¤t]() -> float {
|
auto scalarAsFloat = [¤t]() -> float {
|
||||||
if (current.IsHolding<float>()) return current.UncheckedGet<float>();
|
if (current.IsHolding<float>()) return current.UncheckedGet<float>();
|
||||||
@@ -371,37 +436,44 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
|||||||
if (type == tn->Color3f) {
|
if (type == tn->Color3f) {
|
||||||
pxr::GfVec3f v = current.IsHolding<pxr::GfVec3f>() ? current.UncheckedGet<pxr::GfVec3f>()
|
pxr::GfVec3f v = current.IsHolding<pxr::GfVec3f>() ? current.UncheckedGet<pxr::GfVec3f>()
|
||||||
: pxr::GfVec3f(0.0f);
|
: 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;
|
newValue = v;
|
||||||
} else if (type == tn->Float3 || type == tn->Vector3f || type == tn->Normal3f || type == tn->Point3f) {
|
} 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 v = current.IsHolding<pxr::GfVec3f>() ? current.UncheckedGet<pxr::GfVec3f>()
|
||||||
: pxr::GfVec3f(0.0f);
|
: pxr::GfVec3f(0.0f);
|
||||||
if (ImGui::DragFloat3(widgetId.c_str(), v.data(), 0.01f))
|
ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 3, nullptr, nullptr, "%.4f");
|
||||||
newValue = v;
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||||
|
finalValue = v;
|
||||||
} else if (type == tn->Float2 || type == tn->TexCoord2f) {
|
} else if (type == tn->Float2 || type == tn->TexCoord2f) {
|
||||||
pxr::GfVec2f v = current.IsHolding<pxr::GfVec2f>() ? current.UncheckedGet<pxr::GfVec2f>()
|
pxr::GfVec2f v = current.IsHolding<pxr::GfVec2f>() ? current.UncheckedGet<pxr::GfVec2f>()
|
||||||
: pxr::GfVec2f(0.0f);
|
: pxr::GfVec2f(0.0f);
|
||||||
if (ImGui::DragFloat2(widgetId.c_str(), v.data(), 0.01f))
|
ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 2, nullptr, nullptr, "%.4f");
|
||||||
newValue = v;
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||||
} else if (type == tn->Float4 || type == tn->Color4f) {
|
finalValue = v;
|
||||||
|
} else if (type == tn->Float4) {
|
||||||
pxr::GfVec4f v = current.IsHolding<pxr::GfVec4f>() ? current.UncheckedGet<pxr::GfVec4f>()
|
pxr::GfVec4f v = current.IsHolding<pxr::GfVec4f>() ? current.UncheckedGet<pxr::GfVec4f>()
|
||||||
: pxr::GfVec4f(0.0f);
|
: pxr::GfVec4f(0.0f);
|
||||||
bool changed = (type == tn->Color4f)
|
ImGui::InputScalarN(widgetId.c_str(), ImGuiDataType_Float, v.data(), 4, nullptr, nullptr, "%.4f");
|
||||||
? ImGui::ColorEdit4(widgetId.c_str(), v.data(), ImGuiColorEditFlags_Float)
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||||
: ImGui::DragFloat4(widgetId.c_str(), v.data(), 0.01f);
|
finalValue = v;
|
||||||
if (changed)
|
|
||||||
newValue = v;
|
|
||||||
} else if (type == tn->Float || type == tn->Double || type == tn->Half) {
|
} else if (type == tn->Float || type == tn->Double || type == tn->Half) {
|
||||||
float f = scalarAsFloat();
|
float f = scalarAsFloat();
|
||||||
if (ImGui::DragFloat(widgetId.c_str(), &f, 0.01f)) {
|
ImGui::InputFloat(widgetId.c_str(), &f, 0.f, 0.f, "%.4f");
|
||||||
if (type == tn->Double) newValue = static_cast<double>(f);
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||||
else if (type == tn->Half) newValue = pxr::GfHalf(f);
|
if (type == tn->Double) finalValue = static_cast<double>(f);
|
||||||
else newValue = f;
|
else if (type == tn->Half) finalValue = pxr::GfHalf(f);
|
||||||
|
else finalValue = f;
|
||||||
}
|
}
|
||||||
} else if (type == tn->Int) {
|
} else if (type == tn->Int) {
|
||||||
int v = current.IsHolding<int>() ? current.UncheckedGet<int>() : 0;
|
int v = current.IsHolding<int>() ? current.UncheckedGet<int>() : 0;
|
||||||
if (ImGui::DragInt(widgetId.c_str(), &v))
|
ImGui::InputInt(widgetId.c_str(), &v);
|
||||||
newValue = v;
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||||
|
finalValue = v;
|
||||||
} else if (type == tn->Bool) {
|
} else if (type == tn->Bool) {
|
||||||
bool v = current.IsHolding<bool>() && current.UncheckedGet<bool>();
|
bool v = current.IsHolding<bool>() && current.UncheckedGet<bool>();
|
||||||
if (ImGui::Checkbox(widgetId.c_str(), &v))
|
if (ImGui::Checkbox(widgetId.c_str(), &v))
|
||||||
@@ -419,7 +491,8 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
|||||||
else newValue = pxr::SdfAssetPath(buf);
|
else newValue = pxr::SdfAssetPath(buf);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextDisabled(" %s (unsupported)", type.GetAsToken().GetText());
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextDisabled("%s", type.GetAsToken().GetText());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,11 +500,16 @@ void MaterialEditorPanel::RenderInputValueWidget(const ShaderGraphNode& node,
|
|||||||
m_preEditValue = current;
|
m_preEditValue = current;
|
||||||
m_preEditWasAuthored = authored;
|
m_preEditWasAuthored = authored;
|
||||||
}
|
}
|
||||||
if (!newValue.IsEmpty()) {
|
|
||||||
|
auto setValue = [&](const pxr::VtValue& v) {
|
||||||
pxr::UsdShadeShader shader(prim);
|
pxr::UsdShadeShader shader(prim);
|
||||||
if (shader)
|
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())
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||||
CommitInputEdit(node, input);
|
CommitInputEdit(node, input);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user