Compare commits

..

2 Commits

Author SHA1 Message Date
indigo 0640201d5b Add keyable attrs, clear override, material binding, and API schema groups to PropertyPanel
Attributes with SdfVariabilityVarying now show a (k) button when the
timeline is at a non-default frame. Orange = exact time sample at the
current frame; gray = no sample here. Clicking keys the current value
via AttributeSetCommand (undoable).

Right-click on any attribute or relationship name opens a context menu
with "Clear Override" (greyed when nothing is authored at the edit
target). Undo restores all previously authored time samples and default
values.

Prims with UsdShadeMaterialBindingAPI now show a "Material Binding"
collapsing section above the transform block, listing Direct / Preview /
Full purpose bindings and the resolved computed material path.

RenderPropertiesTable now groups attributes and relationships under
CollapsingHeader sections named after their USD API schema. Base-type
attributes (e.g. Mesh, Xform) appear first; applied API schema groups
(MaterialBindingAPI, CollectionAPI, etc.) follow alphabetically.
Uncategorised attributes are bucketed by namespace prefix (primvars,
inputs, outputs, …). xformOp attrs are still filtered out (shown in the
TRS section above).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 07:13:21 +08:00
indigo f7a2262e80 Add multi-select, specifier column, and visibility shortcuts to hierarchy panel
Ctrl+click toggles prim selection; Shift+click range-selects using a
per-frame visible-order list. Multi-selection is broadcast to the viewport
via a new SetOnPrimsSelected callback so Hydra highlights all selected prims.

Adds a 5th table column showing each prim's SdfSpecifier (def/over/class)
with colour coding. 'over' prims are shown in orange to make overrides
immediately visible.

