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:
+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
|
||||
|
||||
Reference in New Issue
Block a user