Stage Editor Panel — sublayer editing with ET control, mute, reorder, dirty state

Replace LayerPanel with StageEditorPanel for full sublayer composition workflow:
- Edit target (ET) checkbox on root/session and each sublayer; pen icon header
- Dirty indicator (*) per layer; muted layers remain visible in list
- Drag-drop and Move Up/Down reordering with undo (LayerReorderCommand)
- Add existing sublayer via file dialog; create new sublayer modal
- Fixed LayerReorderCommand::ApplyOrder using full-stack indices on sublayer-local
  list (silent remove failure → duplicate insert → USD _ValidateEdit error)
- Fixed muted sublayer visibility: walk GetSubLayerPaths() + FindOrOpenRelativeToLayer
  + m_layerRefs to keep muted layers alive and visible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 16:37:30 +08:00
parent 9ec9761dd7
commit 9816fb8759
13 changed files with 514 additions and 333 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-288-128 0c-17.7 0-32-14.3-32-32L224 0 64 0zM256 0l0 128 128 0L256 0zM216 232l0 48 48 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-48 0 0 48c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-48-48 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0 0-48c0-13.3 10.7-24 24-24s24 10.7 24 24z"/></svg>

After

Width:  |  Height:  |  Size: 634 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>

After

Width:  |  Height:  |  Size: 552 B

+48 -20
View File
@@ -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());
}
+8 -1
View File
@@ -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<LayerInfo> m_layers;
// Strong references keep sublayers alive even when the stage drops them after muting.
std::vector<pxr::SdfLayerRefPtr> m_layerRefs;
};
} // namespace UsdLayerManager
+10 -18
View File
@@ -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<int>(layers.size())) {
m_savedPath = layers[index].identifier;
m_description = "Remove Layer " + layers[index].displayName;
auto sublayers = mgr->GetSublayers();
if (index >= 0 && index < static_cast<int>(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<std::string>& 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<int> sublayerIndices;
for (int i = 0; i < static_cast<int>(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<int>(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<int>(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);
}
+7 -6
View File
@@ -57,9 +57,9 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig
m_layerManager = std::make_unique<LayerManager>();
m_propertyManager = std::make_unique<PropertyManager>();
m_propertyManager->SetCommandHistory(&m_commandHistory);
m_layerPanel = std::make_unique<LayerPanel>();
m_layerPanel->SetLayerManager(m_layerManager.get());
m_layerPanel->SetCommandHistory(&m_commandHistory);
m_stageEditorPanel = std::make_unique<StageEditorPanel>();
m_stageEditorPanel->SetLayerManager(m_layerManager.get());
m_stageEditorPanel->SetCommandHistory(&m_commandHistory);
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
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();
+2 -2
View File
@@ -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<LayerManager> m_layerManager;
std::unique_ptr<PropertyManager> m_propertyManager;
CommandHistory m_commandHistory;
std::unique_ptr<LayerPanel> m_layerPanel;
std::unique_ptr<StageEditorPanel> m_stageEditorPanel;
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
std::unique_ptr<ViewportPanel> m_viewportPanel;
std::unique_ptr<PropertyPanel> m_propertyPanel;
+4
View File
@@ -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,
};
// ---------------------------------------------------------------------------
+4
View File
@@ -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
-253
View File
@@ -1,253 +0,0 @@
#include "LayerPanel.h"
#include "../utils/Logger.h"
#include "../core/commands/LayerCommands.h"
#include <imgui.h>
#include <memory>
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<int>(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<int>(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<std::string> 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<int>(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<LayerReorderCommand>(
m_layerManager, before, after));
} else {
m_layerManager->MoveSublayerUp(layerIndex);
}
}
if (ImGui::MenuItem("Move Down", nullptr, false, layerIndex < static_cast<int>(layers.size()) - 1)) {
if (m_commandHistory) {
auto layersNow = m_layerManager->GetLayerStack();
std::vector<std::string> 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<int>(before.size()); ++i)
if (before[i] == info.identifier) { subIdx = i; break; }
if (subIdx >= 0 && subIdx + 1 < static_cast<int>(after.size()))
std::swap(after[subIdx], after[subIdx + 1]);
m_commandHistory->Push(std::make_unique<LayerReorderCommand>(
m_layerManager, before, after));
} else {
m_layerManager->MoveSublayerDown(layerIndex);
}
}
ImGui::Separator();
if (ImGui::MenuItem("Remove")) {
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<LayerRemoveCommand>(
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<LayerCreateCommand>(
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
-33
View File
@@ -1,33 +0,0 @@
#pragma once
#include "../core/LayerManager.h"
#include "../core/CommandHistory.h"
#include <imgui.h>
#include <memory>
#include <string>
#include <functional>
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
+392
View File
@@ -0,0 +1,392 @@
#include "StageEditorPanel.h"
#include "../utils/FileDialog.h"
#include "../core/commands/LayerCommands.h"
#include <imgui.h>
#include <memory>
#include <string>
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<LayerCreateCommand>(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<LayerInfo> 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<LayerInfo>& 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<LayerInfo>& 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<int>(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<const int*>(payload->Data);
int dst = i;
if (src != dst) {
std::vector<std::string> 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<LayerReorderCommand>(
m_layerManager, before, after));
} else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(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<LayerInfo>& 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<int>(sublayers.size()) - 1);
auto buildBeforeAfter = [&](int a, int b,
std::vector<std::string>& before,
std::vector<std::string>& 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<std::string> before, after;
buildBeforeAfter(i, i - 1, before, after);
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
if (ImGui::MenuItem("Move Down", nullptr, false, canDown)) {
std::vector<std::string> before, after;
buildBeforeAfter(i, i + 1, before, after);
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
else {
LayerReorderCommand cmd(m_layerManager, before, after);
static_cast<ICommand&>(cmd).Execute();
}
}
ImGui::Separator();
if (ImGui::MenuItem("Remove")) {
if (m_commandHistory)
m_commandHistory->Push(
std::make_unique<LayerRemoveCommand>(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<LayerCreateCommand>(
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
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "../core/LayerManager.h"
#include "../core/CommandHistory.h"
#include "IconManager.h"
#include <imgui.h>
#include <memory>
#include <string>
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<LayerInfo>& layers);
void RenderSublayerList(const std::vector<LayerInfo>& sublayers);
void RenderSublayerContextMenu(int i, const std::vector<LayerInfo>& 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