diff --git a/resources/icons/file-plus.svg b/resources/icons/file-plus.svg
new file mode 100644
index 0000000..c96900c
--- /dev/null
+++ b/resources/icons/file-plus.svg
@@ -0,0 +1 @@
+
diff --git a/resources/icons/pen.svg b/resources/icons/pen.svg
new file mode 100644
index 0000000..408a9e2
--- /dev/null
+++ b/resources/icons/pen.svg
@@ -0,0 +1 @@
+
diff --git a/src/core/LayerManager.cpp b/src/core/LayerManager.cpp
index e051400..bdbd74f 100644
--- a/src/core/LayerManager.cpp
+++ b/src/core/LayerManager.cpp
@@ -148,6 +148,15 @@ void LayerManager::UnmuteLayer(const std::string& layerIdentifier) {
}
}
+bool LayerManager::SetEditTarget(const std::string& identifier) {
+ if (!m_stage) return false;
+ SdfLayerHandle layer = SdfLayer::Find(identifier);
+ if (!layer) return false;
+ m_stage->SetEditTarget(layer);
+ Refresh();
+ return true;
+}
+
bool LayerManager::IsLayerMuted(const std::string& layerIdentifier) const {
if (m_stage) {
return m_stage->IsLayerMuted(layerIdentifier);
@@ -181,32 +190,51 @@ std::string LayerManager::ExtractDisplayName(const std::string& identifier) {
void LayerManager::BuildLayerList() {
m_layers.clear();
-
+ m_layerRefs.clear();
if (!m_stage) return;
-
+
try {
- // Get the full layer stack (root + all sublayers)
- SdfLayerHandleVector layerStack = m_stage->GetLayerStack();
-
- SdfLayerHandle rootLayer = m_stage->GetRootLayer();
+ SdfLayerHandle rootLayer = m_stage->GetRootLayer();
SdfLayerHandle sessionLayer = m_stage->GetSessionLayer();
-
- for (const auto& layer : layerStack) {
+ SdfLayerHandle editTarget = m_stage->GetEditTarget().GetLayer();
+
+ auto makeInfo = [&](SdfLayerHandle layer, bool isRoot, bool isSession) {
LayerInfo info;
- info.layer = layer;
- info.identifier = layer->GetIdentifier();
- info.displayName = ExtractDisplayName(info.identifier);
- info.realPath = layer->GetRealPath();
- info.isMuted = layer->IsMuted();
- info.isAnonymous = layer->IsAnonymous();
- info.isRootLayer = (layer == rootLayer);
- info.isSessionLayer = (layer == sessionLayer);
-
- m_layers.push_back(info);
+ info.layer = layer;
+ info.identifier = layer->GetIdentifier();
+ info.displayName = ExtractDisplayName(info.identifier);
+ info.realPath = layer->GetRealPath();
+ info.isMuted = m_stage->IsLayerMuted(info.identifier);
+ info.isAnonymous = layer->IsAnonymous();
+ info.isRootLayer = isRoot;
+ info.isSessionLayer = isSession;
+ info.isEditTarget = (layer == editTarget);
+ info.isDirty = layer->IsDirty();
+ return info;
+ };
+
+ // Root and session layers: stage always holds strong refs, handles are valid.
+ if (sessionLayer) m_layers.push_back(makeInfo(sessionLayer, false, true));
+ if (rootLayer) m_layers.push_back(makeInfo(rootLayer, true, false));
+
+ // Walk root layer's sublayer paths instead of stage->GetLayerStack().
+ // GetLayerStack() drops muted layers because they don't contribute to
+ // composition. FindOrOpenRelativeToLayer locates or reopens the layer
+ // regardless of mute state. We store the returned RefPtr in m_layerRefs
+ // so the layer object isn't garbage-collected before the next Refresh().
+ if (rootLayer) {
+ for (const auto& subPath : rootLayer->GetSubLayerPaths()) {
+ SdfLayerRefPtr subRef =
+ SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath);
+ if (subRef) {
+ m_layerRefs.push_back(subRef);
+ m_layers.push_back(makeInfo(SdfLayerHandle(subRef), false, false));
+ }
+ }
}
-
+
LOG_DEBUG("Built layer list with " + std::to_string(m_layers.size()) + " layers");
-
+
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to build layer list: ") + e.what());
}
diff --git a/src/core/LayerManager.h b/src/core/LayerManager.h
index fc825e0..c84e37a 100644
--- a/src/core/LayerManager.h
+++ b/src/core/LayerManager.h
@@ -17,6 +17,8 @@ struct LayerInfo {
bool isAnonymous;
bool isRootLayer;
bool isSessionLayer;
+ bool isEditTarget;
+ bool isDirty;
};
class LayerManager {
@@ -39,6 +41,9 @@ public:
bool MoveSublayerUp(int index);
bool MoveSublayerDown(int index);
+ // Edit target
+ bool SetEditTarget(const std::string& identifier);
+
// Muting
void MuteLayer(const std::string& layerIdentifier);
void UnmuteLayer(const std::string& layerIdentifier);
@@ -49,9 +54,11 @@ public:
private:
void BuildLayerList();
-
+
pxr::UsdStageRefPtr m_stage;
std::vector m_layers;
+ // Strong references keep sublayers alive even when the stage drops them after muting.
+ std::vector m_layerRefs;
};
} // namespace UsdLayerManager
\ No newline at end of file
diff --git a/src/core/commands/LayerCommands.cpp b/src/core/commands/LayerCommands.cpp
index 67475e7..5361484 100644
--- a/src/core/commands/LayerCommands.cpp
+++ b/src/core/commands/LayerCommands.cpp
@@ -46,10 +46,10 @@ LayerRemoveCommand::LayerRemoveCommand(LayerManager* mgr, int index)
, m_description("Remove Layer")
{
if (!mgr) return;
- auto layers = mgr->GetLayerStack();
- if (index >= 0 && index < static_cast(layers.size())) {
- m_savedPath = layers[index].identifier;
- m_description = "Remove Layer " + layers[index].displayName;
+ auto sublayers = mgr->GetSublayers();
+ if (index >= 0 && index < static_cast(sublayers.size())) {
+ m_savedPath = sublayers[index].identifier;
+ m_description = "Remove Layer " + sublayers[index].displayName;
}
}
@@ -81,22 +81,14 @@ void LayerReorderCommand::Undo() { ApplyOrder(m_before); }
void LayerReorderCommand::ApplyOrder(const std::vector& order) {
if (!m_mgr) return;
- // Remove all sublayers and re-insert in the desired order.
- // We only control sublayers — root and session are fixed.
- // First gather current sublayer indices (non-root, non-session).
- auto layers = m_mgr->GetLayerStack();
- // Count sublayers and remove them from highest index down.
- std::vector sublayerIndices;
- for (int i = 0; i < static_cast(layers.size()); ++i) {
- if (!layers[i].isRootLayer && !layers[i].isSessionLayer)
- sublayerIndices.push_back(i);
- }
- // Remove from back to front to preserve indices.
- for (int i = static_cast(sublayerIndices.size()) - 1; i >= 0; --i)
- m_mgr->RemoveSublayer(sublayerIndices[i]);
+ // RemoveSublayer() operates on GetSubLayerPaths() — a sublayer-local list
+ // with indices 0..N-1. GetLayerStack() prepends session and root layers, so
+ // its indices are always wrong here. Use GetSublayers().size() for the count.
+ int count = static_cast(m_mgr->GetSublayers().size());
+ for (int i = count - 1; i >= 0; --i)
+ m_mgr->RemoveSublayer(i);
- // Re-add in desired order.
for (const auto& path : order)
m_mgr->InsertSublayerPath(path, -1);
}
diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp
index c693eb4..8745ee0 100644
--- a/src/ui/Application.cpp
+++ b/src/ui/Application.cpp
@@ -57,9 +57,9 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_layerManager = std::make_unique();
m_propertyManager = std::make_unique();
m_propertyManager->SetCommandHistory(&m_commandHistory);
- m_layerPanel = std::make_unique();
- m_layerPanel->SetLayerManager(m_layerManager.get());
- m_layerPanel->SetCommandHistory(&m_commandHistory);
+ m_stageEditorPanel = std::make_unique();
+ m_stageEditorPanel->SetLayerManager(m_layerManager.get());
+ m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
m_sceneHierarchyPanel = std::make_unique();
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
@@ -82,6 +82,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_sceneHierarchyPanel->SetIconManager(m_iconManager.get());
m_viewportPanel->SetIconManager(m_iconManager.get());
m_timelinePanel->SetIconManager(m_iconManager.get());
+ m_stageEditorPanel->SetIconManager(m_iconManager.get());
m_sceneHierarchyPanel->SetOnPrimSelected(
[this](const std::string& path) {
@@ -132,7 +133,7 @@ void Application::Shutdown() {
m_viewportPanel.reset();
m_sceneHierarchyPanel.reset();
m_propertyPanel.reset();
- m_layerPanel.reset();
+ m_stageEditorPanel.reset();
m_propertyManager.reset();
m_layerManager.reset();
@@ -206,8 +207,8 @@ void Application::RenderUI() {
RenderStageInfo();
}
- ImGui::Begin("Layer Panel", nullptr, ImGuiWindowFlags_NoCollapse);
- m_layerPanel->Render();
+ ImGui::Begin("Stage Editor", nullptr, ImGuiWindowFlags_NoCollapse);
+ m_stageEditorPanel->Render();
ImGui::End();
m_viewportPanel->Render();
diff --git a/src/ui/Application.h b/src/ui/Application.h
index bb3ffed..6511dba 100644
--- a/src/ui/Application.h
+++ b/src/ui/Application.h
@@ -2,7 +2,7 @@
#include "ImGuiContext.h"
#include "IconManager.h"
-#include "LayerPanel.h"
+#include "StageEditorPanel.h"
#include "SceneHierarchyPanel.h"
#include "ViewportPanel.h"
#include "PropertyPanel.h"
@@ -49,7 +49,7 @@ private:
std::unique_ptr m_layerManager;
std::unique_ptr m_propertyManager;
CommandHistory m_commandHistory;
- std::unique_ptr m_layerPanel;
+ std::unique_ptr m_stageEditorPanel;
std::unique_ptr m_sceneHierarchyPanel;
std::unique_ptr m_viewportPanel;
std::unique_ptr m_propertyPanel;
diff --git a/src/ui/IconManager.cpp b/src/ui/IconManager.cpp
index c8b9c62..6c26576 100644
--- a/src/ui/IconManager.cpp
+++ b/src/ui/IconManager.cpp
@@ -57,6 +57,9 @@ static const char* IconFilename(Icon icon) {
case Icon::SkipEnd: return "skip-end.svg";
case Icon::Loop: return "loop.svg";
case Icon::Bounce: return "bounce.svg";
+ case Icon::Refresh: return "arrows-rotate.svg";
+ case Icon::FilePlus: return "file-plus.svg";
+ case Icon::Pen: return "pen.svg";
default: return nullptr;
}
}
@@ -72,6 +75,7 @@ static constexpr Icon kAllIcons[] = {
Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad,
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
+ Icon::Refresh, Icon::FilePlus, Icon::Pen,
};
// ---------------------------------------------------------------------------
diff --git a/src/ui/IconManager.h b/src/ui/IconManager.h
index 1de2595..f13a899 100644
--- a/src/ui/IconManager.h
+++ b/src/ui/IconManager.h
@@ -50,6 +50,10 @@ enum class Icon {
SkipEnd, // go to last frame (▶|)
Loop, // loop toggle (↻)
Bounce, // bounce/ping-pong (↔)
+ // General actions
+ Refresh, // reload / refresh
+ FilePlus, // create new file
+ Pen, // edit / edit target
};
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
diff --git a/src/ui/LayerPanel.cpp b/src/ui/LayerPanel.cpp
deleted file mode 100644
index c6d4eca..0000000
--- a/src/ui/LayerPanel.cpp
+++ /dev/null
@@ -1,253 +0,0 @@
-#include "LayerPanel.h"
-#include "../utils/Logger.h"
-#include "../core/commands/LayerCommands.h"
-#include
-#include
-
-namespace UsdLayerManager {
-
-LayerPanel::LayerPanel()
- : m_layerManager(nullptr)
- , m_selectedLayerIndex(-1)
- , m_showCreateDialog(false) {
- m_newLayerPath[0] = '\0';
- m_newLayerName[0] = '\0';
-}
-
-LayerPanel::~LayerPanel() {
-}
-
-void LayerPanel::SetLayerManager(LayerManager* manager) {
- m_layerManager = manager;
-}
-
-void LayerPanel::Render() {
- if (!m_layerManager) return;
-
- // Header with buttons
- ImGui::Text("Layers");
- ImGui::SameLine(ImGui::GetWindowWidth() - 110);
-
- if (ImGui::Button("Refresh")) {
- m_layerManager->Refresh();
- }
- ImGui::SameLine();
- if (ImGui::Button("Add Layer")) {
- m_showCreateDialog = true;
- m_newLayerPath[0] = '\0';
- strcpy_s(m_newLayerName, "new_layer.usd");
- }
-
- ImGui::Separator();
-
- // Create layer dialog
- if (m_showCreateDialog) {
- ShowCreateLayerDialog();
- }
-
- // Layer list
- auto layers = m_layerManager->GetLayerStack();
-
- if (layers.empty()) {
- ImGui::TextDisabled("No layers loaded");
- return;
- }
-
- // Layer table
- if (ImGui::BeginTable("LayerTable", 4,
- ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
- ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) {
-
- ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 20.0f);
- ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
- ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 60.0f);
- ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80.0f);
- ImGui::TableHeadersRow();
-
- for (int i = 0; i < static_cast(layers.size()); i++) {
- const auto& layerInfo = layers[i];
- ImGui::TableNextRow();
-
- bool isSelected = (m_selectedLayerIndex == i);
-
- // Selection column
- ImGui::TableSetColumnIndex(0);
- ImGui::PushID(i);
- if (ImGui::Selectable("##select", isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) {
- m_selectedLayerIndex = i;
- }
- ImGui::PopID();
-
- // Name column
- ImGui::TableSetColumnIndex(1);
- ImVec4 textColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
- if (layerInfo.isMuted) {
- textColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f);
- } else if (layerInfo.isRootLayer) {
- textColor = ImVec4(0.5f, 1.0f, 0.5f, 1.0f);
- } else if (layerInfo.isSessionLayer) {
- textColor = ImVec4(0.5f, 0.7f, 1.0f, 1.0f);
- }
-
- ImGui::TextColored(textColor, "%s", layerInfo.displayName.c_str());
-
- if (ImGui::IsItemHovered() && !layerInfo.realPath.empty()) {
- ImGui::SetTooltip("%s\n%s", layerInfo.identifier.c_str(), layerInfo.realPath.c_str());
- }
-
- // Mute toggle
- ImGui::TableSetColumnIndex(2);
- bool muted = layerInfo.isMuted;
- ImGui::PushID(("mute_" + std::to_string(i)).c_str());
- if (ImGui::Checkbox("##muted", &muted)) {
- if (muted) {
- m_layerManager->MuteLayer(layerInfo.identifier);
- } else {
- m_layerManager->UnmuteLayer(layerInfo.identifier);
- }
- }
- ImGui::PopID();
-
- // Type column
- ImGui::TableSetColumnIndex(3);
- if (layerInfo.isRootLayer) {
- ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Root");
- } else if (layerInfo.isSessionLayer) {
- ImGui::TextColored(ImVec4(0.5f, 0.7f, 1.0f, 1.0f), "Session");
- } else if (layerInfo.isAnonymous) {
- ImGui::Text("Anonymous");
- } else {
- ImGui::Text("Sublayer");
- }
-
- // Context menu
- RenderLayerContextMenu(i);
- }
-
- ImGui::EndTable();
- }
-}
-
-void LayerPanel::RenderLayerContextMenu(int layerIndex) {
- if (ImGui::BeginPopupContextItem(("layer_ctx_" + std::to_string(layerIndex)).c_str())) {
- auto layers = m_layerManager->GetLayerStack();
- if (layerIndex < 0 || layerIndex >= static_cast(layers.size())) {
- ImGui::EndPopup();
- return;
- }
-
- const auto& info = layers[layerIndex];
-
- if (ImGui::MenuItem(info.isMuted ? "Unmute" : "Mute")) {
- if (info.isMuted) {
- m_layerManager->UnmuteLayer(info.identifier);
- } else {
- m_layerManager->MuteLayer(info.identifier);
- }
- }
-
- ImGui::Separator();
-
- if (!info.isRootLayer && !info.isSessionLayer) {
- if (ImGui::MenuItem("Move Up", nullptr, false, layerIndex > 0)) {
- if (m_commandHistory) {
- // Capture order before move.
- auto layers = m_layerManager->GetLayerStack();
- std::vector before, after;
- for (auto& l : layers)
- if (!l.isRootLayer && !l.isSessionLayer)
- before.push_back(l.identifier);
- after = before;
- // Find index within sublayer-only list.
- int subIdx = -1;
- for (int i = 0; i < static_cast(before.size()); ++i)
- if (before[i] == info.identifier) { subIdx = i; break; }
- if (subIdx > 0) std::swap(after[subIdx], after[subIdx - 1]);
- m_commandHistory->Push(std::make_unique(
- m_layerManager, before, after));
- } else {
- m_layerManager->MoveSublayerUp(layerIndex);
- }
- }
- if (ImGui::MenuItem("Move Down", nullptr, false, layerIndex < static_cast(layers.size()) - 1)) {
- if (m_commandHistory) {
- auto layersNow = m_layerManager->GetLayerStack();
- std::vector before, after;
- for (auto& l : layersNow)
- if (!l.isRootLayer && !l.isSessionLayer)
- before.push_back(l.identifier);
- after = before;
- int subIdx = -1;
- for (int i = 0; i < static_cast(before.size()); ++i)
- if (before[i] == info.identifier) { subIdx = i; break; }
- if (subIdx >= 0 && subIdx + 1 < static_cast(after.size()))
- std::swap(after[subIdx], after[subIdx + 1]);
- m_commandHistory->Push(std::make_unique(
- m_layerManager, before, after));
- } else {
- m_layerManager->MoveSublayerDown(layerIndex);
- }
- }
-
- ImGui::Separator();
-
- if (ImGui::MenuItem("Remove")) {
- if (m_commandHistory) {
- m_commandHistory->Push(std::make_unique(
- m_layerManager, layerIndex));
- } else {
- m_layerManager->RemoveSublayer(layerIndex);
- }
- }
- }
-
- ImGui::EndPopup();
- }
-}
-
-void LayerPanel::ShowCreateLayerDialog() {
- ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always);
- ImGui::OpenPopup("Create New Layer");
-
- if (ImGui::BeginPopupModal("Create New Layer", &m_showCreateDialog)) {
- ImGui::Text("Layer Name:");
- ImGui::InputText("##name", m_newLayerName, sizeof(m_newLayerName));
-
- ImGui::Spacing();
- ImGui::Text("Save Path:");
- ImGui::InputText("##path", m_newLayerPath, sizeof(m_newLayerPath));
- ImGui::SameLine();
- if (ImGui::Button("Browse...")) {
- // TODO: File save dialog
- }
-
- ImGui::Spacing();
-
- if (ImGui::Button("Create", ImVec2(120, 0))) {
- std::string path;
- if (m_newLayerPath[0] != '\0') {
- path = std::string(m_newLayerPath) + "/" + m_newLayerName;
- } else {
- path = m_newLayerName;
- }
-
- if (!path.empty()) {
- if (m_commandHistory) {
- m_commandHistory->Push(std::make_unique(
- m_layerManager, "./" + path));
- } else {
- m_layerManager->CreateSublayer("./" + path);
- }
- m_showCreateDialog = false;
- }
- }
- ImGui::SameLine();
- if (ImGui::Button("Cancel", ImVec2(120, 0))) {
- m_showCreateDialog = false;
- }
-
- ImGui::EndPopup();
- }
-}
-
-} // namespace UsdLayerManager
\ No newline at end of file
diff --git a/src/ui/LayerPanel.h b/src/ui/LayerPanel.h
deleted file mode 100644
index 5a5de61..0000000
--- a/src/ui/LayerPanel.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma once
-
-#include "../core/LayerManager.h"
-#include "../core/CommandHistory.h"
-#include
-#include
-#include
-#include
-
-namespace UsdLayerManager {
-
-class LayerPanel {
-public:
- LayerPanel();
- ~LayerPanel();
-
- void SetLayerManager(LayerManager* manager);
- void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
- void Render();
-
-private:
- void RenderLayerContextMenu(int layerIndex);
- void ShowCreateLayerDialog();
-
- LayerManager* m_layerManager;
- CommandHistory* m_commandHistory = nullptr;
- int m_selectedLayerIndex;
- bool m_showCreateDialog;
- char m_newLayerPath[256];
- char m_newLayerName[128];
-};
-
-} // namespace UsdLayerManager
\ No newline at end of file
diff --git a/src/ui/StageEditorPanel.cpp b/src/ui/StageEditorPanel.cpp
new file mode 100644
index 0000000..8711a33
--- /dev/null
+++ b/src/ui/StageEditorPanel.cpp
@@ -0,0 +1,392 @@
+#include "StageEditorPanel.h"
+#include "../utils/FileDialog.h"
+#include "../core/commands/LayerCommands.h"
+#include
+#include
+#include
+
+namespace UsdLayerManager {
+
+StageEditorPanel::StageEditorPanel() {
+ m_newLayerPath[0] = '\0';
+ m_newLayerName[0] = '\0';
+}
+
+StageEditorPanel::~StageEditorPanel() {}
+
+// ---------------------------------------------------------------------------
+void StageEditorPanel::Render() {
+ if (!m_layerManager) return;
+
+ auto iconBtn = [&](const char* id, Icon icon, const char* fallback) -> bool {
+ if (m_iconManager) {
+ ImTextureID tex = m_iconManager->Get(icon);
+ return ImGui::ImageButton(id, ImTextureRef(tex), ImVec2(18.f, 18.f));
+ }
+ return ImGui::Button(fallback);
+ };
+
+ // Toolbar
+ ImGui::Text("Stage Editor");
+ ImGui::SameLine(ImGui::GetWindowWidth() - 100);
+
+ if (iconBtn("##refresh", Icon::Refresh, "R"))
+ m_layerManager->Refresh();
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh");
+
+ ImGui::SameLine();
+ if (iconBtn("##addExisting", Icon::FolderOpen, "+E")) {
+ std::string path = FileDialog::OpenFile(
+ "USD Files (*.usd;*.usda;*.usdc)\0*.usd;*.usda;*.usdc\0All Files (*.*)\0*.*\0",
+ "Add Sublayer");
+ if (!path.empty()) {
+ if (m_commandHistory)
+ m_commandHistory->Push(
+ std::make_unique(m_layerManager, path));
+ else
+ m_layerManager->CreateSublayer(path);
+ }
+ }
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add Existing Sublayer");
+
+ ImGui::SameLine();
+ if (iconBtn("##createNew", Icon::FilePlus, "+N")) {
+ m_showCreateDialog = true;
+ m_newLayerPath[0] = '\0';
+ strcpy_s(m_newLayerName, "new_layer.usd");
+ }
+ if (ImGui::IsItemHovered()) ImGui::SetTooltip("Create New Sublayer");
+
+ ImGui::Separator();
+
+ auto allLayers = m_layerManager->GetLayerStack();
+ if (allLayers.empty()) {
+ ImGui::TextDisabled("No stage loaded");
+ if (m_showCreateDialog) ShowCreateLayerDialog();
+ return;
+ }
+
+ std::vector fixed, sublayers;
+ for (const auto& li : allLayers) {
+ if (li.isRootLayer || li.isSessionLayer)
+ fixed.push_back(li);
+ else
+ sublayers.push_back(li);
+ }
+
+ RenderFixedLayers(fixed);
+
+ ImGui::Spacing();
+ ImGui::TextDisabled("Sublayers");
+ ImGui::Separator();
+
+ RenderSublayerList(sublayers);
+
+ if (m_showCreateDialog) ShowCreateLayerDialog();
+}
+
+// ---------------------------------------------------------------------------
+void StageEditorPanel::RenderFixedLayers(const std::vector& layers) {
+ if (layers.empty()) return;
+
+ if (!ImGui::BeginTable("FixedLayerTable", 4,
+ ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
+ ImGuiTableFlags_SizingFixedFit))
+ return;
+
+ ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f);
+ ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
+ ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70.0f);
+ ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f);
+
+ for (const auto& li : layers) {
+ ImGui::TableNextRow();
+ ImGui::PushID(li.identifier.c_str());
+
+ ImGui::TableSetColumnIndex(0);
+ if (li.isDirty)
+ ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*");
+ else
+ ImGui::TextDisabled(" ");
+
+ ImGui::TableSetColumnIndex(1);
+ ImVec4 color = li.isRootLayer
+ ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f)
+ : ImVec4(0.5f, 0.7f, 1.0f, 1.0f);
+ ImGui::TextColored(color, "%s", li.displayName.c_str());
+ if (ImGui::IsItemHovered() && !li.realPath.empty())
+ ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str());
+
+ ImGui::TableSetColumnIndex(2);
+ if (li.isRootLayer)
+ ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Root");
+ else
+ ImGui::TextColored(ImVec4(0.5f, 0.7f, 1.0f, 1.0f), "Session");
+
+ ImGui::TableSetColumnIndex(3);
+ {
+ bool isET = li.isEditTarget;
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
+ if (ImGui::Checkbox("##et", &isET) && isET)
+ m_layerManager->SetEditTarget(li.identifier);
+ ImGui::PopStyleVar();
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target");
+ }
+
+ ImGui::PopID();
+ }
+
+ ImGui::EndTable();
+}
+
+// ---------------------------------------------------------------------------
+void StageEditorPanel::RenderSublayerList(const std::vector& sublayers) {
+ if (sublayers.empty()) {
+ ImGui::TextDisabled(" No sublayers");
+ return;
+ }
+
+ float tableHeight = ImGui::GetContentRegionAvail().y;
+
+ if (!ImGui::BeginTable("SublayerTable", 5,
+ 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);
+
+ // 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");
+
+ 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_selectedIdx == i);
+ if (ImGui::Selectable("##row", isSelected,
+ ImGuiSelectableFlags_SpanAllColumns |
+ ImGuiSelectableFlags_AllowOverlap,
+ ImVec2(0, 0)))
+ m_selectedIdx = 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("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("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(" ");
+
+ // 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());
+
+ // 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));
+ if (ImGui::Checkbox("##et", &isET) && isET)
+ m_layerManager->SetEditTarget(li.identifier);
+ ImGui::PopStyleVar();
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target");
+ }
+
+ // Col 4: mute checkbox
+ ImGui::TableSetColumnIndex(4);
+ {
+ bool muted = li.isMuted;
+ ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
+ if (ImGui::Checkbox("##muted", &muted)) {
+ if (muted) m_layerManager->MuteLayer(li.identifier);
+ else m_layerManager->UnmuteLayer(li.identifier);
+ }
+ ImGui::PopStyleVar();
+ }
+
+ ImGui::PopID();
+ }
+
+ ImGui::EndTable();
+}
+
+// ---------------------------------------------------------------------------
+void StageEditorPanel::RenderSublayerContextMenu(int i,
+ const std::vector& sublayers) {
+ if (!ImGui::BeginPopupContextItem(("ctx_" + std::to_string(i)).c_str()))
+ return;
+
+ const auto& li = sublayers[i];
+
+ if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute")) {
+ if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
+ else m_layerManager->MuteLayer(li.identifier);
+ }
+
+ if (!li.isEditTarget && ImGui::MenuItem("Set as Edit Target"))
+ m_layerManager->SetEditTarget(li.identifier);
+
+ ImGui::Separator();
+
+ 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]);
+ };
+
+ if (ImGui::MenuItem("Move Up", nullptr, false, canUp)) {
+ 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();
+ }
+ }
+
+ if (ImGui::MenuItem("Move Down", nullptr, false, canDown)) {
+ 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::Separator();
+
+ if (ImGui::MenuItem("Remove")) {
+ if (m_commandHistory)
+ m_commandHistory->Push(
+ std::make_unique(m_layerManager, i));
+ else
+ m_layerManager->RemoveSublayer(i);
+ }
+
+ ImGui::EndPopup();
+}
+
+// ---------------------------------------------------------------------------
+void StageEditorPanel::ShowCreateLayerDialog() {
+ ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always);
+ ImGui::OpenPopup("Create Sublayer");
+
+ if (ImGui::BeginPopupModal("Create Sublayer", &m_showCreateDialog)) {
+ ImGui::Text("Layer Name:");
+ ImGui::InputText("##name", m_newLayerName, sizeof(m_newLayerName));
+
+ ImGui::Spacing();
+ ImGui::Text("Save Path:");
+ ImGui::InputText("##path", m_newLayerPath, sizeof(m_newLayerPath));
+
+ ImGui::Spacing();
+
+ if (ImGui::Button("Create", ImVec2(120, 0))) {
+ std::string path;
+ if (m_newLayerPath[0] != '\0')
+ path = std::string(m_newLayerPath) + "/" + m_newLayerName;
+ else
+ path = m_newLayerName;
+
+ if (!path.empty()) {
+ if (m_commandHistory)
+ m_commandHistory->Push(std::make_unique(
+ m_layerManager, "./" + path));
+ else
+ m_layerManager->CreateSublayer("./" + path);
+ m_showCreateDialog = false;
+ }
+ }
+ ImGui::SameLine();
+ if (ImGui::Button("Cancel", ImVec2(120, 0)))
+ m_showCreateDialog = false;
+
+ ImGui::EndPopup();
+ }
+}
+
+} // namespace UsdLayerManager
diff --git a/src/ui/StageEditorPanel.h b/src/ui/StageEditorPanel.h
new file mode 100644
index 0000000..399b586
--- /dev/null
+++ b/src/ui/StageEditorPanel.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "../core/LayerManager.h"
+#include "../core/CommandHistory.h"
+#include "IconManager.h"
+#include
+#include
+#include
+
+namespace UsdLayerManager {
+
+class StageEditorPanel {
+public:
+ StageEditorPanel();
+ ~StageEditorPanel();
+
+ void SetLayerManager(LayerManager* manager) { m_layerManager = manager; }
+ void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
+ void SetIconManager(IconManager* icons) { m_iconManager = icons; }
+ void Render();
+
+private:
+ void RenderFixedLayers(const std::vector& layers);
+ void RenderSublayerList(const std::vector& sublayers);
+ void RenderSublayerContextMenu(int i, const std::vector& sublayers);
+ void ShowCreateLayerDialog();
+
+ LayerManager* m_layerManager = nullptr;
+ CommandHistory* m_commandHistory = nullptr;
+ IconManager* m_iconManager = nullptr;
+ int m_selectedIdx = -1;
+ bool m_showCreateDialog = false;
+ char m_newLayerPath[256];
+ char m_newLayerName[128];
+};
+
+} // namespace UsdLayerManager