diff --git a/resources/icons/chevron-down.svg b/resources/icons/chevron-down.svg
new file mode 100644
index 0000000..e6e3bed
--- /dev/null
+++ b/resources/icons/chevron-down.svg
@@ -0,0 +1 @@
+
diff --git a/resources/icons/chevron-up.svg b/resources/icons/chevron-up.svg
new file mode 100644
index 0000000..f105c34
--- /dev/null
+++ b/resources/icons/chevron-up.svg
@@ -0,0 +1 @@
+
diff --git a/src/ui/IconManager.cpp b/src/ui/IconManager.cpp
index 7a8b899..0738faa 100644
--- a/src/ui/IconManager.cpp
+++ b/src/ui/IconManager.cpp
@@ -61,6 +61,8 @@ static const char* IconFilename(Icon icon) {
case Icon::FilePlus: return "file-plus.svg";
case Icon::Pen: return "pen.svg";
case Icon::Settings: return "gear.svg";
+ case Icon::ChevronUp: return "chevron-up.svg";
+ case Icon::ChevronDown: return "chevron-down.svg";
default: return nullptr;
}
}
@@ -77,6 +79,7 @@ static constexpr Icon kAllIcons[] = {
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
+ Icon::ChevronUp, Icon::ChevronDown,
};
// ---------------------------------------------------------------------------
diff --git a/src/ui/IconManager.h b/src/ui/IconManager.h
index 47ed9e4..7fb9bbf 100644
--- a/src/ui/IconManager.h
+++ b/src/ui/IconManager.h
@@ -55,6 +55,9 @@ enum class Icon {
FilePlus, // create new file
Pen, // edit / edit target
Settings, // gear / viewport options
+ // List reordering
+ ChevronUp, // move item up
+ ChevronDown, // move item down
};
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
diff --git a/src/ui/SceneHierarchyPanel.cpp b/src/ui/SceneHierarchyPanel.cpp
index afa76ce..2e63a82 100644
--- a/src/ui/SceneHierarchyPanel.cpp
+++ b/src/ui/SceneHierarchyPanel.cpp
@@ -8,6 +8,7 @@
#include "../core/commands/RenamePrimCommand.h"
#include "../core/commands/ReparentPrimCommand.h"
#include "../core/commands/GroupPrimsCommand.h"
+#include "../core/commands/LayerCommands.h"
#include
#include
#include
@@ -309,6 +310,8 @@ void SceneHierarchyPanel::Render() {
}
}
+ bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
+ ImGui::BeginDisabled(renderLayerActive);
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
ImGuiComboFlags_HeightRegular)) {
@@ -326,9 +329,16 @@ void SceneHierarchyPanel::Render() {
}
ImGui::EndCombo();
}
+ bool comboHovered = ImGui::IsItemHovered();
+ ImGui::EndDisabled();
+ if (renderLayerActive && comboHovered)
+ ImGui::SetTooltip("Edit target is locked to the active render layer");
ImGui::Spacing();
}
+ RenderSublayerSection();
+ ImGui::Spacing();
+
auto paths = m_propertyManager->GetPrimPaths();
if (paths.empty()) {
ImGui::TextDisabled("No prims in stage");
@@ -582,6 +592,272 @@ void SceneHierarchyPanel::Render() {
ProcessPendingReplaceRef();
}
+// ---------------------------------------------------------------------------
+void SceneHierarchyPanel::RenderSublayerSection() {
+ if (!ImGui::CollapsingHeader("Sublayers", ImGuiTreeNodeFlags_DefaultOpen))
+ return;
+
+ if (!m_layerManager) {
+ ImGui::TextDisabled(" No layer manager");
+ return;
+ }
+
+ auto sublayers = m_layerManager->GetSublayers();
+ if (sublayers.empty()) {
+ ImGui::TextDisabled(" No sublayers");
+ return;
+ }
+
+ const float rowHeight = ImGui::GetTextLineHeightWithSpacing();
+ const int visibleRows = std::min(static_cast(sublayers.size()), 6);
+ float tableHeight = rowHeight * (visibleRows + 1); // +1 for header row
+
+ if (!ImGui::BeginTable("HierarchySublayerTable", 6,
+ ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
+ ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY,
+ ImVec2(0, tableHeight)))
+ return;
+
+ ImGui::TableSetupScrollFreeze(0, 1);
+ ImGui::TableSetupColumn("##drag", ImGuiTableColumnFlags_WidthFixed, 14.0f);
+ ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
+ ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
+ ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f);
+ ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 55.0f);
+ ImGui::TableSetupColumn("##order", ImGuiTableColumnFlags_WidthFixed, 44.0f);
+
+ // Custom header row: pen icon for the ET column, text for the rest.
+ ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
+ ImGui::TableSetColumnIndex(2); ImGui::TableHeader("Name");
+ ImGui::TableSetColumnIndex(3);
+ {
+ float cellW = ImGui::GetContentRegionAvail().x;
+ float iconW = 14.0f;
+ float off = (cellW - iconW) * 0.5f;
+ if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
+ if (m_iconManager)
+ ImGui::Image(ImTextureRef(m_iconManager->Get(Icon::Pen)), ImVec2(iconW, iconW));
+ else
+ ImGui::TextUnformatted("ET");
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Edit Target");
+ }
+ ImGui::TableSetColumnIndex(4); ImGui::TableHeader("Muted");
+ ImGui::TableSetColumnIndex(5); ImGui::TableHeader("Order");
+
+ for (int i = 0; i < static_cast(sublayers.size()); i++) {
+ const auto& li = sublayers[i];
+ ImGui::TableNextRow();
+ ImGui::PushID(i);
+
+ // Col 0: invisible span-all Selectable for row selection / drag / context menu.
+ ImGui::TableSetColumnIndex(0);
+ ImVec2 rowStart = ImGui::GetCursorScreenPos(); // save before Selectable moves cursor
+
+ bool isSelected = (m_sublayerSelectedIdx == i);
+ if (ImGui::Selectable("##row", isSelected,
+ ImGuiSelectableFlags_SpanAllColumns |
+ ImGuiSelectableFlags_AllowOverlap,
+ ImVec2(0, 0)))
+ m_sublayerSelectedIdx = i;
+
+ // Context menu: must come immediately after the span-all Selectable so
+ // BeginPopupContextItem sees it as the "last item" and responds to
+ // right-click anywhere in the row.
+ RenderSublayerContextMenu(i, sublayers);
+
+ // Drag source: also attached to the Selectable (left-button drag).
+ if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
+ ImGui::SetDragDropPayload("HIER_SUBLAYER_IDX", &i, sizeof(int));
+ ImGui::Text("%s", li.displayName.c_str());
+ ImGui::EndDragDropSource();
+ }
+
+ // Drop target on the Selectable.
+ if (ImGui::BeginDragDropTarget()) {
+ if (const ImGuiPayload* payload =
+ ImGui::AcceptDragDropPayload("HIER_SUBLAYER_IDX")) {
+ int src = *static_cast(payload->Data);
+ int dst = i;
+ if (src != dst) {
+ std::vector before, after;
+ for (const auto& sl : sublayers) before.push_back(sl.identifier);
+ after = before;
+ std::string moved = after[src];
+ after.erase(after.begin() + src);
+ after.insert(after.begin() + dst, moved);
+ if (m_commandHistory) {
+ m_commandHistory->Push(std::make_unique(
+ m_layerManager, before, after));
+ } else {
+ LayerReorderCommand cmd(m_layerManager, before, after);
+ static_cast(cmd).Execute();
+ }
+ }
+ }
+ ImGui::EndDragDropTarget();
+ }
+
+ // "=" drag handle: overlay at col 0 using the saved screen position so it
+ // sits inside the row, not below it (TableSetColumnIndex only resets X).
+ ImGui::SetCursorScreenPos(rowStart);
+ ImGui::TextDisabled("=");
+
+ // Col 1: dirty indicator
+ ImGui::TableSetColumnIndex(1);
+ if (li.isDirty)
+ ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
+ else
+ ImGui::TextDisabled(" ");
+
+ bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
+ bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
+
+ // Col 2: layer name
+ ImGui::TableSetColumnIndex(2);
+ ImVec4 nameColor = li.isMuted
+ ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f)
+ : ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
+ ImGui::TextColored(nameColor, "%s", li.displayName.c_str());
+ if (ImGui::IsItemHovered() && !li.realPath.empty())
+ ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
+ if (isRenderLayer) {
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Managed from the Render Layer panel");
+ }
+
+ // Col 3: edit target checkbox — checking sets this layer as edit target;
+ // unchecking does nothing (there is always an active edit target).
+ ImGui::TableSetColumnIndex(3);
+ {
+ bool isET = li.isEditTarget;
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
+ ImGui::BeginDisabled(renderLayerActive);
+ if (ImGui::Checkbox("##et", &isET) && isET)
+ m_layerManager->SetEditTarget(li.identifier);
+ ImGui::EndDisabled();
+ ImGui::PopStyleVar();
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
+ : li.isEditTarget ? "Edit target (active)"
+ : "Set as edit target");
+ }
+
+ // Col 4: mute checkbox — disabled for render-layer sublayers, which
+ // RenderLayerManager mutes/unmutes exclusively as layers are switched.
+ ImGui::TableSetColumnIndex(4);
+ {
+ bool muted = li.isMuted;
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
+ ImGui::BeginDisabled(isRenderLayer);
+ if (ImGui::Checkbox("##muted", &muted)) {
+ if (muted) m_layerManager->MuteLayer(li.identifier);
+ else m_layerManager->UnmuteLayer(li.identifier);
+ }
+ ImGui::EndDisabled();
+ ImGui::PopStyleVar();
+ if (isRenderLayer && ImGui::IsItemHovered())
+ ImGui::SetTooltip("Managed from the Render Layer panel");
+ }
+
+ // Col 5: up/down reorder icon buttons.
+ ImGui::TableSetColumnIndex(5);
+ {
+ bool canUp = (i > 0);
+ bool canDown = (i < static_cast(sublayers.size()) - 1);
+
+ auto buildBeforeAfter = [&](int a, int b,
+ std::vector& before,
+ std::vector& after) {
+ for (const auto& sl : sublayers) before.push_back(sl.identifier);
+ after = before;
+ std::swap(after[a], after[b]);
+ };
+
+ ImVec2 iconSize(14.0f, 14.0f);
+
+ ImGui::BeginDisabled(!canUp);
+ ImTextureID upTex = m_iconManager ? m_iconManager->Get(Icon::ChevronUp) : ImTextureID_Invalid;
+ if (m_iconManager ? ImGui::ImageButton("##up", ImTextureRef(upTex), iconSize)
+ : ImGui::ArrowButton("##up", ImGuiDir_Up)) {
+ std::vector before, after;
+ buildBeforeAfter(i, i - 1, before, after);
+ if (m_commandHistory)
+ m_commandHistory->Push(
+ std::make_unique(m_layerManager, before, after));
+ else {
+ LayerReorderCommand cmd(m_layerManager, before, after);
+ static_cast(cmd).Execute();
+ }
+ }
+ ImGui::EndDisabled();
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Up");
+
+ ImGui::SameLine(0.0f, 2.0f);
+
+ ImGui::BeginDisabled(!canDown);
+ ImTextureID downTex = m_iconManager ? m_iconManager->Get(Icon::ChevronDown) : ImTextureID_Invalid;
+ if (m_iconManager ? ImGui::ImageButton("##down", ImTextureRef(downTex), iconSize)
+ : ImGui::ArrowButton("##down", ImGuiDir_Down)) {
+ std::vector before, after;
+ buildBeforeAfter(i, i + 1, before, after);
+ if (m_commandHistory)
+ m_commandHistory->Push(
+ std::make_unique(m_layerManager, before, after));
+ else {
+ LayerReorderCommand cmd(m_layerManager, before, after);
+ static_cast(cmd).Execute();
+ }
+ }
+ ImGui::EndDisabled();
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Down");
+ }
+
+ ImGui::PopID();
+ }
+
+ ImGui::EndTable();
+}
+
+// ---------------------------------------------------------------------------
+void SceneHierarchyPanel::RenderSublayerContextMenu(int i,
+ const std::vector& sublayers) {
+ if (!ImGui::BeginPopupContextItem(("ctx_sublayer_" + std::to_string(i)).c_str()))
+ return;
+
+ const auto& li = sublayers[i];
+ bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
+ bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
+
+ if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
+ if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
+ else m_layerManager->MuteLayer(li.identifier);
+ }
+ if (isRenderLayer && ImGui::IsItemHovered())
+ ImGui::SetTooltip("Managed from the Render Layer panel");
+
+ if (!li.isEditTarget &&
+ ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
+ m_layerManager->SetEditTarget(li.identifier);
+ if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
+ ImGui::SetTooltip("Edit target is locked to the active render layer");
+
+ ImGui::Separator();
+
+ if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
+ if (m_commandHistory)
+ m_commandHistory->Push(
+ std::make_unique(m_layerManager, i));
+ else
+ m_layerManager->RemoveSublayer(i);
+ }
+ if (isRenderLayer && ImGui::IsItemHovered())
+ ImGui::SetTooltip("Managed from the Render Layer panel");
+
+ ImGui::EndPopup();
+}
+
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
if (!prim.IsValid()) return;