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>
This commit is contained in:
@@ -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();
|
||||
|
||||
+120
-11
@@ -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_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())
|
||||
SetSelectedPathFromClick(primStr);
|
||||
// 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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user