Files
UsdLayerManager/src/ui/Application.cpp
T
indigo a62764397c Scene Hierarchy ET switcher + Create Sublayer via Save dialog
- Add edit-target layer combo at top of Scene Hierarchy panel (always
  visible when stage loaded — session, root, sublayers); routes through
  LayerManager::SetEditTarget so StageEditor ET checkboxes stay in sync
- Replace custom Create Sublayer modal with native SaveFile dialog;
  LayerManager::CreateNewSublayer creates the file on disk, inserts it
  at the top of the sublayer stack, and activates it as edit target

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 03:49:35 +08:00

583 lines
20 KiB
C++

#include "Application.h"
#include "../utils/Logger.h"
#include "../utils/FileDialog.h"
#include "../utils/PathUtils.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/references.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/tf/token.h>
#include <imgui.h>
#include <imgui_internal.h>
#include <vector>
#include <string>
#include <filesystem>
#include <algorithm>
#include <cctype>
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// Internal helper: convert a raw string into a valid USD identifier.
// ---------------------------------------------------------------------------
static std::string SanitizeUsdNameApp(const std::string& raw) {
std::string result;
result.reserve(raw.size());
for (char c : raw) {
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_')
result += c;
else
result += '_';
}
if (result.empty() || std::isdigit(static_cast<unsigned char>(result[0])))
result = "_" + result;
return result;
}
Application::Application()
: m_showDemoWindow(false)
, m_showStageInfo(true)
, m_showStageEditor(true)
, m_showSceneHierarchy(true)
, m_showViewport(true)
, m_showPropertyPanel(true)
, m_showTimeline(true)
, m_running(false) {
}
Application::~Application() {
Shutdown();
}
bool Application::Initialize(const std::string& windowTitle, int width, int height) {
LOG_INFO("Initializing USD Layer Manager Application...");
// Create and initialize ImGui context
m_imguiContext = std::make_unique<ImGuiContext>();
if (!m_imguiContext->Initialize(windowTitle, width, height)) {
LOG_ERROR("Failed to initialize ImGui context");
return false;
}
// Create managers
m_stageManager = std::make_unique<UsdStageManager>();
m_layerManager = std::make_unique<LayerManager>();
m_propertyManager = std::make_unique<PropertyManager>();
m_propertyManager->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);
m_sceneHierarchyPanel->SetLayerManager(m_layerManager.get());
m_viewportPanel = std::make_unique<ViewportPanel>();
m_viewportPanel->SetCommandHistory(&m_commandHistory);
m_propertyPanel = std::make_unique<PropertyPanel>();
m_propertyPanel->SetPropertyManager(m_propertyManager.get());
m_propertyPanel->SetCommandHistory(&m_commandHistory);
m_timelinePanel = std::make_unique<TimelinePanel>();
m_timelinePanel->OnTimeChanged = [this](pxr::UsdTimeCode displayTime,
pxr::UsdTimeCode editTime) {
m_viewportPanel->SetTimeCodes(displayTime, editTime);
m_propertyPanel->SetTimeCodes(displayTime, editTime);
};
// Initialize IconManager — must happen after OpenGL context is ready (ImGui init above).
m_iconManager = std::make_unique<IconManager>();
m_iconManager->Initialize(ResourcePath("resources/icons"), 24);
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) {
m_viewportPanel->SetSelectedPrimPath(path);
m_propertyPanel->SetSelectedPrimPath(path);
});
m_sceneHierarchyPanel->SetOnStageMetadataChanged(
[this]() {
RefreshManagers();
});
// Single click in viewport → sync hierarchy + property panel
m_viewportPanel->OnPrimPicked = [this](const std::string& path) {
m_sceneHierarchyPanel->SetSelectedPath(path);
m_propertyPanel->SetSelectedPrimPath(path);
};
// Rect drag in viewport → sync hierarchy + property panel (primary path)
m_viewportPanel->OnPrimsPickedRect = [this](const std::vector<std::string>& paths) {
m_sceneHierarchyPanel->SetSelectedPaths(paths);
m_propertyPanel->SetSelectedPrimPath(paths.empty() ? "" : paths.front());
};
if (!m_stageManager->CreateInMemoryStage()) {
LOG_ERROR("Failed to create default in-memory stage");
} else {
RefreshManagers();
}
LOG_INFO("Application initialized successfully");
return true;
}
void Application::Run() {
LOG_INFO("Starting application main loop...");
m_running = true;
while (m_running && m_imguiContext->ProcessEvents()) {
Update();
RenderUI();
}
LOG_INFO("Application main loop ended");
}
void Application::Shutdown() {
m_viewportPanel.reset();
m_sceneHierarchyPanel.reset();
m_propertyPanel.reset();
m_stageEditorPanel.reset();
m_propertyManager.reset();
m_layerManager.reset();
if (m_iconManager) {
m_iconManager->Shutdown();
m_iconManager.reset();
}
if (m_stageManager) {
m_stageManager->CloseStage();
m_stageManager.reset();
}
if (m_imguiContext) {
LOG_INFO("Shutting down application...");
m_imguiContext->Shutdown();
m_imguiContext.reset();
}
}
void Application::RefreshManagers() {
m_commandHistory.Clear();
if (m_stageManager->HasStage()) {
auto stage = m_stageManager->GetStage();
m_layerManager->SetStage(stage);
m_propertyManager->SetStage(stage);
m_sceneHierarchyPanel->SetStage(stage);
m_viewportPanel->SetStage(stage);
m_viewportPanel->FrameScene();
m_propertyPanel->SetStage(stage);
m_timelinePanel->SetStage(stage);
} else {
m_layerManager->SetStage(nullptr);
m_propertyManager->SetStage(nullptr);
m_sceneHierarchyPanel->SetStage(nullptr);
m_viewportPanel->SetStage(nullptr);
m_propertyPanel->SetStage(nullptr);
m_timelinePanel->SetStage(nullptr);
}
}
void Application::Update() {
ImGuiIO& io = ImGui::GetIO();
m_timelinePanel->Update(io.DeltaTime);
// Process undo/redo hotkeys (Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z).
// Only fire when no ImGui text-input widget has keyboard focus.
if (!io.WantTextInput) {
if (io.KeyCtrl && !io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z, false)) {
m_commandHistory.Undo();
}
if (io.KeyCtrl && (ImGui::IsKeyPressed(ImGuiKey_Y, false) ||
(io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z, false)))) {
m_commandHistory.Redo();
}
}
}
void Application::RenderUI() {
m_imguiContext->NewFrame();
// Status bar must be rendered before DockSpaceOverViewport so it
// reserves space at the bottom before the dockspace claims the rest.
RenderStatusBar();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
if (m_showDemoWindow) {
ImGui::ShowDemoWindow(&m_showDemoWindow);
}
RenderMenuBar();
if (m_showStageInfo && m_stageManager->HasStage()) {
RenderStageInfo();
}
if (m_showStageEditor) {
ImGui::Begin("Stage Editor", &m_showStageEditor, ImGuiWindowFlags_NoCollapse);
m_stageEditorPanel->Render();
ImGui::End();
}
if (m_showViewport) {
m_viewportPanel->Render(&m_showViewport);
}
// Scene Hierarchy is rendered AFTER the viewport so that viewport picks
// (OnPrimPicked / OnPrimsPickedRect) are visible to the hierarchy in the
// same frame — eliminating the one-frame-late scroll/highlight lag.
if (m_showSceneHierarchy) {
ImGui::Begin("Scene Hierarchy", &m_showSceneHierarchy, ImGuiWindowFlags_NoCollapse);
m_sceneHierarchyPanel->Render();
ImGui::End();
}
if (m_showPropertyPanel) {
ImGui::Begin("Property Panel", &m_showPropertyPanel, ImGuiWindowFlags_NoCollapse);
m_propertyPanel->Render();
ImGui::End();
}
if (m_showTimeline) {
ImGui::Begin("Timeline", &m_showTimeline, ImGuiWindowFlags_NoCollapse);
m_timelinePanel->Render();
ImGui::End();
}
m_imguiContext->Render();
}
void Application::RenderStatusBar() {
ImGuiViewport* vp = ImGui::GetMainViewport();
float height = ImGui::GetFrameHeight();
ImGuiWindowFlags flags =
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_MenuBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 2.f));
bool open = ImGui::BeginViewportSideBar("##statusbar", vp,
ImGuiDir_Down, height, flags);
ImGui::PopStyleVar();
if (open) {
ImGui::BeginMenuBar();
// FPS
ImGuiIO& io = ImGui::GetIO();
char buf[32];
snprintf(buf, sizeof(buf), "FPS: %.1f", io.Framerate);
// Right-align: measure text, then position cursor.
float textW = ImGui::CalcTextSize(buf).x;
float avail = ImGui::GetContentRegionAvail().x;
if (avail > textW)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - textW);
ImGui::TextDisabled("%s", buf);
ImGui::EndMenuBar();
}
ImGui::End();
}
void Application::RenderMenuBar() {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Open...", "Ctrl+O")) {
OpenUsdFile();
}
if (ImGui::MenuItem("New", "Ctrl+N")) {
CreateNewUsdFile();
}
ImGui::Separator();
bool hasStage = m_stageManager->HasStage();
if (ImGui::MenuItem("Save", "Ctrl+S", false, hasStage)) {
SaveUsdFile();
}
if (ImGui::MenuItem("Save As...", "Ctrl+Shift+S", false, hasStage)) {
SaveUsdFileAs();
}
ImGui::Separator();
if (ImGui::MenuItem("Close", nullptr, false, hasStage)) {
CloseUsdFile();
}
ImGui::Separator();
if (ImGui::MenuItem("Exit", "Alt+F4")) {
m_running = false;
}
ImGui::EndMenu();
}
// Edit menu — Undo / Redo
{
bool canUndo = m_commandHistory.CanUndo();
bool canRedo = m_commandHistory.CanRedo();
std::string undoLabel = canUndo
? ("Undo: " + m_commandHistory.GetUndoDescription())
: "Undo";
std::string redoLabel = canRedo
? ("Redo: " + m_commandHistory.GetRedoDescription())
: "Redo";
if (ImGui::BeginMenu("Edit")) {
ImGui::BeginDisabled(!canUndo);
if (ImGui::MenuItem(undoLabel.c_str(), "Ctrl+Z"))
m_commandHistory.Undo();
ImGui::EndDisabled();
ImGui::BeginDisabled(!canRedo);
if (ImGui::MenuItem(redoLabel.c_str(), "Ctrl+Y"))
m_commandHistory.Redo();
ImGui::EndDisabled();
ImGui::EndMenu();
}
}
// Stage editing menu — always available (default stage is always present).
bool hasStage = m_stageManager->HasStage();
if (ImGui::BeginMenu("Stage", hasStage)) {
if (ImGui::MenuItem("Add Reference...")) {
AddReferenceToStage();
}
ImGui::Separator();
if (ImGui::BeginMenu("Create Prim")) {
static const char* primTypes[] = {
"Xform", "Scope",
"Mesh", "Sphere", "Cube", "Cylinder", "Cone", "Capsule",
"Camera",
"SphereLight", "DomeLight", "RectLight", "DiskLight", "CylinderLight", "DistantLight"
};
for (const char* t : primTypes) {
if (ImGui::MenuItem(t)) {
CreatePrimOnStage(t);
}
}
ImGui::EndMenu();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Stage Editor", nullptr, &m_showStageEditor);
ImGui::MenuItem("Scene Hierarchy", nullptr, &m_showSceneHierarchy);
ImGui::MenuItem("Viewport", nullptr, &m_showViewport);
ImGui::MenuItem("Property Panel", nullptr, &m_showPropertyPanel);
ImGui::MenuItem("Timeline", nullptr, &m_showTimeline);
ImGui::Separator();
ImGui::MenuItem("Stage Info", nullptr, &m_showStageInfo);
ImGui::MenuItem("Demo Window", nullptr, &m_showDemoWindow);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About")) {
// Future: Show about dialog
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void Application::RenderStageInfo() {
ImGui::Begin("Stage Info", &m_showStageInfo);
if (m_stageManager->HasStage()) {
ImGui::Text("Root Layer:");
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "%s",
m_stageManager->GetRootLayerIdentifier().c_str());
std::string realPath = m_stageManager->GetRootLayerPath();
if (!realPath.empty()) {
ImGui::Text("Real Path:");
ImGui::SameLine();
ImGui::TextWrapped("%s", realPath.c_str());
}
auto stage = m_stageManager->GetStage();
if (stage) {
ImGui::Separator();
ImGui::Text("Pseudo Root: %s", stage->GetPseudoRoot().GetPath().GetText());
ImGui::Text("Default Prim: %s",
stage->HasDefaultPrim() ? stage->GetDefaultPrim().GetPath().GetText() : "(none)");
}
} else {
ImGui::TextDisabled("No stage loaded");
}
ImGui::End();
}
void Application::OpenUsdFile() {
std::string filePath = FileDialog::OpenFile(
"USD Files (*.usd;*.usda;*.usdc)\0*.usd;*.usda;*.usdc\0All Files (*.*)\0*.*\0",
"Open USD File",
m_imguiContext->GetWindowHandle()
);
if (!filePath.empty()) {
if (m_stageManager->OpenStage(filePath)) {
RefreshManagers();
} else {
LOG_ERROR("Failed to open USD file: " + m_stageManager->GetLastError());
}
}
}
void Application::CreateNewUsdFile() {
// Create a fresh anonymous in-memory stage — no file path required.
// The user can save via File > Save As... when they are ready.
if (m_stageManager->CreateInMemoryStage()) {
RefreshManagers();
LOG_INFO("Created new default in-memory stage");
} else {
LOG_ERROR("Failed to create new in-memory stage");
}
}
void Application::SaveUsdFile() {
if (!m_stageManager->HasStage()) {
return;
}
// If the current stage is in-memory (anonymous root layer), fall through to
// Save As — UsdStage::Save() cannot write anonymous layers to disk.
auto rootLayer = m_stageManager->GetRootLayer();
if (!rootLayer || rootLayer->IsAnonymous()) {
SaveUsdFileAs();
return;
}
if (!m_stageManager->SaveStage()) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
}
}
void Application::SaveUsdFileAs() {
if (!m_stageManager->HasStage()) {
return;
}
std::string filePath = FileDialog::SaveFile(
"USD Files (*.usd;*.usda;*.usdc)\0*.usd;*.usda;*.usdc\0All Files (*.*)\0*.*\0",
"Save USD File As",
"usd",
m_imguiContext->GetWindowHandle()
);
if (!filePath.empty()) {
if (!m_stageManager->SaveStageAs(filePath)) {
LOG_ERROR("Failed to save USD file: " + m_stageManager->GetLastError());
} else {
// SaveStageAs reopens m_stageManager's stage from the new file path.
// RefreshManagers syncs m_layerManager (and others) to that new stage;
// without this, subsequent sublayer edits go to the old (now stale) stage
// and are silently lost on the next save.
RefreshManagers();
}
}
}
void Application::CloseUsdFile() {
m_stageManager->CloseStage();
// Re-create a fresh default stage so the app is always in an editable state.
if (m_stageManager->CreateInMemoryStage()) {
RefreshManagers();
LOG_INFO("Closed stage — reset to new default in-memory stage");
} else {
RefreshManagers();
LOG_ERROR("Failed to re-create default stage after close");
}
}
void Application::AddReferenceToStage() {
if (!m_stageManager->HasStage()) return;
std::string filePath = FileDialog::OpenFile(
"USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0All Files (*.*)\0*.*\0",
"Add Reference File",
m_imguiContext->GetWindowHandle()
);
if (filePath.empty()) return;
auto stage = m_stageManager->GetStage();
// Derive a valid USD prim name from the file stem.
std::string stem = std::filesystem::path(filePath).stem().string();
std::string xformName = SanitizeUsdNameApp(stem);
if (xformName.empty()) xformName = "Reference";
// Avoid name collision — append _N if the path already exists.
std::string finalName = xformName;
int suffix = 1;
while (stage->GetPrimAtPath(pxr::SdfPath("/" + finalName)).IsValid()) {
finalName = xformName + "_" + std::to_string(suffix++);
}
try {
pxr::SdfPath xformPath("/" + finalName);
pxr::UsdPrim xformPrim = stage->DefinePrim(xformPath, pxr::TfToken("Xform"));
if (xformPrim.IsValid()) {
bool ok = xformPrim.GetReferences().AddReference(filePath);
if (ok) {
LOG_INFO("Added reference '" + filePath + "' under prim: " + xformPath.GetString());
} else {
LOG_ERROR("Failed to add reference '" + filePath + "' to: " + xformPath.GetString());
}
} else {
LOG_ERROR("Failed to define Xform prim: " + xformPath.GetString());
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Add reference error: ") + e.what());
}
}
void Application::CreatePrimOnStage(const std::string& typeName) {
if (!m_stageManager->HasStage()) return;
auto stage = m_stageManager->GetStage();
// Auto-generate a unique prim name from the type (e.g. Sphere → /Sphere, /Sphere_1, …).
std::string baseName = typeName;
std::string finalName = baseName;
int suffix = 1;
while (stage->GetPrimAtPath(pxr::SdfPath("/" + finalName)).IsValid()) {
finalName = baseName + "_" + std::to_string(suffix++);
}
try {
pxr::SdfPath primPath("/" + finalName);
pxr::UsdPrim prim = stage->DefinePrim(primPath, pxr::TfToken(typeName));
if (prim.IsValid()) {
LOG_INFO("Created prim '" + primPath.GetString() + "' of type " + typeName);
// Sync selection to the new prim.
m_sceneHierarchyPanel->SetSelectedPath(primPath.GetString());
m_propertyPanel->SetSelectedPrimPath(primPath.GetString());
} else {
LOG_ERROR("Failed to create prim of type: " + typeName);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Create prim error: ") + e.what());
}
}
} // namespace UsdLayerManager