Visibility icon updated: invisible prims show EyeSlash in orange instead
of grey. Ctrl+H / Shift+H keyboard shortcuts hide / show all selected prims.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 07:04:22 +08:00
7 changed files with 520 additions and 55 deletions
+5
View File
@@ -115,6 +115,11 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_curveEditorPanel->SetSelectedPrimPath(path);
});
m_sceneHierarchyPanel->SetOnPrimsSelected(
[this](const std::vector<std::string>& paths) {
m_viewportPanel->SetSelectedPrimPaths(paths);
});
m_sceneHierarchyPanel->SetOnStageMetadataChanged(
[this]() {
RefreshManagers();
+359 -35
View File
@@ -1,6 +1,7 @@
#include "PropertyPanel.h"
#include "../utils/Logger.h"
#include "../core/commands/TransformCommand.h"
#include "../core/commands/AttributeSetCommand.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/attribute.h>
@@ -38,8 +39,15 @@
#include <pxr/base/vt/array.h>
#include <pxr/base/tf/token.h>
#include <pxr/usd/sdf/assetPath.h>
#include <pxr/usd/usdShade/materialBindingAPI.h>
#include <pxr/usd/usdShade/tokens.h>
#include <pxr/usd/usd/schemaRegistry.h>
#include <pxr/usd/usd/primDefinition.h>
#include <memory>
#include <sstream>
#include <unordered_map>
#include <vector>
#include <set>
PXR_NAMESPACE_USING_DIRECTIVE
@@ -52,6 +60,8 @@ static const ImVec4 kColorX { 0.86f, 0.30f, 0.30f, 1.f }; // X axis red
static const ImVec4 kColorY { 0.40f, 0.80f, 0.30f, 1.f }; // Y axis green
static const ImVec4 kColorZ { 0.28f, 0.56f, 0.96f, 1.f }; // Z axis blue
static const ImVec4 kColorLabel { 0.80f, 0.80f, 0.80f, 1.f }; // row labels
static const ImVec4 kColorKeyOn { 1.00f, 0.65f, 0.10f, 1.f }; // key exists at editTime
static const ImVec4 kColorKeyOff { 0.35f, 0.35f, 0.35f, 1.f }; // varying, no sample here
// ---------------------------------------------------------------------------
PropertyPanel::PropertyPanel() = default;
@@ -535,6 +545,60 @@ static std::string GetPropDisplayName(const pxr::UsdProperty& prop) {
return ns.empty() ? bn : (ns + ":" + bn);
}
// ---------------------------------------------------------------------------
// Helper: check whether an attribute has an exact time sample at t
// ---------------------------------------------------------------------------
static bool HasExactTimeSample(const pxr::UsdAttribute& attr, double t) {
double lo = 0.0, hi = 0.0;
bool has = false;
attr.GetBracketingTimeSamples(t, &lo, &hi, &has);
return has && (lo == t) && (hi == t);
}
// ---------------------------------------------------------------------------
// Helper: small "(k)" key button for animatable (Varying) attributes.
// Orange = exact time sample at editTime; dim gray = no sample at this frame.
// Click = Set(currentValue, editTime) with undo.
// ---------------------------------------------------------------------------
static void DrawKeyButton(pxr::UsdAttribute attr,
pxr::UsdTimeCode editTime,
UsdLayerManager::CommandHistory* cmdHistory)
{
using namespace pxr;
double t = editTime.GetValue();
bool exactSample = HasExactTimeSample(attr, t);
ImVec4 col = exactSample ? kColorKeyOn : kColorKeyOff;
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
bool clicked = ImGui::SmallButton("(k)");
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Set key at frame %.3g", t);
if (!clicked || !cmdHistory) return;
VtValue val;
if (!attr.Get(&val, editTime)) return;
std::function<void()> executeFn = [attr, val, editTime]() mutable {
attr.Set(val, editTime);
};
std::function<void()> undoFn;
if (exactSample) {
// overwrite: undo restores the same value that was there before
VtValue prev;
attr.Get(&prev, editTime);
undoFn = [attr, prev, editTime]() mutable { attr.Set(prev, editTime); };
} else {
undoFn = [attr, editTime]() mutable { attr.ClearAtTime(editTime.GetValue()); };
}
cmdHistory->Push(std::make_unique<AttributeSetCommand>(
"Set Key", std::move(executeFn), std::move(undoFn)));
}
// ---------------------------------------------------------------------------
// Helper: mini-button (SmallButton coloured by authored state, like usdtweak)
// Opens a popup context with "Copy path" on left-click.
@@ -1109,35 +1173,112 @@ void PropertyPanel::RenderVariantSetsSection(const pxr::UsdPrim& prim) {
}
// ---------------------------------------------------------------------------
// Unified properties table (attributes + relationships)
//
// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties:
// • Flags: SizingFixedFit | RowBg — NO borders, NO header row
// • Col 0 (fixed ~20px) : SmallButton mini-button coloured by authored state
// • Col 1 (fixed 140px) : property display name (namespace:basename)
// • Col 2 (stretch) : type-aware value widget, PushItemWidth(-FLT_MIN)
// Material binding section
// ---------------------------------------------------------------------------
void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
// Exact flags used by usdtweak no borders, alternating row background
constexpr ImGuiTableFlags kFlags =
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
void PropertyPanel::RenderMaterialBindSection(const pxr::UsdPrim& prim) {
using namespace pxr;
if (!ImGui::BeginTable("##properties", 3, kFlags)) return;
if (!prim.HasAPI<UsdShadeMaterialBindingAPI>()) return;
UsdShadeMaterialBindingAPI bindAPI(prim);
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, 20.f);
if (!ImGui::CollapsingHeader("Material Binding", ImGuiTreeNodeFlags_DefaultOpen))
return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##matbind", 2, kFlags)) return;
ImGui::TableSetupColumn("##mf", ImGuiTableColumnFlags_WidthFixed, 80.f);
ImGui::TableSetupColumn("##mv", ImGuiTableColumnFlags_WidthStretch);
// Direct binding (allPurpose)
{
auto direct = bindAPI.GetDirectBinding();
SdfPath matPath = direct.GetMaterialPath();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Direct");
ImGui::TableSetColumnIndex(1);
if (!matPath.IsEmpty()) {
bool authored = direct.GetBindingRel() && direct.GetBindingRel().IsAuthored();
ImVec4 col = authored ? ImVec4(0.6f, 0.9f, 1.f, 1.f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.f);
ImGui::TextColored(col, "%s", matPath.GetText());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", matPath.GetText());
} else {
ImGui::TextDisabled("(none)");
}
}
// Preview purpose direct binding (only show if different from allPurpose)
{
auto preview = bindAPI.GetDirectBinding(UsdShadeTokens->preview);
SdfPath previewPath = preview.GetMaterialPath();
if (!previewPath.IsEmpty()) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Preview");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", previewPath.GetText());
}
}
// Full purpose direct binding
{
auto full = bindAPI.GetDirectBinding(UsdShadeTokens->full);
SdfPath fullPath = full.GetMaterialPath();
if (!fullPath.IsEmpty()) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Full");
ImGui::TableSetColumnIndex(1);
ImGui::TextColored(ImVec4(0.6f, 0.9f, 1.f, 1.f), "%s", fullPath.GetText());
}
}
// Resolved (computed) bound material
{
UsdShadeMaterial resolved = bindAPI.ComputeBoundMaterial();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextDisabled("Resolved");
ImGui::TableSetColumnIndex(1);
if (resolved) {
ImGui::TextColored(ImVec4(0.7f, 1.f, 0.7f, 1.f),
"%s", resolved.GetPrim().GetPath().GetText());
} else {
ImGui::TextDisabled("(none)");
}
}
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// Inner attribute + relationship table — one category group
// ---------------------------------------------------------------------------
void PropertyPanel::RenderAttrRelSubTable(
const std::vector<pxr::UsdAttribute>& attrs,
const std::vector<pxr::UsdRelationship>& rels,
const pxr::UsdEditTarget& editTarget)
{
using namespace pxr;
if (attrs.empty() && rels.empty()) return;
constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg;
if (!ImGui::BeginTable("##propSubTable", 3, kFlags)) return;
// Col 0 wider when the key button column is active
float col0W = m_editTime.IsDefault() ? 20.f : 44.f;
ImGui::TableSetupColumn("##dot", ImGuiTableColumnFlags_WidthFixed, col0W);
ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthFixed, 140.f);
ImGui::TableSetupColumn("##val", ImGuiTableColumnFlags_WidthStretch);
// No TableHeadersRow matches usdtweak "no header" intent
const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();
const float rowH = ImGui::GetFrameHeight();
int uid = 0;
// ---- Attributes (mirrors usdtweak attribute loop) ----
for (auto& attr : prim.GetAttributes()) {
// ── Attributes ──────────────────────────────────────────────────────────
for (auto attr : attrs) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
// Col 0 mini button "(a)"
// Col 0 (a) mini-button + optional (k) key button
ImGui::TableSetColumnIndex(0);
ImGui::PushID(uid++);
DrawPropertyMiniButton("(a)",
@@ -1146,29 +1287,75 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
attr.GetTypeName().GetAsToken().GetText());
ImGui::PopID();
// Col 1 display name (namespace:basename)
bool isVarying = (attr.GetVariability() == SdfVariabilityVarying);
if (!m_editTime.IsDefault() && isVarying) {
ImGui::SameLine(0, 2);
ImGui::PushID(uid++);
DrawKeyButton(attr, m_editTime, m_commandHistory);
ImGui::PopID();
}
// Col 1 display name + right-click context menu
ImGui::TableSetColumnIndex(1);
ImGui::PushID(static_cast<int>(attr.GetPath().GetHash()) ^ 0x100);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(GetPropDisplayName(attr).c_str());
if (ImGui::BeginPopupContextItem("##attrCtx")) {
if (ImGui::MenuItem("Copy attribute path"))
ImGui::SetClipboardText(attr.GetPath().GetString().c_str());
ImGui::Separator();
bool authored = attr.IsAuthoredAt(editTarget);
ImGui::BeginDisabled(!authored);
if (ImGui::MenuItem("Clear Override")) {
// Capture current authored opinions so undo can restore them
std::vector<double> times;
attr.GetTimeSamples(&times);
std::vector<std::pair<double, VtValue>> samples;
for (double t : times) {
VtValue v;
if (attr.Get(&v, UsdTimeCode(t)))
samples.emplace_back(t, v);
}
VtValue defVal;
bool hasDefault = attr.Get(&defVal, UsdTimeCode::Default());
// Col 2 value widget with PushItemWidth(-FLT_MIN) (label hidden)
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Clear Override",
[attr]() mutable { attr.Clear(); },
[attr, samps = samples, hasDefault, defVal]() mutable {
if (hasDefault) attr.Set(defVal, UsdTimeCode::Default());
for (auto& [t, v] : samps)
attr.Set(v, UsdTimeCode(t));
}
));
} else {
attr.Clear();
}
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
ImGui::PopID();
// Col 2 value widget
ImGui::TableSetColumnIndex(2);
ImGui::PushID(static_cast<int>(attr.GetPath().GetHash()));
ImGui::PushItemWidth(-FLT_MIN);
// Check for allowedTokens first → Combo (mirrors DrawTfToken in usdtweak)
pxr::VtValue allowedTokens;
attr.GetMetadata(pxr::TfToken("allowedTokens"), &allowedTokens);
VtValue allowedTokens;
attr.GetMetadata(TfToken("allowedTokens"), &allowedTokens);
pxr::VtValue val;
VtValue val;
bool hasVal = attr.Get(&val, m_displayTime);
if (!allowedTokens.IsEmpty() &&
allowedTokens.IsHolding<pxr::VtArray<pxr::TfToken>>())
allowedTokens.IsHolding<VtArray<TfToken>>())
{
const auto& tokens = allowedTokens.UncheckedGet<pxr::VtArray<pxr::TfToken>>();
std::string cur = (hasVal && val.IsHolding<pxr::TfToken>())
? val.UncheckedGet<pxr::TfToken>().GetString() : "";
const auto& tokens = allowedTokens.UncheckedGet<VtArray<TfToken>>();
std::string cur = (hasVal && val.IsHolding<TfToken>())
? val.UncheckedGet<TfToken>().GetString() : "";
if (ImGui::BeginCombo("##tok", cur.c_str())) {
for (const auto& tok : tokens) {
bool sel = (tok.GetString() == cur);
@@ -1178,8 +1365,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImGui::EndCombo();
}
} else if (hasVal) {
// Type-aware widget dispatch (mirrors DrawVtValue in usdtweak)
pxr::VtValue modified = DrawVtValueWidget(attr, val);
VtValue modified = DrawVtValueWidget(attr, val);
if (!modified.IsEmpty())
attr.Set(modified, m_editTime);
} else if (attr.HasAuthoredConnections()) {
@@ -1198,11 +1384,11 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImGui::PopID();
}
// ---- Relationships (mirrors usdtweak relationship loop) ----
for (const auto& rel : prim.GetRelationships()) {
// ── Relationships ────────────────────────────────────────────────────────
for (const auto& rel : rels) {
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
// Col 0 mini button "(r)"
// Col 0 (r) mini-button (no key button for relationships)
ImGui::TableSetColumnIndex(0);
ImGui::PushID(uid++);
DrawPropertyMiniButton("(r)",
@@ -1210,15 +1396,41 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
rel.GetPath().GetString().c_str());
ImGui::PopID();
// Col 1 display name, coloured by authored state (mirrors DrawUsdRelationshipDisplayName)
// Col 1 display name + right-click context menu
ImGui::TableSetColumnIndex(1);
ImGui::PushID(static_cast<int>(rel.GetPath().GetHash()) ^ 0x100);
ImVec4 relColor = rel.IsAuthored()
? ImVec4(0.6f, 0.9f, 1.f, 1.f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.f);
ImGui::AlignTextToFramePadding();
ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str());
if (ImGui::BeginPopupContextItem("##relCtx")) {
if (ImGui::MenuItem("Copy path"))
ImGui::SetClipboardText(rel.GetPath().GetString().c_str());
ImGui::Separator();
ImGui::BeginDisabled(!rel.IsAuthored());
if (ImGui::MenuItem("Clear Override")) {
SdfPathVector oldTargets;
rel.GetTargets(&oldTargets);
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
"Clear Override",
[rel]() mutable { rel.ClearTargets(false); },
[rel, tgts = oldTargets]() mutable {
for (const auto& p : tgts) rel.AddTarget(p);
}
));
} else {
rel.ClearTargets(false);
}
ImGui::CloseCurrentPopup();
}
ImGui::EndDisabled();
ImGui::EndPopup();
}
ImGui::PopID();
// Col 2 target list: single row when collapsed, expand for multiple
// Col 2 target list
ImGui::TableSetColumnIndex(2);
SdfPathVector targets;
rel.GetTargets(&targets);
@@ -1253,6 +1465,113 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImGui::EndTable();
}
// ---------------------------------------------------------------------------
// Unified properties table (attributes + relationships)
//
// Mirrors ##DrawPropertyEditorTable in usdtweak DrawUsdPrimProperties:
// • Flags: SizingFixedFit | RowBg — NO borders, NO header row
// • Col 0 (fixed ~20px) : SmallButton mini-button coloured by authored state
// • Col 1 (fixed 140px) : property display name (namespace:basename)
// • Col 2 (stretch) : type-aware value widget, PushItemWidth(-FLT_MIN)
// ---------------------------------------------------------------------------
void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
using namespace pxr;
// Build attr-name → category label map from schema registry
std::unordered_map<std::string, std::string> attrToCategory;
const UsdSchemaRegistry& reg = UsdSchemaRegistry::GetInstance();
if (!m_primType.empty()) {
if (const UsdPrimDefinition* typeDef =
reg.FindConcretePrimDefinition(TfToken(m_primType))) {
for (const TfToken& prop : typeDef->GetPropertyNames())
attrToCategory.emplace(prop.GetString(), m_primType);
}
}
for (const auto& schemaToken : prim.GetAppliedSchemas()) {
std::string s = schemaToken.GetString();
// Multi-apply tokens look like "CollectionAPI:name"; strip instance suffix
auto pos = s.find(':');
std::string baseName = (pos != std::string::npos) ? s.substr(0, pos) : s;
if (const UsdPrimDefinition* apiDef =
reg.FindAppliedAPIPrimDefinition(TfToken(baseName))) {
for (const TfToken& prop : apiDef->GetPropertyNames())
attrToCategory.emplace(prop.GetString(), baseName); // first writer wins
}
}
// Bucket attrs and rels into ordered category groups
struct PropGroup {
std::vector<UsdAttribute> attrs;
std::vector<UsdRelationship> rels;
};
std::vector<std::string> orderedCats;
std::set<std::string> seenCats;
std::unordered_map<std::string, PropGroup> groups;
auto addCat = [&](const std::string& cat) {
if (seenCats.insert(cat).second) orderedCats.push_back(cat);
};
if (!m_primType.empty()) addCat(m_primType);
const std::string fallback = m_primType.empty() ? "Properties" : m_primType;
for (auto attr : prim.GetAttributes()) {
const std::string name = attr.GetName().GetString();
if (TfStringStartsWith(name, "xformOp:")) continue; // shown in TRS section
auto it = attrToCategory.find(name);
std::string cat;
if (it != attrToCategory.end()) {
cat = it->second;
} else {
std::string ns = attr.GetNamespace().GetString();
if (ns.empty()) {
cat = fallback;
} else {
auto c = ns.find(':');
cat = (c != std::string::npos) ? ns.substr(0, c) : ns;
}
}
addCat(cat);
groups[cat].attrs.push_back(attr);
}
for (const auto& rel : prim.GetRelationships()) {
std::string ns = rel.GetNamespace().GetString();
std::string cat;
if (ns.empty()) {
cat = fallback;
} else {
auto c = ns.find(':');
cat = (c != std::string::npos) ? ns.substr(0, c) : ns;
}
addCat(cat);
groups[cat].rels.push_back(rel);
}
// Sort: primType first (already inserted), remaining alphabetically
if (orderedCats.size() > 1) {
size_t firstSort = m_primType.empty() ? 0 : 1;
std::sort(orderedCats.begin() + firstSort, orderedCats.end());
}
// Render each category under a collapsing header
const UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();
for (const auto& cat : orderedCats) {
auto& group = groups[cat];
if (group.attrs.empty() && group.rels.empty()) continue;
ImGui::PushID(cat.c_str());
if (ImGui::CollapsingHeader(cat.c_str(), ImGuiTreeNodeFlags_DefaultOpen))
RenderAttrRelSubTable(group.attrs, group.rels, editTarget);
ImGui::PopID();
}
}
// ---------------------------------------------------------------------------
// Main render entry point
// ---------------------------------------------------------------------------
@@ -1289,6 +1608,11 @@ void PropertyPanel::Render() {
ImGui::Separator();
}
if (prim.HasAPI<pxr::UsdShadeMaterialBindingAPI>()) {
RenderMaterialBindSection(prim);
ImGui::Separator();
}
if (m_hasXform) {
RenderTransformSection();
ImGui::Separator();
+5 -1
View File
@@ -52,7 +52,11 @@ private:
void RenderPrimHeader(const pxr::UsdPrim& prim); ///< fixed-height header child
void RenderVariantSetsSection(const pxr::UsdPrim& prim);
void RenderTransformSection();
void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< unified attr+rel table
void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< groups attrs by API schema
void RenderMaterialBindSection(const pxr::UsdPrim& prim); ///< material:binding info
void RenderAttrRelSubTable(const std::vector<pxr::UsdAttribute>& attrs,
const std::vector<pxr::UsdRelationship>& rels,
const pxr::UsdEditTarget& editTarget); ///< inner 3-col table
PropertyManager* m_propertyManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
+118 -9
View File
@@ -86,9 +86,58 @@ void SceneHierarchyPanel::SetSelectedPathFromClick(const std::string& path) {
m_primarySelectedPath = path;
m_primarySdfPath = path.empty() ? SdfPath() : SdfPath(path);
if (!path.empty()) m_selectedPaths.insert(path);
// No scroll — user clicked the item directly, it's already visible.
if (!path.empty()) m_rangeAnchorPath = SdfPath(path);
m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(path);
if (m_onPrimsSelected) {
std::vector<std::string> v;
if (!path.empty()) v.push_back(path);
m_onPrimsSelected(v);
}
}
void SceneHierarchyPanel::ToggleSelectionFromClick(const std::string& path) {
if (m_selectedPaths.count(path)) {
m_selectedPaths.erase(path);
if (m_primarySelectedPath == path) {
m_primarySelectedPath = m_selectedPaths.empty() ? "" : *m_selectedPaths.begin();
m_primarySdfPath = m_primarySelectedPath.empty()
? SdfPath() : SdfPath(m_primarySelectedPath);
}
} else {
m_selectedPaths.insert(path);
m_primarySelectedPath = path;
m_primarySdfPath = SdfPath(path);
m_rangeAnchorPath = SdfPath(path);
}
m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(m_primarySelectedPath);
if (m_onPrimsSelected) {
std::vector<std::string> v(m_selectedPaths.begin(), m_selectedPaths.end());
m_onPrimsSelected(v);
}
}
void SceneHierarchyPanel::RangeSelectToPath(const SdfPath& path) {
auto it1 = std::find(m_visiblePrimOrder.begin(), m_visiblePrimOrder.end(), m_rangeAnchorPath);
auto it2 = std::find(m_visiblePrimOrder.begin(), m_visiblePrimOrder.end(), path);
if (it1 != m_visiblePrimOrder.end() && it2 != m_visiblePrimOrder.end()) {
int idx1 = static_cast<int>(it1 - m_visiblePrimOrder.begin());
int idx2 = static_cast<int>(it2 - m_visiblePrimOrder.begin());
if (idx1 > idx2) std::swap(idx1, idx2);
for (int i = idx1; i <= idx2; ++i)
m_selectedPaths.insert(m_visiblePrimOrder[i].GetString());
} else {
m_selectedPaths.insert(path.GetString());
}
m_primarySelectedPath = path.GetString();
m_primarySdfPath = path;
m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(m_primarySelectedPath);
if (m_onPrimsSelected) {
std::vector<std::string> v(m_selectedPaths.begin(), m_selectedPaths.end());
m_onPrimsSelected(v);
}
}
const char* SceneHierarchyPanel::GetPrimTypeIcon(const UsdPrim& prim) const {
@@ -185,6 +234,26 @@ void SceneHierarchyPanel::HandleKeyboardShortcuts() {
SetSelectedPathFromClick(groupPath.GetString());
}
// Ctrl+H — hide selected prims (visibility = invisible).
if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_H, false) &&
!m_selectedPaths.empty() && m_stage) {
for (const auto& pathStr : m_selectedPaths) {
UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(pathStr));
if (prim.IsValid() && prim.IsA<UsdGeomImageable>())
UsdGeomImageable(prim).GetVisibilityAttr().Set(UsdGeomTokens->invisible);
}
}
// Shift+H — show selected prims (visibility = inherited).
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_H, false) &&
!m_selectedPaths.empty() && m_stage) {
for (const auto& pathStr : m_selectedPaths) {
UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(pathStr));
if (prim.IsValid() && prim.IsA<UsdGeomImageable>())
UsdGeomImageable(prim).GetVisibilityAttr().Set(UsdGeomTokens->inherited);
}
}
// Delete — open the remove-prim confirmation modal.
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false) && !m_primarySdfPath.IsEmpty()) {
auto rootLayer = m_stage->GetRootLayer();
@@ -266,6 +335,9 @@ void SceneHierarchyPanel::Render() {
} else {
UsdPrim root = m_stage->GetPseudoRoot();
// Rebuilt each frame as RenderPrimNode visits prims — used for range select.
m_visiblePrimOrder.clear();
// Rebuild local-layer set once per frame (used by RenderPrimNode to
// detect attribute overrides). GetLayerStack() returns only the stage's
// own layers — root layer, sublayers, session layer — NOT reference layers.
@@ -288,12 +360,14 @@ void SceneHierarchyPanel::Render() {
ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingFixedFit;
if (ImGui::BeginTable("##primtree", 4, tblFlags)) {
// Col 0 stretches; cols 1-3 are small fixed-width icon columns.
const float kSpecW = ImGui::CalcTextSize("over").x + 2.f;
if (ImGui::BeginTable("##primtree", 5, tblFlags)) {
// Col 0 stretches; cols 1-4 are small fixed-width columns.
ImGui::TableSetupColumn("##prim", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##type", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##vis", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##ref", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##spec", ImGuiTableColumnFlags_WidthFixed, kSpecW);
for (const auto& child : root.GetChildren())
RenderPrimNode(child);
@@ -524,6 +598,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
bool isInvisible = false;
bool hasRefs = prim.HasAuthoredReferences();
bool hasChildren = !prim.GetChildren().empty();
SdfSpecifier spec = prim.GetSpecifier();
if (isImageable) {
UsdGeomImageable img(prim);
@@ -565,6 +640,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
// ────────────────────────────────────────────────────────────────────────
ImGui::TableNextRow();
m_visiblePrimOrder.push_back(primPath); // for Shift+LMB range select
ImGui::TableNextColumn(); // Col 0 — prim name + tree arrow
ImGui::PushID(primStr.c_str());
@@ -642,6 +718,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::TableNextColumn(); // Col 1
ImGui::TableNextColumn(); // Col 2
ImGui::TableNextColumn(); // Col 3
ImGui::TableNextColumn(); // Col 4
if (open && hasChildren)
ImGui::TreePop();
ImGui::PopID();
@@ -655,9 +732,16 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
}
if (!isRenaming) {
// Selection on click (not on toggle arrow).
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
// Selection on LMB click (not on toggle arrow).
// Shift+LMB: range-select from anchor. Ctrl+LMB: toggle. Plain: replace.
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
if (ImGui::GetIO().KeyShift)
RangeSelectToPath(primPath);
else if (ImGui::GetIO().KeyCtrl)
ToggleSelectionFromClick(primStr);
else
SetSelectedPathFromClick(primStr);
}
// Double-click to start inline rename.
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
@@ -705,7 +789,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::EndDragDropTarget();
}
// Context menu (must follow the last widget = the tree node).
// Context menu on RMB.
RenderContextMenu(prim);
// A context menu op (Unparent, Group, etc.) may have moved this prim,
@@ -714,6 +798,7 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::TableNextColumn(); // Col 1
ImGui::TableNextColumn(); // Col 2
ImGui::TableNextColumn(); // Col 3
ImGui::TableNextColumn(); // Col 4
if (open && hasChildren)
ImGui::TreePop();
ImGui::PopID();
@@ -740,8 +825,9 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
const float iconSz = ImGui::GetTextLineHeight();
const ImVec2 iconVec(iconSz, iconSz);
Icon visIcon = isInvisible ? Icon::EyeSlash : Icon::Eye;
ImVec4 visTint = isInvisible ? ImVec4(0.45f, 0.45f, 0.45f, 0.6f)
: ImVec4(0.9f, 0.9f, 0.9f, 1.0f);
// Invisible: EyeSlash in orange; Visible: Eye at full alpha (dimmer tint).
ImVec4 visTint = isInvisible ? ImVec4(1.00f, 0.60f, 0.15f, 1.0f)
: ImVec4(0.60f, 0.60f, 0.60f, 1.0f);
ImTextureID id = m_iconManager ? m_iconManager->Get(visIcon) : ImTextureID_Invalid;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
@@ -750,7 +836,8 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0));
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 1.f);
if (ImGui::ImageButton("##vis", ImTextureRef(id), iconVec,
std::string visId = "##vis_" + primStr;
if (ImGui::ImageButton(visId.c_str(), ImTextureRef(id), iconVec,
ImVec2(0,0), ImVec2(1,1), ImVec4(0,0,0,0), visTint)) {
try {
UsdGeomImageable img(prim);
@@ -789,6 +876,28 @@ void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
ImGui::SetTooltip("Has references");
}
// ── Col 4: Specifier (def / over / class) ──────────────────────────────
ImGui::TableNextColumn();
{
const char* label = nullptr;
ImVec4 color;
switch (spec) {
case SdfSpecifierOver:
label = "over";
color = ImVec4(1.00f, 0.60f, 0.10f, isActive ? 1.0f : 0.45f);
break;
case SdfSpecifierClass:
label = "class";
color = ImVec4(0.40f, 0.70f, 1.00f, isActive ? 1.0f : 0.45f);
break;
default: // SdfSpecifierDef
label = "def";
color = ImVec4(0.45f, 0.45f, 0.45f, isActive ? 0.55f : 0.30f);
break;
}
ImGui::TextColored(color, "%s", label);
}
// ── Recurse into children ───────────────────────────────────────────────
// TreePop must be called in the SAME column as TreeNodeEx (col 0).
// Since we called TableNextColumn three more times above, we must move
+11 -1
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "../core/PropertyManager.h"
#include "../core/CommandHistory.h"
@@ -62,6 +62,9 @@ public:
using PrimSelectCallback = std::function<void(const std::string& path)>;
void SetOnPrimSelected(PrimSelectCallback callback) { m_onPrimSelected = callback; }
using MultiPrimSelectCallback = std::function<void(const std::vector<std::string>&)>;
void SetOnPrimsSelected(MultiPrimSelectCallback cb) { m_onPrimsSelected = cb; }
/// Called when stage-level metadata (e.g. up axis) is changed via the hierarchy panel.
using StageMetadataChangedCallback = std::function<void()>;
void SetOnStageMetadataChanged(StageMetadataChangedCallback callback) { m_onStageMetadataChanged = callback; }
@@ -96,7 +99,12 @@ private:
/// NOT layers that came in through references or payloads.
std::unordered_set<std::string> m_localLayers;
/// Visible prim order rebuilt each frame — used for Shift+RMB range select.
std::vector<pxr::SdfPath> m_visiblePrimOrder;
pxr::SdfPath m_rangeAnchorPath;
PrimSelectCallback m_onPrimSelected;
MultiPrimSelectCallback m_onPrimsSelected;
StageMetadataChangedCallback m_onStageMetadataChanged;
/// Remove-prim confirmation state.
@@ -121,6 +129,8 @@ private:
bool m_renameJustStarted = false;
void HandleKeyboardShortcuts();
void ToggleSelectionFromClick(const std::string& path);
void RangeSelectToPath(const pxr::SdfPath& path);
SdfPath FindUniqueChildPath(const SdfPath& parent, const std::string& baseName);
};
+12 -1
View File
@@ -138,6 +138,17 @@ void ViewportPanel::SetSelectedPrimPath(const std::string& path)
BroadcastSelection();
}
void ViewportPanel::SetSelectedPrimPaths(const std::vector<std::string>& paths)
{
m_selectedSdfPaths.clear();
m_selectedPrimPath.clear();
for (const auto& p : paths)
m_selectedSdfPaths.push_back(pxr::SdfPath(p));
if (!paths.empty())
m_selectedPrimPath = paths.front();
BroadcastSelection();
}
// ---------------------------------------------------------------------------
// Forwarding accessors
// ---------------------------------------------------------------------------
@@ -384,7 +395,7 @@ void ViewportPanel::RenderGlobalLeftToolbar(ImVec2 contentPos, ImVec2 /*contentS
const float kIconPad = 5.0f;
const float kRounding = 4.0f;
const float kSpacing = 3.0f;
const float kPadX = 9.0f; // left padding inside the strip
const float kPadX = 4.0f; // left padding inside the strip
const float kPadY = 10.0f; // top padding
const float kSepH = 1.0f; // separator line height
const float kSepGap = 6.0f; // space around separator
+3 -1
View File
@@ -48,6 +48,8 @@ public:
// ── Selection (called by SceneHierarchyPanel) ────────────────────────────
/// Set a single selected prim (clears any multi-selection).
void SetSelectedPrimPath(const std::string& path);
/// Set multiple selected prims (from hierarchy Ctrl+click multi-select).
void SetSelectedPrimPaths(const std::vector<std::string>& paths);
// ── Main render (called from Application::RenderUI) ──────────────────────
void Render(bool* p_open = nullptr);
@@ -99,7 +101,7 @@ private:
bool IsMouseOverDivider(ImVec2 origin, ImVec2 total) const;
/// Width (px) of the reserved left toolbar strip.
static constexpr float kToolbarW = 52.0f;
static constexpr float kToolbarW = 40.0f;
// ── Selection management ─────────────────────────────────────────────────
/// Push the current shared selection into every tile and the manipulator.