Init Repo

This commit is contained in:
2026-06-03 09:00:11 +08:00
commit 9be48d8b9e
155 changed files with 14827 additions and 0 deletions
+490
View File
@@ -0,0 +1,490 @@
#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 <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_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_layerPanel = std::make_unique<LayerPanel>();
m_layerPanel->SetLayerManager(m_layerManager.get());
m_layerPanel->SetCommandHistory(&m_commandHistory);
m_sceneHierarchyPanel = std::make_unique<SceneHierarchyPanel>();
m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get());
m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory);
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);
// 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_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_layerPanel.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);
} else {
m_layerManager->SetStage(nullptr);
m_propertyManager->SetStage(nullptr);
m_sceneHierarchyPanel->SetStage(nullptr);
m_viewportPanel->SetStage(nullptr);
m_propertyPanel->SetStage(nullptr);
}
}
void Application::Update() {
// Process undo/redo hotkeys (Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z).
// Only fire when no ImGui text-input widget has keyboard focus.
ImGuiIO& io = ImGui::GetIO();
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();
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
if (m_showDemoWindow) {
ImGui::ShowDemoWindow(&m_showDemoWindow);
}
RenderMenuBar();
if (m_showStageInfo && m_stageManager->HasStage()) {
RenderStageInfo();
}
ImGui::Begin("Layer Panel", nullptr, ImGuiWindowFlags_NoCollapse);
m_layerPanel->Render();
ImGui::End();
m_viewportPanel->Render();
// 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.
ImGui::Begin("Scene Hierarchy", nullptr, ImGuiWindowFlags_NoCollapse);
m_sceneHierarchyPanel->Render();
ImGui::End();
ImGui::Begin("Property Panel", nullptr, ImGuiWindowFlags_NoCollapse);
m_propertyPanel->Render();
ImGui::End();
m_imguiContext->Render();
}
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 Info", nullptr, &m_showStageInfo);
ImGui::MenuItem("Show 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 (!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());
}
}
}
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
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include "ImGuiContext.h"
#include "IconManager.h"
#include "LayerPanel.h"
#include "SceneHierarchyPanel.h"
#include "ViewportPanel.h"
#include "PropertyPanel.h"
#include "../core/UsdStageManager.h"
#include "../core/LayerManager.h"
#include "../core/PropertyManager.h"
#include "../core/CommandHistory.h"
#include <memory>
#include <string>
namespace UsdLayerManager {
class Application {
public:
Application();
~Application();
bool Initialize(const std::string& windowTitle = "USD Layer Manager", int width = 1280, int height = 720);
void Run();
void Shutdown();
private:
void Update();
void RenderUI();
void RenderMenuBar();
void RenderStageInfo();
void RefreshManagers();
// File operations
void OpenUsdFile();
void CreateNewUsdFile(); // creates fresh in-memory stage
void SaveUsdFile();
void SaveUsdFileAs();
void CloseUsdFile(); // closes file-backed stage, falls back to default stage
// Stage editing operations (also exposed via Stage menu)
void AddReferenceToStage();
void CreatePrimOnStage(const std::string& typeName);
std::unique_ptr<ImGuiContext> m_imguiContext;
std::unique_ptr<IconManager> m_iconManager;
std::unique_ptr<UsdStageManager> m_stageManager;
std::unique_ptr<LayerManager> m_layerManager;
std::unique_ptr<PropertyManager> m_propertyManager;
CommandHistory m_commandHistory;
std::unique_ptr<LayerPanel> m_layerPanel;
std::unique_ptr<SceneHierarchyPanel> m_sceneHierarchyPanel;
std::unique_ptr<ViewportPanel> m_viewportPanel;
std::unique_ptr<PropertyPanel> m_propertyPanel;
bool m_showDemoWindow;
bool m_showStageInfo;
bool m_running;
};
} // namespace UsdLayerManager
+189
View File
@@ -0,0 +1,189 @@
#include "IconManager.h"
#include "../utils/Logger.h"
// NanoSVG — header-only SVG parser and rasteriser.
// Define implementation macros in exactly one .cpp file.
#define NANOSVG_IMPLEMENTATION
#include <nanosvg.h>
#define NANOSVGRAST_IMPLEMENTATION
#include <nanosvgrast.h>
// OpenGL (via GLAD)
#include "../utils/GLExt.h"
#include <glad/gl.h>
#include <cstring>
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
static const char* IconFilename(Icon icon) {
switch (icon) {
case Icon::Eye: return "eye.svg";
case Icon::EyeSlash: return "eye-slash.svg";
case Icon::Link: return "link.svg";
case Icon::LinkSlash: return "link-slash.svg";
case Icon::Globe: return "globe.svg";
case Icon::Cube: return "cube.svg";
case Icon::Camera: return "camera.svg";
case Icon::Lightbulb: return "lightbulb.svg";
case Icon::FolderOpen: return "folder-open.svg";
case Icon::ObjectGroup: return "object-group.svg";
case Icon::Swatchbook: return "swatchbook.svg";
case Icon::Code: return "code.svg";
case Icon::LayerGroup: return "layer-group.svg";
case Icon::CircleDot: return "circle-dot.svg";
case Icon::ToolSelect: return "cursor.svg";
case Icon::ToolMove: return "arrows-move.svg";
case Icon::ToolRotate: return "arrows-rotate.svg";
case Icon::ToolScale: return "arrows-scale.svg";
case Icon::WorldSpace: return "world-space.svg";
case Icon::LocalSpace: return "local-space.svg";
case Icon::Grid: return "grid.svg";
case Icon::Antialias: return "antialias.svg";
case Icon::LayoutSingle: return "layout-single.svg";
case Icon::LayoutHSplit: return "layout-hsplit.svg";
case Icon::LayoutVSplit: return "layout-vsplit.svg";
case Icon::LayoutQuad: return "layout-quad.svg";
default: return nullptr;
}
}
static constexpr Icon kAllIcons[] = {
Icon::Eye, Icon::EyeSlash,
Icon::Link, Icon::LinkSlash,
Icon::Globe, Icon::Cube, Icon::Camera, Icon::Lightbulb,
Icon::FolderOpen, Icon::ObjectGroup, Icon::Swatchbook,
Icon::Code, Icon::LayerGroup, Icon::CircleDot,
Icon::ToolSelect, Icon::ToolMove, Icon::ToolRotate, Icon::ToolScale,
Icon::WorldSpace, Icon::LocalSpace, Icon::Grid, Icon::Antialias,
Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad,
};
// ---------------------------------------------------------------------------
// IconManager
// ---------------------------------------------------------------------------
IconManager::IconManager() {}
IconManager::~IconManager() {
Shutdown();
}
bool IconManager::Initialize(const std::string& iconDir, int sizePixels) {
m_sizePixels = sizePixels;
CreateFallback();
bool allOk = true;
for (Icon ic : kAllIcons) {
const char* filename = IconFilename(ic);
if (!filename) continue;
std::string path = iconDir;
if (!path.empty() && path.back() != '/' && path.back() != '\\')
path += '/';
path += filename;
if (!LoadSVG(ic, path)) {
LOG_WARNING(std::string("IconManager: failed to load ") + path);
allOk = false;
}
}
return allOk;
}
void IconManager::Shutdown() {
for (auto& kv : m_textures) {
GLuint tex = static_cast<GLuint>(static_cast<uintptr_t>(kv.second));
if (tex) glDeleteTextures(1, &tex);
}
m_textures.clear();
if (m_fallback != ImTextureID_Invalid) {
GLuint tex = static_cast<GLuint>(static_cast<uintptr_t>(m_fallback));
glDeleteTextures(1, &tex);
m_fallback = ImTextureID_Invalid;
}
}
ImTextureID IconManager::Get(Icon icon) const {
auto it = m_textures.find(static_cast<int>(icon));
if (it != m_textures.end()) return it->second;
return m_fallback;
}
// ---------------------------------------------------------------------------
// private
// ---------------------------------------------------------------------------
void IconManager::CreateFallback() {
// 1×1 transparent pixel
unsigned char pixel[4] = { 0, 0, 0, 0 };
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
glBindTexture(GL_TEXTURE_2D, 0);
m_fallback = static_cast<ImTextureID>(static_cast<uintptr_t>(tex));
}
bool IconManager::LoadSVG(Icon icon, const std::string& path) {
// NanoSVG parses from a mutable char buffer.
NSVGimage* svg = nsvgParseFromFile(path.c_str(), "px", 96.0f);
if (!svg) return false;
if (svg->width <= 0.0f || svg->height <= 0.0f) {
nsvgDelete(svg);
return false;
}
// Override every shape's fill/stroke to opaque white so the icons render
// correctly on the dark ImGui theme. NanoSVG stores colours as 0xAABBGGRR;
// default fills use NSVG_RGB(0,0,0) which has alpha=0 in the high byte.
// We must force alpha=0xFF (fully opaque) — not preserve the SVG alpha —
// otherwise the rasteriser multiplies by alpha=0 and produces transparent pixels.
for (NSVGshape* shape = svg->shapes; shape != nullptr; shape = shape->next) {
if (shape->fill.type == NSVG_PAINT_COLOR) {
// Force opaque white: RGB channels from existing colour are irrelevant,
// just make every filled shape a solid white mask.
shape->fill.color = 0xFFFFFFFF;
}
if (shape->stroke.type == NSVG_PAINT_COLOR) {
shape->stroke.color = 0xFFFFFFFF;
}
}
// Rasterise at target size.
NSVGrasterizer* rast = nsvgCreateRasterizer();
if (!rast) { nsvgDelete(svg); return false; }
int w = m_sizePixels;
int h = m_sizePixels;
float scale = static_cast<float>(w) / svg->width;
std::vector<unsigned char> pixels(static_cast<size_t>(w * h * 4), 0);
nsvgRasterize(rast, svg, 0.0f, 0.0f, scale, pixels.data(), w, h, w * 4);
nsvgDeleteRasterizer(rast);
nsvgDelete(svg);
// Upload to OpenGL.
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
glBindTexture(GL_TEXTURE_2D, 0);
m_textures[static_cast<int>(icon)] =
static_cast<ImTextureID>(static_cast<uintptr_t>(tex));
return true;
}
} // namespace UsdLayerManager
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <imgui.h>
#include <string>
#include <unordered_map>
namespace UsdLayerManager {
/// Symbolic icon names used throughout the UI.
enum class Icon {
// Visibility
Eye,
EyeSlash,
// Reference state
Link,
LinkSlash,
// Prim types
Globe, // PseudoRoot / World
Cube, // Mesh / Subdiv
Camera,
Lightbulb, // Light
FolderOpen, // Scope
ObjectGroup, // Xform
Swatchbook, // Material
Code, // Shader
LayerGroup, // Model
CircleDot, // Generic prim
// Viewport manipulator tools
ToolSelect, // cursor / arrow-pointer
ToolMove, // four-directional arrows
ToolRotate, // circular arrows
ToolScale, // expand/compress arrows
// Viewport display toggles
WorldSpace, // globe / world coordinate space
LocalSpace, // coordinate axes / local object space
Grid, // ground grid toggle
Antialias, // anti-aliasing toggle
// Viewport layout modes
LayoutSingle, // single viewport
LayoutHSplit, // two panels side-by-side
LayoutVSplit, // two panels top/bottom
LayoutQuad, // four panels 2×2
};
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
/// OpenGL textures, and hands out ImTextureID handles for use with
/// ImGui::Image() / ImGui::ImageButton().
///
/// Lifecycle: Initialize() after OpenGL is ready, Shutdown() before context
/// is destroyed. One global instance is owned by Application.
class IconManager {
public:
IconManager();
~IconManager();
/// Load and rasterize all icons from the given directory.
/// @param iconDir Path to the directory containing the .svg files.
/// @param sizePixels Rasterisation size in pixels (both axes).
bool Initialize(const std::string& iconDir, int sizePixels = 16);
/// Release all GPU textures.
void Shutdown();
/// Return the ImTextureID for the given icon.
/// Returns a 1×1 transparent fallback texture when the icon is missing.
ImTextureID Get(Icon icon) const;
/// Pixel size the icons were rasterised at.
int SizePixels() const { return m_sizePixels; }
ImVec2 SizeVec() const { return ImVec2(static_cast<float>(m_sizePixels),
static_cast<float>(m_sizePixels)); }
private:
bool LoadSVG(Icon icon, const std::string& path);
void CreateFallback();
int m_sizePixels = 16;
ImTextureID m_fallback = ImTextureID_Invalid;
std::unordered_map<int, ImTextureID> m_textures; // key = (int)Icon
};
} // namespace UsdLayerManager
+255
View File
@@ -0,0 +1,255 @@
#include "ImGuiContext.h"
#include "../utils/Logger.h"
#include "../utils/GLExt.h"
#include "../utils/PathUtils.h"
#include <imgui.h>
#include <imgui_impl_win32.h>
#include <imgui_impl_opengl3.h>
// Forward declare message handler from imgui_impl_win32.cpp
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
namespace UsdLayerManager {
ImGuiContext::ImGuiContext()
: m_hwnd(nullptr)
, m_hdc(nullptr)
, m_hglrc(nullptr)
, m_shouldClose(false)
, m_width(1280)
, m_height(720) {
}
ImGuiContext::~ImGuiContext() {
Shutdown();
}
bool ImGuiContext::Initialize(const std::string& windowTitle, int width, int height) {
m_width = width;
m_height = height;
LOG_INFO("Initializing ImGui context...");
// Create application window
WNDCLASSEXW wc = {
sizeof(wc),
CS_OWNDC,
WndProc,
0L,
0L,
GetModuleHandle(nullptr),
nullptr,
nullptr,
nullptr,
nullptr,
L"UsdLayerManager",
nullptr
};
::RegisterClassExW(&wc);
m_hwnd = ::CreateWindowW(
wc.lpszClassName,
L"USD Layer Manager",
WS_OVERLAPPEDWINDOW,
100, 100,
m_width, m_height,
nullptr,
nullptr,
wc.hInstance,
nullptr
);
if (!m_hwnd) {
LOG_ERROR("Failed to create window");
return false;
}
// Store this pointer in window user data
::SetWindowLongPtr(m_hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
// Initialize OpenGL
if (!CreateDeviceWGL()) {
LOG_ERROR("Failed to initialize OpenGL");
::DestroyWindow(m_hwnd);
::UnregisterClassW(wc.lpszClassName, wc.hInstance);
return false;
}
// Initialize OpenGL extensions
if (!GL::InitExtensions()) {
LOG_ERROR("Failed to initialize OpenGL extensions");
}
// Show the window
::ShowWindow(m_hwnd, SW_SHOWDEFAULT);
::UpdateWindow(m_hwnd);
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
// DockingEnable only — NavEnableKeyboard is intentionally omitted for a DCC
// viewport application that manages its own keyboard shortcuts.
// With NavEnableKeyboard set, io.WantCaptureKeyboard becomes true whenever any
// ImGui window is focused, which would block all viewport hotkeys (F, A, etc.).
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Load Inter font.
// The build directory copies the font as Inter.ttc; the install step renames
// it to Inter.ttf. Try .ttf first (install layout), fall back to .ttc (build).
{
ImFontConfig fontConfig;
fontConfig.FontNo = 0; // select Regular face from the .ttc collection
// Build exe-relative font paths so they work from both build and install dirs
std::string fontPath0 = ResourcePath("resources/font/Inter.ttf");
std::string fontPath1 = ResourcePath("resources/font/Inter.ttc");
const char* tryPaths[] = { fontPath0.c_str(), fontPath1.c_str() };
bool loaded = false;
for (const char* p : tryPaths) {
ImFont* f = io.Fonts->AddFontFromFileTTF(p, 16.0f, &fontConfig);
if (f) {
LOG_INFO(std::string("Loaded Inter font from ") + p);
loaded = true;
break;
}
}
if (!loaded) {
LOG_WARNING("Could not load Inter font; using built-in default");
io.Fonts->AddFontDefault();
}
}
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// Setup Platform/Renderer backends
ImGui_ImplWin32_Init(m_hwnd);
ImGui_ImplOpenGL3_Init("#version 130");
LOG_INFO("ImGui context initialized successfully");
return true;
}
void ImGuiContext::Shutdown() {
if (m_hwnd) {
LOG_INFO("Shutting down ImGui context...");
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyPlatformWindows();
ImGui::DestroyContext();
CleanupDeviceWGL();
::DestroyWindow(m_hwnd);
::UnregisterClassW(L"UsdLayerManager", ::GetModuleHandle(nullptr));
m_hwnd = nullptr;
}
}
void ImGuiContext::NewFrame() {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
}
void ImGuiContext::Render() {
ImGui::Render();
glViewport(0, 0, m_width, m_height);
glClearColor(0.45f, 0.55f, 0.60f, 1.00f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
::SwapBuffers(m_hdc);
}
bool ImGuiContext::ProcessEvents() {
MSG msg;
while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
if (msg.message == WM_QUIT) {
m_shouldClose = true;
}
}
return !m_shouldClose;
}
bool ImGuiContext::CreateDeviceWGL() {
m_hdc = ::GetDC(m_hwnd);
PIXELFORMATDESCRIPTOR pfd = {};
pfd.nSize = sizeof(pfd);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 24;
pfd.cStencilBits = 8;
int pixelFormat = ::ChoosePixelFormat(m_hdc, &pfd);
if (pixelFormat == 0) {
LOG_ERROR("ChoosePixelFormat failed");
return false;
}
if (!::SetPixelFormat(m_hdc, pixelFormat, &pfd)) {
LOG_ERROR("SetPixelFormat failed");
return false;
}
m_hglrc = ::wglCreateContext(m_hdc);
if (!m_hglrc) {
LOG_ERROR("wglCreateContext failed");
return false;
}
if (!::wglMakeCurrent(m_hdc, m_hglrc)) {
LOG_ERROR("wglMakeCurrent failed");
return false;
}
return true;
}
void ImGuiContext::CleanupDeviceWGL() {
if (m_hglrc) {
::wglMakeCurrent(nullptr, nullptr);
::wglDeleteContext(m_hglrc);
m_hglrc = nullptr;
}
if (m_hdc) {
::ReleaseDC(m_hwnd, m_hdc);
m_hdc = nullptr;
}
}
LRESULT WINAPI ImGuiContext::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) {
return true;
}
ImGuiContext* context = reinterpret_cast<ImGuiContext*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
switch (msg) {
case WM_SIZE:
if (context && wParam != SIZE_MINIMIZED) {
context->m_width = LOWORD(lParam);
context->m_height = HIWORD(lParam);
}
return 0;
case WM_SYSCOMMAND:
if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu
return 0;
break;
case WM_DESTROY:
::PostQuitMessage(0);
return 0;
}
return ::DefWindowProcW(hWnd, msg, wParam, lParam);
}
} // namespace UsdLayerManager
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <Windows.h>
#include <string>
namespace UsdLayerManager {
class ImGuiContext {
public:
ImGuiContext();
~ImGuiContext();
bool Initialize(const std::string& windowTitle, int width, int height);
void Shutdown();
void NewFrame();
void Render();
bool ShouldClose() const { return m_shouldClose; }
void SetShouldClose(bool value) { m_shouldClose = value; }
HWND GetWindowHandle() const { return m_hwnd; }
// Process Windows messages
bool ProcessEvents();
private:
bool CreateDeviceWGL();
void CleanupDeviceWGL();
static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
HWND m_hwnd;
HDC m_hdc;
HGLRC m_hglrc;
bool m_shouldClose;
int m_width;
int m_height;
};
} // namespace UsdLayerManager
+253
View File
@@ -0,0 +1,253 @@
#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
@@ -0,0 +1,33 @@
#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
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include "../core/PropertyManager.h"
#include "../core/CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <imgui.h>
#include <string>
namespace UsdLayerManager {
/// Maya Channel Box-style property panel.
/// Displays TRS transform, variant sets, all USD attributes, and relationships
/// for the currently selected prim.
class PropertyPanel {
public:
PropertyPanel();
~PropertyPanel();
void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetStage(pxr::UsdStageRefPtr stage);
void SetSelectedPrimPath(const std::string& path);
void Render();
private:
void ReadTransform();
void WriteTranslate();
void WriteRotate();
void WriteScale();
/// When XformCommonAPI is not applicable (e.g. referenced prim with
/// xformOp:transform), author standard common-API ops in the current edit
/// layer so that subsequent XformCommonAPI writes succeed.
void EnsureCommonAPILayout();
// Layout helpers mirroring usdtweak UsdPrimEditor structure
void RenderPrimHeader(const pxr::UsdPrim& prim); ///< fixed-height header child
void RenderVariantSetsSection(const pxr::UsdPrim& prim);
void RenderTransformSection();
void RenderPropertiesTable(const pxr::UsdPrim& prim); ///< unified attr+rel table
PropertyManager* m_propertyManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
pxr::UsdStageRefPtr m_stage;
std::string m_selectedPrimPath;
// Cached TRS values (float matches DragFloat precision)
pxr::GfVec3f m_translate{ 0.f, 0.f, 0.f };
pxr::GfVec3f m_rotate { 0.f, 0.f, 0.f };
pxr::GfVec3f m_scale { 1.f, 1.f, 1.f };
pxr::UsdGeomXformCommonAPI::RotationOrder
m_rotOrder{ pxr::UsdGeomXformCommonAPI::RotationOrderXYZ };
bool m_hasXform = false;
bool m_isXformable = false;
bool m_needsRead = true;
bool m_isAnyFieldActive = false; ///< true when a DragFloat is being dragged
bool m_xformFallback = false; ///< true when values came from matrix decomposition
// Snapshot of TRS values captured when a DragFloat gains focus,
// used to build the undo command when the field is deactivated.
pxr::GfVec3f m_editStartTranslate{ 0.f, 0.f, 0.f };
pxr::GfVec3f m_editStartRotate { 0.f, 0.f, 0.f };
pxr::GfVec3f m_editStartScale { 1.f, 1.f, 1.f };
std::string m_primType;
};
} // namespace UsdLayerManager
+799
View File
@@ -0,0 +1,799 @@
#include "SceneHierarchyPanel.h"
#include "../utils/Logger.h"
#include "../utils/FileDialog.h"
#include "../core/commands/CreatePrimCommand.h"
#include "../core/commands/DeletePrimCommand.h"
#include "../core/commands/AddReferenceCommand.h"
#include "../core/commands/ReplaceReferenceCommand.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usd/references.h>
#include <pxr/usd/usd/primCompositionQuery.h>
#include <pxr/usd/usdGeom/imageable.h>
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/xform.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/reference.h>
#include <pxr/usd/sdf/primSpec.h>
#include <pxr/base/tf/token.h>
#include <filesystem>
#include <algorithm>
#include <cctype>
#include <unordered_set>
#include <memory>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
/// Convert a file base name (e.g. "my asset.v01") into a valid USD prim name.
/// USD identifiers: [A-Za-z_][A-Za-z0-9_]*
static std::string SanitizeUsdName(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;
}
SceneHierarchyPanel::SceneHierarchyPanel()
: m_propertyManager(nullptr)
, m_stage(nullptr) {
}
SceneHierarchyPanel::~SceneHierarchyPanel() {
}
void SceneHierarchyPanel::SetPropertyManager(PropertyManager* manager) {
m_propertyManager = manager;
}
void SceneHierarchyPanel::SetStage(UsdStageRefPtr stage) {
m_stage = stage;
m_selectedPaths.clear();
m_primarySelectedPath.clear();
m_primarySdfPath = SdfPath();
m_scrollToSelected = false;
}
UsdPrim SceneHierarchyPanel::GetSelectedPrim() const {
if (m_stage && !m_primarySelectedPath.empty()) {
return m_stage->GetPrimAtPath(SdfPath(m_primarySelectedPath));
}
return UsdPrim();
}
void SceneHierarchyPanel::SetSelectedPathFromClick(const std::string& path) {
m_selectedPaths.clear();
m_primarySelectedPath = path;
m_primarySdfPath = path.empty() ? SdfPath() : SdfPath(path);
if (!path.empty()) m_selectedPaths.insert(path);
// No scroll — user clicked the item directly, it's already visible.
m_scrollToSelected = false;
if (m_onPrimSelected) m_onPrimSelected(path);
}
const char* SceneHierarchyPanel::GetPrimTypeIcon(const UsdPrim& prim) const {
// Kept for legacy callers; returns a short ASCII label.
if (prim.IsPseudoRoot()) return "W";
std::string t = prim.GetTypeName().GetString();
if (t.find("Mesh") != std::string::npos) return "G";
if (t.find("Camera") != std::string::npos) return "C";
if (t.find("Light") != std::string::npos) return "L";
if (t.find("Material") != std::string::npos) return "S";
if (t.find("Shader") != std::string::npos) return "S";
if (t.find("Xform") != std::string::npos) return "X";
if (t.find("Scope") != std::string::npos) return "O";
if (prim.IsModel()) return "M";
return "P";
}
Icon SceneHierarchyPanel::GetPrimTypeIconEnum(const UsdPrim& prim) const {
if (prim.IsPseudoRoot()) return Icon::Globe;
std::string t = prim.GetTypeName().GetString();
if (t.find("Mesh") != std::string::npos ||
t.find("Subdiv") != std::string::npos) return Icon::Cube;
if (t.find("Camera") != std::string::npos) return Icon::Camera;
if (t.find("Light") != std::string::npos) return Icon::Lightbulb;
if (t.find("Material") != std::string::npos) return Icon::Swatchbook;
if (t.find("Shader") != std::string::npos) return Icon::Code;
if (t.find("Xform") != std::string::npos) return Icon::ObjectGroup;
if (t.find("Scope") != std::string::npos) return Icon::FolderOpen;
if (prim.IsModel()) return Icon::LayerGroup;
return Icon::CircleDot;
}
void SceneHierarchyPanel::Render() {
if (!m_stage) {
ImGui::TextDisabled("No stage loaded");
return;
}
auto paths = m_propertyManager->GetPrimPaths();
if (paths.empty()) {
ImGui::TextDisabled("No prims in stage");
} else {
UsdPrim root = m_stage->GetPseudoRoot();
// Rebuild local-layer set once per frame (used by RenderPrimNode to
// detect attribute overrides). GetLayerStack() returns only the stage's
// own layers — root layer, sublayers, session layer — NOT reference layers.
m_localLayers.clear();
for (const auto& layer : m_stage->GetLayerStack())
m_localLayers.insert(layer->GetIdentifier());
// ── ImGui Demo "Tables/Tree view" pattern ──────────────────────────
// Col 0 │ Col 1 │ Col 2 │ Col 3
// ▶ Prim│ Type │ Vis │ Ref
//
// The tree node lives in col 0 with ImGuiTreeNodeFlags_SpanAllColumns.
// This makes the selection highlight, IsItemClicked, and SetScrollHereY
// all operate on the FULL ROW rect — the correct ImGui tree-in-table model.
const float kIconW = ImGui::GetTextLineHeight() + 4.0f; // small fixed col width
const ImGuiTableFlags tblFlags =
ImGuiTableFlags_NoBordersInBody |
ImGuiTableFlags_NoPadOuterX |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingFixedFit;
if (ImGui::BeginTable("##primtree", 4, tblFlags)) {
// Col 0 stretches; cols 1-3 are small fixed-width icon columns.
ImGui::TableSetupColumn("##prim", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##type", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##vis", ImGuiTableColumnFlags_WidthFixed, kIconW);
ImGui::TableSetupColumn("##ref", ImGuiTableColumnFlags_WidthFixed, kIconW);
for (const auto& child : root.GetChildren())
RenderPrimNode(child);
ImGui::EndTable();
}
// Deselect when left-clicking on blank space (no prim item hovered).
if (ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsAnyItemHovered())
{
SetSelectedPathFromClick("");
}
}
// Window-level right-click context menu (blank area) for stage-level operations.
if (ImGui::BeginPopupContextWindow("StageContextMenu",
ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems)) {
ImGui::TextDisabled("Stage");
ImGui::Separator();
// ---- Create Prim ----
static const char* kPrimTypes[] = {
"Xform", "Scope",
"Mesh", "Sphere", "Cube", "Cylinder", "Cone", "Capsule",
"Camera",
"SphereLight", "DomeLight", "RectLight", "DiskLight",
"CylinderLight", "DistantLight"
};
if (ImGui::BeginMenu("Create Prim")) {
for (const char* typeName : kPrimTypes) {
if (ImGui::MenuItem(typeName)) {
std::string baseName = typeName;
std::string finalName = baseName;
int suffix = 1;
while (m_stage->GetPrimAtPath(SdfPath("/" + finalName)).IsValid()) {
finalName = baseName + "_" + std::to_string(suffix++);
}
SdfPath primPath("/" + finalName);
if (m_commandHistory) {
auto cmd = std::make_unique<CreatePrimCommand>(
m_stage, primPath, TfToken(typeName));
m_commandHistory->Push(std::move(cmd));
UsdPrim newPrim = m_stage->GetPrimAtPath(primPath);
if (newPrim.IsValid()) SetSelectedPathFromClick(primPath.GetString());
} else {
try {
UsdPrim newPrim = m_stage->DefinePrim(primPath, TfToken(typeName));
if (newPrim.IsValid()) {
LOG_INFO("Created prim '" + primPath.GetString() + "' of type " + baseName);
SetSelectedPathFromClick(primPath.GetString());
} else {
LOG_ERROR("Failed to create prim of type: " + baseName);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Create prim error: ") + e.what());
}
}
}
}
ImGui::EndMenu();
}
ImGui::Separator();
// ---- Set Up Axis ----
{
TfToken currentUpAxis = UsdGeomGetStageUpAxis(m_stage);
bool isYUp = (currentUpAxis == UsdGeomTokens->y);
bool isZUp = (currentUpAxis == UsdGeomTokens->z);
if (ImGui::BeginMenu("Set Up Axis")) {
if (ImGui::MenuItem("Y Up", nullptr, isYUp, !isYUp)) {
if (UsdGeomSetStageUpAxis(m_stage, UsdGeomTokens->y)) {
LOG_INFO("Stage up axis set to Y");
if (m_onStageMetadataChanged) m_onStageMetadataChanged();
} else {
LOG_ERROR("Failed to set stage up axis to Y");
}
}
if (ImGui::MenuItem("Z Up", nullptr, isZUp, !isZUp)) {
if (UsdGeomSetStageUpAxis(m_stage, UsdGeomTokens->z)) {
LOG_INFO("Stage up axis set to Z");
if (m_onStageMetadataChanged) m_onStageMetadataChanged();
} else {
LOG_ERROR("Failed to set stage up axis to Z");
}
}
ImGui::EndMenu();
}
}
ImGui::Separator();
// ---- Add Reference ----
if (ImGui::MenuItem("Add Reference...")) {
std::string filePath = FileDialog::OpenFile(
"USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0All Files (*.*)\0*.*\0",
"Add Reference File");
if (!filePath.empty()) {
// Derive a valid USD prim name from the file's stem.
std::string stem = std::filesystem::path(filePath).stem().string();
std::string xformName = SanitizeUsdName(stem);
if (xformName.empty()) xformName = "Reference";
// Avoid name collision: append _N if the path already exists.
std::string finalName = xformName;
int suffix = 1;
while (m_stage->GetPrimAtPath(SdfPath("/" + finalName)).IsValid()) {
finalName = xformName + "_" + std::to_string(suffix++);
}
SdfPath xformPath("/" + finalName);
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<AddReferenceCommand>(
m_stage, xformPath, filePath));
} else {
try {
UsdPrim xformPrim = m_stage->DefinePrim(xformPath, 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());
}
}
}
}
ImGui::EndPopup();
}
// Deferred confirm modal for prim removal (must be opened outside any popup stack).
RenderRemovePrimModal();
// Deferred file-dialog for reference replacement (must run outside any popup stack).
ProcessPendingReplaceRef();
}
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
if (!prim.IsValid()) return;
std::string displayName = prim.GetName().GetString();
if (displayName.empty()) displayName = prim.GetPath().GetString();
std::string typeName = prim.GetTypeName().GetString();
SdfPath primPath = prim.GetPath();
std::string primStr = primPath.GetString();
bool isSelected = (m_selectedPaths.count(primStr) > 0);
bool isActive = prim.IsActive();
bool isImageable = prim.IsA<UsdGeomImageable>();
bool isInvisible = false;
bool hasRefs = prim.HasAuthoredReferences();
bool hasChildren = !prim.GetChildren().empty();
if (isImageable) {
UsdGeomImageable img(prim);
isInvisible = (img.ComputeVisibility() == UsdGeomTokens->invisible);
}
// ── Colour coding ───────────────────────────────────────────────────────
// Orange: prim (or any of its attributes) has an opinion in the stage's
// own layers → indicates a local override on top of references.
// Blue: prim has references but NO local attribute override.
// Both colours are dimmed when the prim is inactive.
// ────────────────────────────────────────────────────────────────────────
bool hasOverride = false;
if (!m_localLayers.empty()) {
for (const auto& attr : prim.GetAuthoredAttributes()) {
for (const auto& spec : attr.GetPropertyStack()) {
if (m_localLayers.count(spec->GetLayer()->GetIdentifier())) {
hasOverride = true;
break;
}
}
if (hasOverride) break;
}
}
// Force-open ancestor nodes when scrolling to the primary selection.
bool isAncestorOfPrimary = m_scrollToSelected &&
!m_primarySdfPath.IsEmpty() &&
!m_primarySdfPath.IsRootPrimPath() &&
m_primarySdfPath.HasPrefix(primPath) &&
(m_primarySdfPath != primPath);
if (isAncestorOfPrimary)
ImGui::SetNextItemOpen(true, ImGuiCond_Always);
// ── ImGui Demo tree-in-table pattern ────────────────────────────────────
// Tree node goes in Col 0 with SpanAllColumns. This makes the full row
// rect the "item" for selection highlight, IsItemClicked, and scroll.
// Subsequent columns are filled AFTER the tree node open/close decision.
// ────────────────────────────────────────────────────────────────────────
ImGui::TableNextRow();
ImGui::TableNextColumn(); // Col 0 — prim name + tree arrow
ImGui::PushID(primStr.c_str());
// Determine final text colour for the prim name.
// Priority: override (orange) > reference (blue) > inactive (dim) > default.
// Alpha is reduced when the prim is inactive.
const float alpha = isActive ? 1.0f : 0.45f;
bool pushedColor = false;
if (hasOverride) {
// Orange — local attribute override present
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.60f, 0.10f, alpha));
pushedColor = true;
} else if (hasRefs) {
// Blue — has references, no local overrides
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.40f, 0.70f, 1.0f, alpha));
pushedColor = true;
} else if (!isActive) {
// Dim grey for inactive prims with no other colour
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45f, 0.45f, 0.45f, 1.0f));
pushedColor = true;
}
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_OpenOnDoubleClick |
ImGuiTreeNodeFlags_SpanAllColumns; // ← key: full-row item rect
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
bool open = ImGui::TreeNodeEx(displayName.c_str(), flags);
if (pushedColor)
ImGui::PopStyleColor();
// ── Scroll-to-selection (now reliable: SpanAllColumns gives correct row rect) ──
if (m_scrollToSelected && primStr == m_primarySelectedPath) {
ImGui::SetScrollHereY(0.5f);
m_scrollToSelected = false;
}
// Selection on click (not on toggle arrow).
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
SetSelectedPathFromClick(primStr);
// Tooltip.
if (ImGui::IsItemHovered()) {
std::string tip = "Type: " + (typeName.empty() ? "(unknown)" : typeName) +
"\nPath: " + primStr +
"\nActive: " + (isActive ? "Yes" : "No");
if (isImageable)
tip += std::string("\nVisibility: ") + (isInvisible ? "Invisible" : "Visible");
if (hasRefs)
tip += "\nHas references";
if (m_selectedPaths.size() > 1)
tip += "\n\n" + std::to_string(m_selectedPaths.size()) + " prims selected";
ImGui::SetTooltip("%s", tip.c_str());
}
// Context menu (must follow the last widget = the tree node).
RenderContextMenu(prim);
// ── Col 1: Prim-type icon ───────────────────────────────────────────────
ImGui::TableNextColumn();
{
const float iconSz = ImGui::GetTextLineHeight();
const ImVec2 iconVec(iconSz, iconSz);
ImTextureID id = m_iconManager ? m_iconManager->Get(GetPrimTypeIconEnum(prim))
: ImTextureID_Invalid;
ImVec4 tint = isActive ? ImVec4(1,1,1,1) : ImVec4(0.45f,0.45f,0.45f,1);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 1.f);
ImGui::ImageWithBg(ImTextureRef(id), iconVec,
ImVec2(0,0), ImVec2(1,1), ImVec4(0,0,0,0), tint);
}
// ── Col 2: Visibility toggle ────────────────────────────────────────────
ImGui::TableNextColumn();
if (isImageable) {
const float iconSz = ImGui::GetTextLineHeight();
const ImVec2 iconVec(iconSz, iconSz);
Icon visIcon = isInvisible ? Icon::EyeSlash : Icon::Eye;
ImVec4 visTint = isInvisible ? ImVec4(0.45f, 0.45f, 0.45f, 0.6f)
: ImVec4(0.9f, 0.9f, 0.9f, 1.0f);
ImTextureID id = m_iconManager ? m_iconManager->Get(visIcon) : ImTextureID_Invalid;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.12f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.20f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0));
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 1.f);
if (ImGui::ImageButton("##vis", ImTextureRef(id), iconVec,
ImVec2(0,0), ImVec2(1,1), ImVec4(0,0,0,0), visTint)) {
try {
UsdGeomImageable img(prim);
UsdAttribute visAttr = img.GetVisibilityAttr();
if (isInvisible) {
visAttr.Set(UsdGeomTokens->inherited);
LOG_INFO("Made visible: " + primStr);
} else {
visAttr.Set(UsdGeomTokens->invisible);
LOG_INFO("Made invisible: " + primStr);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Toggle visibility: ") + e.what());
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(isInvisible ? "Invisible — click to show"
: "Visible — click to hide");
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
} else {
ImGui::Dummy(ImVec2(ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight()));
}
// ── Col 3: Reference indicator ──────────────────────────────────────────
ImGui::TableNextColumn();
if (hasRefs) {
const float iconSz = ImGui::GetTextLineHeight();
const ImVec2 iconVec(iconSz, iconSz);
ImTextureID id = m_iconManager ? m_iconManager->Get(Icon::Link) : ImTextureID_Invalid;
ImVec4 tint(0.45f, 0.75f, 1.0f, 1.0f);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 1.f);
ImGui::ImageWithBg(ImTextureRef(id), iconVec,
ImVec2(0,0), ImVec2(1,1), ImVec4(0,0,0,0), tint);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Has references");
}
// ── Recurse into children ───────────────────────────────────────────────
// TreePop must be called in the SAME column as TreeNodeEx (col 0).
// Since we called TableNextColumn three more times above, we must move
// back to col 0 before TreePop. The correct ImGui demo pattern is to
// recurse BEFORE filling other columns, but we need icons on the same row.
// Solution: recurse here (after columns), but ImGui only needs TreePop to
// be inside the same Begin/End pair — column doesn't matter for TreePop.
if (open && hasChildren) {
for (const auto& child : prim.GetChildren())
RenderPrimNode(child);
ImGui::TreePop();
}
ImGui::PopID();
}
void SceneHierarchyPanel::RenderContextMenu(const UsdPrim& prim) {
if (!prim.IsValid() || prim.IsPseudoRoot()) return;
if (ImGui::BeginPopupContextItem("PrimContextMenu")) {
std::string primName = prim.GetName().GetString();
ImGui::TextDisabled("%s", primName.c_str());
ImGui::Separator();
bool isActive = prim.IsActive();
if (ImGui::MenuItem(isActive ? "Deactivate" : "Activate")) {
try {
prim.SetActive(!isActive);
LOG_INFO(std::string(!isActive ? "Activated" : "Deactivated") + " prim: " + prim.GetPath().GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to toggle active: ") + e.what());
}
}
bool isImageable = prim.IsA<UsdGeomImageable>();
if (isImageable) {
UsdGeomImageable img(prim);
TfToken vis = img.ComputeVisibility();
bool isInvisible = (vis == UsdGeomTokens->invisible);
if (ImGui::MenuItem(isInvisible ? "Make Visible" : "Make Invisible")) {
try {
UsdAttribute visAttr = img.GetVisibilityAttr();
if (isInvisible) {
visAttr.Set(UsdGeomTokens->inherited);
} else {
visAttr.Set(UsdGeomTokens->invisible);
}
LOG_INFO(std::string(isInvisible ? "Made visible" : "Made invisible") + ": " + prim.GetPath().GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to toggle visibility: ") + e.what());
}
}
}
ImGui::Separator();
bool hasChildren = !prim.GetChildren().empty();
if (ImGui::MenuItem("Expand Children", nullptr, false, hasChildren)) {
ImGui::GetStateStorage()->SetInt(ImGui::GetID(prim.GetPath().GetText()), 1);
}
if (ImGui::MenuItem("Collapse Children", nullptr, false, hasChildren)) {
ImGui::GetStateStorage()->SetInt(ImGui::GetID(prim.GetPath().GetText()), 0);
}
ImGui::Separator();
// ---- Reference operations ----
if (ImGui::MenuItem("Add Reference...")) {
std::string filePath = FileDialog::OpenFile(
"USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0All Files (*.*)\0*.*\0",
"Add Reference File");
if (!filePath.empty()) {
try {
bool ok = prim.GetReferences().AddReference(filePath);
if (ok) {
LOG_INFO("Added reference '" + filePath + "' to prim: " + prim.GetPath().GetString());
} else {
LOG_ERROR("Failed to add reference '" + filePath + "' to: " + prim.GetPath().GetString());
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Add reference error: ") + e.what());
}
}
}
bool hasRefs = prim.HasAuthoredReferences();
// ---- Replace Reference ----
if (ImGui::BeginMenu("Replace Reference", hasRefs)) {
UsdPrimCompositionQuery::Filter replFilter;
replFilter.arcTypeFilter = UsdPrimCompositionQuery::ArcTypeFilter::Reference;
replFilter.dependencyTypeFilter = UsdPrimCompositionQuery::DependencyTypeFilter::Direct;
UsdPrimCompositionQuery replQuery(prim, replFilter);
bool anyRepl = false;
for (auto& arc : replQuery.GetCompositionArcs()) {
SdfReferenceEditorProxy editor;
SdfReference oldRef;
if (arc.GetIntroducingListEditor(&editor, &oldRef)) {
std::string label = oldRef.GetAssetPath().empty()
? "(internal reference)"
: oldRef.GetAssetPath();
if (ImGui::MenuItem(label.c_str())) {
// NOTE: file dialog is blocking — close popup first via deferred path.
m_pendingReplaceRef = oldRef;
m_pendingReplaceRefPrim = prim.GetPath();
m_doReplaceRefPick = true;
}
anyRepl = true;
}
}
if (!anyRepl) {
ImGui::TextDisabled("(no direct references)");
}
ImGui::EndMenu();
}
// ---- Remove Reference ----
if (ImGui::BeginMenu("Remove Reference", hasRefs)) {
// Collect direct reference arcs via composition query.
UsdPrimCompositionQuery::Filter filter;
filter.arcTypeFilter = UsdPrimCompositionQuery::ArcTypeFilter::Reference;
filter.dependencyTypeFilter = UsdPrimCompositionQuery::DependencyTypeFilter::Direct;
UsdPrimCompositionQuery query(prim, filter);
bool anyListed = false;
for (auto& arc : query.GetCompositionArcs()) {
SdfReferenceEditorProxy editor;
SdfReference ref;
if (arc.GetIntroducingListEditor(&editor, &ref)) {
std::string label = ref.GetAssetPath().empty()
? "(internal reference)"
: ref.GetAssetPath();
if (ImGui::MenuItem(label.c_str())) {
try {
prim.GetReferences().RemoveReference(ref);
LOG_INFO("Removed reference '" + label + "' from: " + prim.GetPath().GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("Remove reference error: ") + e.what());
}
}
anyListed = true;
}
}
if (anyListed) ImGui::Separator();
if (ImGui::MenuItem("Clear All References")) {
try {
prim.GetReferences().ClearReferences();
LOG_INFO("Cleared all references on: " + prim.GetPath().GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("Clear references error: ") + e.what());
}
}
ImGui::EndMenu();
}
// ---- Prim removal ----
// Only show "Remove Prim" for prims that have a local spec authored in the root
// layer. Prims brought in purely via composition from an external referenced
// stage have no local spec and cannot be removed directly.
ImGui::Separator();
{
auto rootLayer = m_stage->GetRootLayer();
bool hasLocalSpec = rootLayer && !!rootLayer->GetPrimAtPath(prim.GetPath());
if (ImGui::MenuItem("Remove Prim", nullptr, false, hasLocalSpec)) {
// Defer to the confirm modal — can't open a modal from inside a popup.
m_pendingRemovePrimPath = prim.GetPath();
m_showRemovePrimConfirm = true;
}
}
ImGui::EndPopup();
}
}
void SceneHierarchyPanel::RenderRemovePrimModal() {
if (m_showRemovePrimConfirm) {
ImGui::OpenPopup("Remove Prim##confirm");
m_showRemovePrimConfirm = false;
}
// Centre the modal over the main viewport.
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_Always);
if (ImGui::BeginPopupModal("Remove Prim##confirm", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
ImGui::TextUnformatted("Are you sure you want to remove this prim?");
ImGui::Spacing();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s",
m_pendingRemovePrimPath.GetText());
ImGui::Spacing();
ImGui::TextDisabled("This will remove the prim spec from the root layer.\n"
"Child prims authored locally will also be removed.");
ImGui::Separator();
float buttonWidth = 120.0f;
float spacing = ImGui::GetStyle().ItemSpacing.x;
float totalW = buttonWidth * 2.0f + spacing;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f +
ImGui::GetCursorPosX());
if (ImGui::Button("Remove", ImVec2(buttonWidth, 0))) {
if (m_stage && !m_pendingRemovePrimPath.IsEmpty()) {
if (m_commandHistory) {
// Snapshot the spec BEFORE deletion, then push.
auto cmd = std::make_unique<DeletePrimCommand>(
m_stage, m_pendingRemovePrimPath);
std::string removedStr = m_pendingRemovePrimPath.GetString();
m_commandHistory->Push(std::move(cmd));
// Clear selection if removed prim was selected.
if (m_primarySelectedPath == removedStr) {
m_primarySelectedPath.clear();
m_primarySdfPath = SdfPath();
m_selectedPaths.clear();
if (m_onPrimSelected) m_onPrimSelected("");
} else {
m_selectedPaths.erase(removedStr);
}
} else {
try {
bool ok = m_stage->RemovePrim(m_pendingRemovePrimPath);
if (ok) {
LOG_INFO("Removed prim: " + m_pendingRemovePrimPath.GetString());
std::string removedStr = m_pendingRemovePrimPath.GetString();
if (m_primarySelectedPath == removedStr) {
m_primarySelectedPath.clear();
m_primarySdfPath = SdfPath();
m_selectedPaths.clear();
if (m_onPrimSelected) m_onPrimSelected("");
} else {
m_selectedPaths.erase(removedStr);
}
} else {
LOG_ERROR("Failed to remove prim: " + m_pendingRemovePrimPath.GetString());
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Remove prim error: ") + e.what());
}
}
m_pendingRemovePrimPath = SdfPath();
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
m_pendingRemovePrimPath = SdfPath();
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void SceneHierarchyPanel::ProcessPendingReplaceRef() {
if (!m_doReplaceRefPick) return;
m_doReplaceRefPick = false;
if (!m_stage || m_pendingReplaceRefPrim.IsEmpty()) return;
std::string newPath = FileDialog::OpenFile(
"USD Files (*.usd;*.usda;*.usdc;*.usdz)\0*.usd;*.usda;*.usdc;*.usdz\0All Files (*.*)\0*.*\0",
"Replace Reference File");
if (newPath.empty()) return;
UsdPrim prim = m_stage->GetPrimAtPath(m_pendingReplaceRefPrim);
if (!prim.IsValid()) {
LOG_ERROR("Replace reference: prim no longer valid: " + m_pendingReplaceRefPrim.GetString());
return;
}
try {
// Build the new SdfReference preserving prim path and layer offset.
SdfReference newRef(newPath,
m_pendingReplaceRef.GetPrimPath(),
m_pendingReplaceRef.GetLayerOffset());
if (m_commandHistory) {
m_commandHistory->Push(std::make_unique<ReplaceReferenceCommand>(
m_stage, m_pendingReplaceRefPrim, m_pendingReplaceRef, newRef));
} else {
UsdReferences refs = prim.GetReferences();
bool removed = refs.RemoveReference(m_pendingReplaceRef);
if (!removed) {
LOG_ERROR("Replace reference: failed to remove old reference '" +
m_pendingReplaceRef.GetAssetPath() + "'");
} else {
bool added = refs.AddReference(newRef);
if (added) {
LOG_INFO("Replaced reference '" + m_pendingReplaceRef.GetAssetPath() +
"' -> '" + newPath + "' on prim: " + m_pendingReplaceRefPrim.GetString());
} else {
LOG_ERROR("Replace reference: failed to add new reference '" + newPath + "'");
}
}
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Replace reference error: ") + e.what());
}
m_pendingReplaceRefPrim = SdfPath();
m_pendingReplaceRef = SdfReference();
}
} // namespace UsdLayerManager
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include "../core/PropertyManager.h"
#include "../core/CommandHistory.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/reference.h>
#include <imgui.h>
#include <string>
#include <vector>
#include <unordered_set>
#include <functional>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
class SceneHierarchyPanel {
public:
SceneHierarchyPanel();
~SceneHierarchyPanel();
void SetPropertyManager(PropertyManager* manager);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetStage(UsdStageRefPtr stage);
void SetIconManager(IconManager* iconManager) { m_iconManager = iconManager; }
void Render();
std::string GetSelectedPrimPath() const { return m_primarySelectedPath; }
UsdPrim GetSelectedPrim() const;
/// Set single selection from hierarchy click (fires callback).
/// Also clears any rect multi-selection.
void SetSelectedPathFromClick(const std::string& path);
/// Set single selection from viewport single-click (no callback, scroll-to).
void SetSelectedPath(const std::string& path) {
m_selectedPaths.clear();
m_primarySelectedPath = path;
m_primarySdfPath = path.empty() ? SdfPath() : SdfPath(path);
if (!path.empty()) m_selectedPaths.insert(path);
m_scrollToSelected = !path.empty();
}
/// Set multi-selection from viewport rect pick (no callback, scroll-to first).
void SetSelectedPaths(const std::vector<std::string>& paths) {
m_selectedPaths.clear();
m_primarySelectedPath.clear();
m_primarySdfPath = SdfPath();
for (const auto& p : paths) m_selectedPaths.insert(p);
if (!paths.empty()) {
m_primarySelectedPath = paths.front();
m_primarySdfPath = SdfPath(paths.front());
m_scrollToSelected = true;
}
}
using PrimSelectCallback = std::function<void(const std::string& path)>;
void SetOnPrimSelected(PrimSelectCallback callback) { m_onPrimSelected = callback; }
/// Called when stage-level metadata (e.g. up axis) is changed via the hierarchy panel.
using StageMetadataChangedCallback = std::function<void()>;
void SetOnStageMetadataChanged(StageMetadataChangedCallback callback) { m_onStageMetadataChanged = callback; }
private:
void RenderPrimNode(const UsdPrim& prim);
const char* GetPrimTypeIcon(const UsdPrim& prim) const;
Icon GetPrimTypeIconEnum(const UsdPrim& prim) const;
void RenderContextMenu(const UsdPrim& prim);
void RenderRemovePrimModal();
void ProcessPendingReplaceRef();
PropertyManager* m_propertyManager;
CommandHistory* m_commandHistory = nullptr;
IconManager* m_iconManager = nullptr;
UsdStageRefPtr m_stage;
/// Primary path: the scroll/frame target; also used for F-to-frame.
std::string m_primarySelectedPath;
SdfPath m_primarySdfPath;
/// Full set of selected paths (supports multi-select from rect pick).
std::unordered_set<std::string> m_selectedPaths;
bool m_scrollToSelected = false;
/// Stage-local layer identifiers rebuilt once per Render() for override detection.
/// Contains only the stage's own layers (root + sublayers + session),
/// NOT layers that came in through references or payloads.
std::unordered_set<std::string> m_localLayers;
PrimSelectCallback m_onPrimSelected;
StageMetadataChangedCallback m_onStageMetadataChanged;
/// Remove-prim confirmation state.
bool m_showRemovePrimConfirm = false;
SdfPath m_pendingRemovePrimPath;
/// Replace-reference deferred state (file dialog must run outside popup stack).
bool m_doReplaceRefPick = false;
SdfPath m_pendingReplaceRefPrim;
pxr::SdfReference m_pendingReplaceRef;
};
} // namespace UsdLayerManager
+737
View File
@@ -0,0 +1,737 @@
#include "TransformManipulator.h"
#include "../utils/Logger.h"
#include "../core/CommandHistory.h"
#include "../core/commands/TransformCommand.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <pxr/usd/usdGeom/xformCache.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/vec3d.h>
#include <cmath>
#include <algorithm>
#include <vector>
#include <memory>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// ImGuizmo-derived colour palette
// X = red, Y = green, Z = blue (matches Maya / ImGuizmo defaults)
// Highlight (hovered / active) = orange (ImGuizmo SELECTION colour)
// ---------------------------------------------------------------------------
static const ImU32 kColX = IM_COL32(214, 38, 38, 255);
static const ImU32 kColY = IM_COL32( 38, 179, 38, 255);
static const ImU32 kColZ = IM_COL32( 38, 90, 220, 255);
static const ImU32 kColHover = IM_COL32(255, 128, 16, 255); // ImGuizmo SELECTION
static const ImU32 kColCenter = IM_COL32(255, 255, 255, 220);
static const ImU32 kColAxisLine = IM_COL32(170, 170, 170, 170); // shaft tint
static const ImU32 kAxisColors[3] = { kColX, kColY, kColZ };
// ImGuizmo line-thickness defaults (from Style struct)
static constexpr float kTranslationLineThick = 3.0f;
static constexpr float kRotationLineThick = 2.0f;
static constexpr float kScaleLineThick = 3.0f;
static constexpr float kScaleCircleRadius = 5.0f; // pixels, like ScaleLineCircleSize
static constexpr float kCenterCircleRadius = 5.0f; // pixels, like CenterCircleSize
// ──────────────────────────────────────────────────────────────────────────────
// Stage / selection
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::SetStage(pxr::UsdStageRefPtr stage)
{
m_stage = stage;
m_primPath = pxr::SdfPath();
m_isDragging = false;
}
void TransformManipulator::SetSelectedPrim(const pxr::SdfPath& path)
{
m_primPath = path;
m_isDragging = false;
}
// ──────────────────────────────────────────────────────────────────────────────
// GetGizmoAxes
//
// Returns the three gizmo axis vectors in world space.
//
// World space: fixed unit vectors X/Y/Z.
// Object space: the prim's local X/Y/Z axes derived from its local-to-world
// matrix. In USD row-vector convention (p' = p * M), row i of M is the
// world-space image of the i-th local basis vector, so we normalise rows
// 0..2 to get the three local axes expressed in world coordinates.
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::GetGizmoAxes(pxr::GfVec3d outAxes[3]) const
{
// Fallback: world-space unit vectors
outAxes[0] = {1, 0, 0};
outAxes[1] = {0, 1, 0};
outAxes[2] = {0, 0, 1};
if (m_transformSpace == TransformSpace::World) return;
if (!m_stage || m_primPath.IsEmpty()) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (!prim) return;
pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default());
pxr::GfMatrix4d localToWorld = xformCache.GetLocalToWorldTransform(prim);
// Each row i (0..2) of the 4×4 matrix is the world-space direction of
// the i-th local basis vector (USD row-vector convention).
for (int i = 0; i < 3; ++i) {
pxr::GfVec3d row(localToWorld[i][0], localToWorld[i][1], localToWorld[i][2]);
double len = row.GetLength();
outAxes[i] = (len > 1e-9) ? row / len : outAxes[i];
}
}
// ──────────────────────────────────────────────────────────────────────────────
// WorldToScreen
// Converts a world-space point to absolute ImGui screen coordinates.
//
// USD uses row-vector convention: p_clip = (p, 1) * viewProjMatrix
// where viewProjMatrix[row][col].
// ──────────────────────────────────────────────────────────────────────────────
bool TransformManipulator::WorldToScreen(const pxr::GfVec3d& world,
const pxr::GfMatrix4d& vp,
int viewW, int viewH,
const ImVec2& imagePos,
ImVec2& outScreen)
{
// Clip space: (p, 1) * VP (row-vector × matrix)
double cx = vp[0][0]*world[0] + vp[1][0]*world[1] + vp[2][0]*world[2] + vp[3][0];
double cy = vp[0][1]*world[0] + vp[1][1]*world[1] + vp[2][1]*world[2] + vp[3][1];
double cw = vp[0][3]*world[0] + vp[1][3]*world[1] + vp[2][3]*world[2] + vp[3][3];
if (cw <= 0.0) return false; // behind near plane
double invW = 1.0 / cw;
double ndcX = cx * invW; // in [-1, 1]
double ndcY = cy * invW; // in [-1, 1], +Y up in clip space
// Viewport pixel (Y flipped: clip +Y → screen top)
float px = static_cast<float>((ndcX + 1.0) * 0.5 * viewW);
float py = static_cast<float>((1.0 - ndcY) * 0.5 * viewH);
outScreen = ImVec2(imagePos.x + px, imagePos.y + py);
return true;
}
// ──────────────────────────────────────────────────────────────────────────────
// ComputeScreenFactor (ImGuizmo algorithm)
//
// Projects each world-axis unit vector from @p pivot into clip space and
// measures its clip-space length (aspect-ratio corrected, like ImGuizmo's
// GetSegmentLengthClipSpace). Returns the world-space gizmo half-size that
// spans @p desiredFraction of the NDC extent.
// ──────────────────────────────────────────────────────────────────────────────
float TransformManipulator::ComputeScreenFactor(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot,
int viewW, int viewH,
float desiredFraction)
{
// Clip-space coords of the pivot
double pw = vp[0][3]*pivot[0] + vp[1][3]*pivot[1] + vp[2][3]*pivot[2] + vp[3][3];
if (pw <= 0.0) return 1.0f;
double invPW = 1.0 / pw;
double px = (vp[0][0]*pivot[0] + vp[1][0]*pivot[1] + vp[2][0]*pivot[2] + vp[3][0]) * invPW;
double py = (vp[0][1]*pivot[0] + vp[1][1]*pivot[1] + vp[2][1]*pivot[2] + vp[3][1]) * invPW;
// Test each world axis: pick the one that subtends the largest clip length.
// (ImGuizmo uses the camera-right direction; testing all three world axes
// is equivalent and avoids needing to extract the view-inverse.)
const pxr::GfVec3d axes[3] = {{1,0,0},{0,1,0},{0,0,1}};
float displayRatio = (float)viewW / (float)std::max(viewH, 1);
float maxClipLen = 0.f;
for (const auto& ax : axes) {
pxr::GfVec3d tip = pivot + ax;
double tw = vp[0][3]*tip[0] + vp[1][3]*tip[1] + vp[2][3]*tip[2] + vp[3][3];
if (tw <= 0.0) continue;
double invTW = 1.0 / tw;
double tx = (vp[0][0]*tip[0] + vp[1][0]*tip[1] + vp[2][0]*tip[2] + vp[3][0]) * invTW;
double ty = (vp[0][1]*tip[0] + vp[1][1]*tip[1] + vp[2][1]*tip[2] + vp[3][1]) * invTW;
// Clip-space delta, aspect-ratio corrected (ImGuizmo convention)
float dx = static_cast<float>(tx - px);
float dy = static_cast<float>(ty - py);
if (displayRatio < 1.f) dx *= displayRatio;
else dy /= displayRatio;
float len = std::sqrt(dx*dx + dy*dy);
maxClipLen = std::max(maxClipLen, len);
}
if (maxClipLen < 1e-6f) return 1.0f;
return desiredFraction / maxClipLen;
}
// ──────────────────────────────────────────────────────────────────────────────
// PointToSegmentDist
// ──────────────────────────────────────────────────────────────────────────────
float TransformManipulator::PointToSegmentDist(ImVec2 p, ImVec2 a, ImVec2 b)
{
float dx = b.x - a.x, dy = b.y - a.y;
float lenSq = dx*dx + dy*dy;
if (lenSq < 1e-6f) {
float ex = p.x - a.x, ey = p.y - a.y;
return std::sqrt(ex*ex + ey*ey);
}
float t = std::max(0.f, std::min(1.f, ((p.x-a.x)*dx + (p.y-a.y)*dy) / lenSq));
float cx = a.x + t*dx - p.x;
float cy = a.y + t*dy - p.y;
return std::sqrt(cx*cx + cy*cy);
}
// ──────────────────────────────────────────────────────────────────────────────
// HitTestAxes
// Returns 0=X, 1=Y, 2=Z or -1.
// ──────────────────────────────────────────────────────────────────────────────
int TransformManipulator::HitTestAxes(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const ImVec2& imgPos, int vW, int vH,
const ImVec2& mouse,
const pxr::GfVec3d axes[3]) const
{
ImVec2 pivotSS;
if (!WorldToScreen(pivot, vp, vW, vH, imgPos, pivotSS)) return -1;
static constexpr float kPickRadius = 10.0f;
float bestDist = kPickRadius;
int bestAxis = -1;
for (int i = 0; i < 3; ++i) {
ImVec2 tipSS;
if (!WorldToScreen(pivot + axes[i] * sf, vp, vW, vH, imgPos, tipSS)) continue;
float d = PointToSegmentDist(mouse, pivotSS, tipSS);
if (d < bestDist) { bestDist = d; bestAxis = i; }
}
return bestAxis;
}
// ──────────────────────────────────────────────────────────────────────────────
// HitTestRotateRings
//
// Tests proximity to the VISIBLE (front-facing) half-arc of each ring.
// Uses the same angleStart formula as DrawRotateGizmo so hit area exactly
// matches the drawn arcs. Returns 0=X, 1=Y, 2=Z or -1 for no hit.
// ──────────────────────────────────────────────────────────────────────────────
int TransformManipulator::HitTestRotateRings(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const pxr::GfVec3d& cameraEye,
const ImVec2& imgPos, int vW, int vH,
const ImVec2& mouse,
const pxr::GfVec3d axes[3]) const
{
static constexpr int kSegs = 32; // fewer segs needed for hit testing
static constexpr float kDispFactor = 1.2f;
static constexpr float kPickRadius = 10.0f; // pixels, matches ImGuizmo's 8 px + margin
float radius = sf * kDispFactor;
pxr::GfVec3d camToScene = pivot - cameraEye;
double len = camToScene.GetLength();
if (len < 1e-9) camToScene = pxr::GfVec3d(0,0,-1);
else camToScene /= len;
float bestDist = kPickRadius;
int bestAxis = -1;
for (int axis = 0; axis < 3; ++axis) {
// Tangent axes spanning this ring's plane
// axis 0: ring normal = axes[0], plane spanned by axes[1], axes[2]
// axis 1: ring normal = axes[1], plane spanned by axes[0], axes[2]
// axis 2: ring normal = axes[2], plane spanned by axes[0], axes[1]
const pxr::GfVec3d& u = (axis == 0) ? axes[1] : axes[0];
const pxr::GfVec3d& v = (axis < 2) ? axes[2] : axes[1];
// Project camToScene onto ring plane to compute front-facing half-arc start
float a_proj = static_cast<float>(camToScene[0]*u[0] + camToScene[1]*u[1] + camToScene[2]*u[2]);
float b_proj = static_cast<float>(camToScene[0]*v[0] + camToScene[1]*v[1] + camToScene[2]*v[2]);
float as = std::atan2(b_proj, a_proj) + static_cast<float>(M_PI) * 0.5f;
ImVec2 prevSS;
bool hasPrev = false;
for (int s = 0; s <= kSegs; ++s) {
float angle = as + static_cast<float>(M_PI) *
(static_cast<float>(s) / static_cast<float>(kSegs));
float c = std::cos(angle), si = std::sin(angle);
pxr::GfVec3d p = pivot + u * (radius * c) + v * (radius * si);
ImVec2 ss;
if (!WorldToScreen(p, vp, vW, vH, imgPos, ss)) { hasPrev = false; continue; }
if (hasPrev) {
float d = PointToSegmentDist(mouse, prevSS, ss);
if (d < bestDist) { bestDist = d; bestAxis = axis; }
}
prevSS = ss;
hasPrev = true;
}
}
return bestAxis;
}
// ──────────────────────────────────────────────────────────────────────────────
// DrawMoveGizmo
//
// For each axis:
// • Shaft — thick line from pivot to cone-base (~78 % of arrow length)
// • Head — screen-space filled isoceles triangle (ImGuizmo arrowhead style)
// Centre — small filled circle
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::DrawMoveGizmo(ImDrawList* dl,
const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot,
float sf,
const ImVec2& imgPos,
int vW, int vH,
const pxr::GfVec3d axes[3])
{
// Arrow geometry ratios (tuned to match ImGuizmo proportions)
static constexpr float kShaftFrac = 0.78f; // shaft ends at 78 % of arrow
static constexpr float kArrowFrac = 0.12f; // arrowhead half-width / total pixel length
ImVec2 pivotSS;
if (!WorldToScreen(pivot, vp, vW, vH, imgPos, pivotSS)) return;
for (int i = 0; i < 3; ++i) {
ImU32 col = (i == m_dragAxis || i == m_hoveredAxis) ? kColHover : kAxisColors[i];
ImVec2 shaftEndSS, tipSS;
bool okShaft = WorldToScreen(pivot + axes[i] * sf * kShaftFrac,
vp, vW, vH, imgPos, shaftEndSS);
bool okTip = WorldToScreen(pivot + axes[i] * sf,
vp, vW, vH, imgPos, tipSS);
if (!okShaft || !okTip) continue;
// --- Shaft ---
dl->AddLine(pivotSS, shaftEndSS, col, kTranslationLineThick);
// --- Arrowhead (filled triangle in screen space) ---
// Screen-space arrow direction (from base toward tip)
float adx = tipSS.x - shaftEndSS.x;
float ady = tipSS.y - shaftEndSS.y;
float alen = std::sqrt(adx*adx + ady*ady);
if (alen < 1.f) continue;
// Perpendicular to arrow direction
float px = -ady / alen;
float py = adx / alen;
// Total gizmo length in pixels (used to scale arrowhead)
float totalLen = std::sqrt((tipSS.x - pivotSS.x)*(tipSS.x - pivotSS.x) +
(tipSS.y - pivotSS.y)*(tipSS.y - pivotSS.y));
float halfWidth = totalLen * kArrowFrac;
ImVec2 wing1(shaftEndSS.x + px * halfWidth, shaftEndSS.y + py * halfWidth);
ImVec2 wing2(shaftEndSS.x - px * halfWidth, shaftEndSS.y - py * halfWidth);
dl->AddTriangleFilled(tipSS, wing1, wing2, col);
}
// Centre circle (white, like ImGuizmo's center square)
dl->AddCircleFilled(pivotSS, kCenterCircleRadius, kColCenter, 16);
}
// ──────────────────────────────────────────────────────────────────────────────
// DrawRotateGizmo (ImGuizmo-style front-facing half-arc)
//
// Algorithm (ported from ImGuizmo::DrawRotationGizmo):
// viewDir = normalize(pivot - cameraEye) [camera-to-scene direction]
//
// For each ring axis the "angleStart" places the half-arc so that it covers
// exactly the front-facing hemisphere (the half the camera can see).
//
// Ring convention in our code:
// axis 0 → X ring (YZ plane): angleStart = atan2(vz, vy) + π/2
// axis 1 → Y ring (XZ plane): angleStart = atan2(vz, vx) + π/2
// axis 2 → Z ring (XY plane): angleStart = atan2(vy, vx) + π/2
//
// The ring radius is screenFactor × 1.2 (ImGuizmo rotationDisplayFactor).
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::DrawRotateGizmo(ImDrawList* dl,
const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot,
float sf,
const pxr::GfVec3d& cameraEye,
const ImVec2& imgPos,
int vW, int vH,
const pxr::GfVec3d axes[3])
{
static constexpr int kSegs = 64; // half-arc segment count
static constexpr float kDispFactor = 1.2f; // ImGuizmo rotationDisplayFactor
float radius = sf * kDispFactor;
// Camera-to-scene direction in world space
pxr::GfVec3d camToScene = pivot - cameraEye;
double camLen = camToScene.GetLength();
if (camLen < 1e-9) camToScene = pxr::GfVec3d(0, 0, -1);
else camToScene /= camLen;
for (int axis = 0; axis < 3; ++axis) {
ImU32 col = (axis == m_dragAxis || axis == m_hoveredAxis) ? kColHover
: kAxisColors[axis];
float lw = (axis == m_dragAxis || axis == m_hoveredAxis)
? kRotationLineThick + 1.5f : kRotationLineThick;
// Tangent axes spanning this ring's plane
const pxr::GfVec3d& u = (axis == 0) ? axes[1] : axes[0];
const pxr::GfVec3d& v = (axis < 2) ? axes[2] : axes[1];
// Project camToScene onto ring plane to find front-facing half-arc start
float a_proj = static_cast<float>(camToScene[0]*u[0] + camToScene[1]*u[1] + camToScene[2]*u[2]);
float b_proj = static_cast<float>(camToScene[0]*v[0] + camToScene[1]*v[1] + camToScene[2]*v[2]);
float as = std::atan2(b_proj, a_proj) + static_cast<float>(M_PI) * 0.5f;
std::vector<ImVec2> pts;
pts.reserve(kSegs + 1);
for (int s = 0; s <= kSegs; ++s) {
float angle = as + static_cast<float>(M_PI) *
(static_cast<float>(s) / static_cast<float>(kSegs));
float c = std::cos(angle), si = std::sin(angle);
pxr::GfVec3d p = pivot + u * (radius * c) + v * (radius * si);
ImVec2 ss;
if (WorldToScreen(p, vp, vW, vH, imgPos, ss))
pts.push_back(ss);
}
if (pts.size() > 1)
dl->AddPolyline(pts.data(), static_cast<int>(pts.size()),
col, ImDrawFlags_None, lw);
}
}
// ──────────────────────────────────────────────────────────────────────────────
// DrawScaleGizmo
//
// Three lines each capped with a filled circle (ImGuizmo ScaleLineCircleSize).
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::DrawScaleGizmo(ImDrawList* dl,
const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot,
float sf,
const ImVec2& imgPos,
int vW, int vH,
const pxr::GfVec3d axes[3])
{
ImVec2 pivotSS;
if (!WorldToScreen(pivot, vp, vW, vH, imgPos, pivotSS)) return;
for (int i = 0; i < 3; ++i) {
ImU32 col = (i == m_dragAxis || i == m_hoveredAxis) ? kColHover : kAxisColors[i];
ImVec2 tipSS;
if (!WorldToScreen(pivot + axes[i] * sf, vp, vW, vH, imgPos, tipSS)) continue;
dl->AddLine(pivotSS, tipSS, col, kScaleLineThick);
dl->AddCircleFilled(tipSS, kScaleCircleRadius, col, 16);
}
// Centre box / circle (uniform scale handle)
dl->AddCircleFilled(pivotSS, kCenterCircleRadius + 1.f, kColCenter, 16);
}
// ──────────────────────────────────────────────────────────────────────────────
// Render — public entry point
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::Render(ImDrawList* dl,
const pxr::GfMatrix4d& viewProj,
const pxr::GfVec3d& pivot,
const pxr::GfVec3d& cameraEye,
const ImVec2& imagePos,
int viewW, int viewH)
{
if (m_mode == ManipulatorMode::Select) return;
if (!m_stage || m_primPath.IsEmpty()) return;
if (!dl || viewW <= 0 || viewH <= 0) return;
float sf = ComputeScreenFactor(viewProj, pivot, viewW, viewH, /*desiredFraction=*/0.15f);
pxr::GfVec3d axes[3];
GetGizmoAxes(axes);
switch (m_mode) {
case ManipulatorMode::Move:
DrawMoveGizmo (dl, viewProj, pivot, sf, imagePos, viewW, viewH, axes);
break;
case ManipulatorMode::Rotate:
DrawRotateGizmo(dl, viewProj, pivot, sf, cameraEye, imagePos, viewW, viewH, axes);
break;
case ManipulatorMode::Scale:
DrawScaleGizmo (dl, viewProj, pivot, sf, imagePos, viewW, viewH, axes);
break;
default: break;
}
}
// ──────────────────────────────────────────────────────────────────────────────
// HandleInput
// ──────────────────────────────────────────────────────────────────────────────
bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& viewProj,
const pxr::GfVec3d& pivot,
const pxr::GfVec3d& cameraEye,
const ImVec2& imagePos,
int viewW, int viewH,
bool viewportHovered)
{
if (m_mode == ManipulatorMode::Select) return false;
if (!m_stage || m_primPath.IsEmpty()) return false;
ImGuiIO& io = ImGui::GetIO();
ImVec2 mouse = io.MousePos; // absolute screen position
float sf = ComputeScreenFactor(viewProj, pivot, viewW, viewH, 0.15f);
pxr::GfVec3d axes[3];
GetGizmoAxes(axes);
// --- Update hover ---
if (!m_isDragging && viewportHovered) {
if (m_mode == ManipulatorMode::Rotate) {
m_hoveredAxis = HitTestRotateRings(viewProj, pivot, sf, cameraEye,
imagePos, viewW, viewH, mouse, axes);
} else {
m_hoveredAxis = HitTestAxes(viewProj, pivot, sf, imagePos, viewW, viewH, mouse, axes);
}
}
bool consumed = false;
// --- Start drag ---
if (viewportHovered && !m_isDragging &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !io.KeyAlt)
{
int hit = -1;
if (m_mode == ManipulatorMode::Rotate) {
hit = HitTestRotateRings(viewProj, pivot, sf, cameraEye,
imagePos, viewW, viewH, mouse, axes);
} else {
hit = HitTestAxes(viewProj, pivot, sf, imagePos, viewW, viewH, mouse, axes);
}
if (hit >= 0) {
m_isDragging = true;
m_dragAxis = hit;
m_dragLastPos = mouse;
consumed = true;
// For rotation: record initial screen angle around projected pivot center
if (m_mode == ManipulatorMode::Rotate) {
ImVec2 pivSS;
if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS)) {
m_dragRotateLastAngle = std::atan2(mouse.y - pivSS.y,
mouse.x - pivSS.x);
}
}
// Snapshot current xform
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (prim) {
pxr::UsdGeomXformCommonAPI api(prim);
pxr::GfVec3f pivot3f, rot, scale;
pxr::GfVec3d trans;
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder;
api.GetXformVectors(&trans, &rot, &scale, &pivot3f, &rotOrder,
pxr::UsdTimeCode::Default());
m_dragStartTranslate = trans;
m_dragStartRotate = rot;
m_dragStartScale = scale;
// Also save original (immutable) for the undo command.
m_dragOriginalTranslate = trans;
m_dragOriginalRotate = rot;
m_dragOriginalScale = scale;
m_dragOriginalRotOrder = rotOrder;
}
}
}
// --- Drag ongoing ---
if (m_isDragging) {
consumed = true;
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
ImVec2 delta = { mouse.x - m_dragLastPos.x,
mouse.y - m_dragLastPos.y };
if (m_mode == ManipulatorMode::Move) {
ImVec2 pivSS, tipSS;
if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS) &&
WorldToScreen(pivot + axes[m_dragAxis] * sf,
viewProj, viewW, viewH, imagePos, tipSS))
{
float axDx = tipSS.x - pivSS.x;
float axDy = tipSS.y - pivSS.y;
float axLen = std::sqrt(axDx*axDx + axDy*axDy);
if (axLen > 1e-3f) {
float screenDot = (delta.x*axDx + delta.y*axDy) / axLen;
float worldDelta = screenDot * sf / axLen;
pxr::GfVec3d move(
m_dragAxis == 0 ? worldDelta : 0.f,
m_dragAxis == 1 ? worldDelta : 0.f,
m_dragAxis == 2 ? worldDelta : 0.f);
ApplyMoveDelta(move);
}
}
}
else if (m_mode == ManipulatorMode::Rotate) {
// Screen-angle-around-pivot approach (much more precise than
// horizontal-only mapping — mirrors Maya's rotate manipulator feel).
ImVec2 pivSS;
if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS)) {
float dx = mouse.x - pivSS.x;
float dy = mouse.y - pivSS.y;
// Only respond when mouse is outside a small dead-zone around center
if (dx*dx + dy*dy > 4.f * 4.f) {
float currentAngle = std::atan2(dy, dx);
float deltaAngle = currentAngle - m_dragRotateLastAngle;
// Wrap to [-π, π]
while (deltaAngle > static_cast<float>(M_PI)) deltaAngle -= 2.f * static_cast<float>(M_PI);
while (deltaAngle < -static_cast<float>(M_PI)) deltaAngle += 2.f * static_cast<float>(M_PI);
float angleDeg = deltaAngle * (180.f / static_cast<float>(M_PI));
ApplyRotateDelta(m_dragAxis, angleDeg);
m_dragRotateLastAngle = currentAngle;
}
}
}
else if (m_mode == ManipulatorMode::Scale) {
ImVec2 pivSS, tipSS;
float screenDot = 0.f;
if (WorldToScreen(pivot, viewProj, viewW, viewH, imagePos, pivSS) &&
WorldToScreen(pivot + axes[m_dragAxis] * sf,
viewProj, viewW, viewH, imagePos, tipSS))
{
float axDx = tipSS.x - pivSS.x;
float axDy = tipSS.y - pivSS.y;
float axLen = std::sqrt(axDx*axDx + axDy*axDy);
if (axLen > 1e-3f)
screenDot = (delta.x*axDx + delta.y*axDy) / axLen;
}
float factor = 1.f + screenDot * 0.01f;
factor = std::max(0.01f, factor);
ApplyScaleDelta(m_dragAxis, factor);
}
m_dragLastPos = mouse;
}
else {
// Released — check whether the prim actually moved.
bool moved =
(m_dragStartTranslate != m_dragOriginalTranslate) ||
(m_dragStartRotate != m_dragOriginalRotate) ||
(m_dragStartScale != m_dragOriginalScale);
if (moved && m_commandHistory && m_stage && !m_primPath.IsEmpty()) {
// The Apply* helpers already wrote the final value to USD.
// Push a command so Undo can restore the original.
pxr::SdfLayerHandle editLayer = m_stage->GetEditTarget().GetLayer();
auto cmd = std::make_unique<TransformCommand>(
m_stage, m_primPath, editLayer,
m_dragOriginalTranslate, m_dragOriginalRotate, m_dragOriginalScale,
m_dragStartTranslate, m_dragStartRotate, m_dragStartScale,
m_dragOriginalRotOrder,
"Transform " + m_primPath.GetName());
// Execute() would write the new value again — we already wrote it,
// so push directly onto the stack without re-executing.
// We bypass Push() and manipulate the stacks via a "no-op execute" trick:
// wrap in a lambda that does nothing on first Execute().
// Simpler: just store final state as "new" and call Push which re-applies.
// Since the value is already applied, re-applying has no visible effect.
m_commandHistory->Push(std::move(cmd));
}
m_isDragging = false;
m_dragAxis = -1;
}
}
return consumed;
}
// ──────────────────────────────────────────────────────────────────────────────
// USD transform write helpers
// ──────────────────────────────────────────────────────────────────────────────
void TransformManipulator::ApplyMoveDelta(const pxr::GfVec3d& worldDelta)
{
if (!m_stage || m_primPath.IsEmpty()) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (!prim) return;
// XformCommonAPI::SetTranslate writes the prim's translation in *parent* space.
// The incoming worldDelta is in world space, so we must transform it into the
// parent's local space before accumulating.
//
// For a direction vector (no translation component) the conversion is:
// parentSpaceDelta = worldDelta * inverse(parentToWorld) [upper-3x3 only]
//
// When the parent is the pseudo-root its localToWorld is identity, so the
// conversion is a no-op for top-level prims.
pxr::GfVec3d parentSpaceDelta = worldDelta;
pxr::UsdPrim parent = prim.GetParent();
if (parent) {
pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default());
pxr::GfMatrix4d parentToWorld = xformCache.GetLocalToWorldTransform(parent);
double det = 0.0;
pxr::GfMatrix4d worldToParent = parentToWorld.GetInverse(&det);
if (std::abs(det) > 1e-9) {
// TransformDir applies only the rotation+scale part (no translation),
// which is correct for a displacement/direction vector.
parentSpaceDelta = worldToParent.TransformDir(worldDelta);
}
}
pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget());
pxr::UsdGeomXformCommonAPI api(prim);
m_dragStartTranslate += parentSpaceDelta;
api.SetTranslate(m_dragStartTranslate, pxr::UsdTimeCode::Default());
}
void TransformManipulator::ApplyRotateDelta(int axisIndex, float angleDeg)
{
if (!m_stage || m_primPath.IsEmpty()) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (!prim) return;
pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget());
pxr::UsdGeomXformCommonAPI api(prim);
m_dragStartRotate[axisIndex] += angleDeg;
api.SetRotate(m_dragStartRotate,
pxr::UsdGeomXformCommonAPI::RotationOrderXYZ,
pxr::UsdTimeCode::Default());
}
void TransformManipulator::ApplyScaleDelta(int axisIndex, float factor)
{
if (!m_stage || m_primPath.IsEmpty()) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (!prim) return;
pxr::UsdEditContext ec(m_stage, m_stage->GetEditTarget());
pxr::UsdGeomXformCommonAPI api(prim);
m_dragStartScale[axisIndex] *= factor;
m_dragStartScale[axisIndex] = std::max(0.001f, m_dragStartScale[axisIndex]);
api.SetScale(m_dragStartScale, pxr::UsdTimeCode::Default());
}
} // namespace UsdLayerManager
+211
View File
@@ -0,0 +1,211 @@
#pragma once
#include <imgui.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
namespace UsdLayerManager {
class CommandHistory;
/// Active transform tool mode — mirrors Maya Q/W/E/R convention.
enum class ManipulatorMode {
Select, ///< Q — no gizmo, normal click-to-select
Move, ///< W — translate along axis arrows
Rotate, ///< E — rotate around axis rings
Scale ///< R — scale along axis handles
};
/// Coordinate space in which the gizmo axes are expressed.
enum class TransformSpace {
Object, ///< Gizmo axes align with the selected prim's local axes (default)
World, ///< Gizmo axes are fixed world-space X/Y/Z
};
/// Maya-style interactive transform gizmo.
///
/// Rendering is done with ImGui DrawList (2-D screen-space overlay), drawn
/// AFTER ImGui::Image() for the viewport — exactly the same approach used by
/// ImGuizmo. No OpenGL resources are needed; the FBO bind/unbind dance is
/// entirely eliminated.
///
/// Gizmo world-space size is computed each frame using ImGuizmo's screen-
/// factor formula: project a camera-aligned unit vector to clip space and
/// derive the world size that spans a fixed fraction of the screen. This
/// gives constant apparent size regardless of camera distance or FOV.
class TransformManipulator {
public:
TransformManipulator() = default;
~TransformManipulator() = default;
// -----------------------------------------------------------------------
// Mode
// -----------------------------------------------------------------------
void SetMode(ManipulatorMode mode) { m_mode = mode; }
ManipulatorMode GetMode() const { return m_mode; }
// -----------------------------------------------------------------------
// Transform space
// -----------------------------------------------------------------------
void SetTransformSpace(TransformSpace space) { m_transformSpace = space; }
TransformSpace GetTransformSpace() const { return m_transformSpace; }
// -----------------------------------------------------------------------
// Stage / selection
// -----------------------------------------------------------------------
void SetStage(pxr::UsdStageRefPtr stage);
void SetSelectedPrim(const pxr::SdfPath& path);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
// -----------------------------------------------------------------------
// Per-frame API (called from ViewportPanel::Render)
// -----------------------------------------------------------------------
/// Draw the gizmo as a 2-D overlay onto @p dl.
/// Call AFTER ImGui::Image() so the overlay appears on top of the scene.
/// @param dl ImGui::GetWindowDrawList() of the Viewport window.
/// @param viewProj Combined view × projection matrix (USD row-major).
/// @param pivot World-space pivot (bounding-box centre of selection).
/// @param cameraEye World-space camera eye position (for half-arc orientation).
/// @param imagePos Screen-space top-left corner of the rendered image.
/// @param viewW/H Viewport pixel dimensions.
void Render(ImDrawList* dl,
const pxr::GfMatrix4d& viewProj,
const pxr::GfVec3d& pivot,
const pxr::GfVec3d& cameraEye,
const ImVec2& imagePos,
int viewW, int viewH);
/// Process mouse input. Must be called BEFORE camera-drag / prim-pick
/// logic in ViewportPanel so the gizmo can consume LMB clicks first.
/// @return true if the gizmo consumed the event.
bool HandleInput(const pxr::GfMatrix4d& viewProj,
const pxr::GfVec3d& pivot,
const pxr::GfVec3d& cameraEye,
const ImVec2& imagePos,
int viewW, int viewH,
bool viewportHovered);
bool IsDragging() const { return m_isDragging; }
private:
// -----------------------------------------------------------------------
// ImGuizmo-style screen-factor computation
// -----------------------------------------------------------------------
/// Compute the world-space gizmo size so that the gizmo spans
/// @p desiredFraction of the smaller viewport dimension in NDC.
///
/// Algorithm (from ImGuizmo):
/// 1. Project @p pivot to clip space.
/// 2. Project @p pivot + each world axis unit vector to clip space.
/// 3. Measure clip-space length (aspect-ratio corrected).
/// 4. screenFactor = desiredFraction / maxClipLen.
static float ComputeScreenFactor(const pxr::GfMatrix4d& viewProj,
const pxr::GfVec3d& pivot,
int viewW, int viewH,
float desiredFraction = 0.15f);
// -----------------------------------------------------------------------
// Screen-space helpers
// -----------------------------------------------------------------------
/// Project a world-space point to absolute screen coordinates.
/// Returns false if the point is behind the camera (w ≤ 0).
static bool WorldToScreen(const pxr::GfVec3d& world,
const pxr::GfMatrix4d& viewProj,
int viewW, int viewH,
const ImVec2& imagePos,
ImVec2& outScreen);
/// Distance from point @p p to line segment @p a @p b (2-D).
static float PointToSegmentDist(ImVec2 p, ImVec2 a, ImVec2 b);
// -----------------------------------------------------------------------
// Per-mode drawing (all ImGui DrawList, screen-space)
// -----------------------------------------------------------------------
void DrawMoveGizmo (ImDrawList* dl, const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const ImVec2& imgPos, int vW, int vH,
const pxr::GfVec3d axes[3]);
void DrawRotateGizmo(ImDrawList* dl, const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const pxr::GfVec3d& cameraEye,
const ImVec2& imgPos, int vW, int vH,
const pxr::GfVec3d axes[3]);
void DrawScaleGizmo (ImDrawList* dl, const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const ImVec2& imgPos, int vW, int vH,
const pxr::GfVec3d axes[3]);
// -----------------------------------------------------------------------
// Hit-testing
// -----------------------------------------------------------------------
/// Returns axis index 0=X 1=Y 2=Z, or -1 if nothing hit (move / scale).
int HitTestAxes(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const ImVec2& imgPos, int vW, int vH,
const ImVec2& mousePosAbsolute,
const pxr::GfVec3d axes[3]) const;
/// Returns axis index 0=X 1=Y 2=Z, or -1 if nothing hit (rotate rings).
/// Uses proximity to the VISIBLE front-facing half-arc only.
int HitTestRotateRings(const pxr::GfMatrix4d& vp,
const pxr::GfVec3d& pivot, float sf,
const pxr::GfVec3d& cameraEye,
const ImVec2& imgPos, int vW, int vH,
const ImVec2& mousePosAbsolute,
const pxr::GfVec3d axes[3]) const;
// -----------------------------------------------------------------------
// USD transform write helpers
// -----------------------------------------------------------------------
void ApplyMoveDelta (const pxr::GfVec3d& worldDelta);
void ApplyRotateDelta(int axisIndex, float angleDeg);
void ApplyScaleDelta (int axisIndex, float factor);
/// Fills @p outAxes[3] with the gizmo X/Y/Z axis directions in world space.
/// In World space: fixed unit vectors.
/// In Object space: the prim's local axes extracted from its local-to-world matrix.
void GetGizmoAxes(pxr::GfVec3d outAxes[3]) const;
// -----------------------------------------------------------------------
// State
// -----------------------------------------------------------------------
ManipulatorMode m_mode = ManipulatorMode::Select;
TransformSpace m_transformSpace = TransformSpace::Object;
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_primPath;
CommandHistory* m_commandHistory = nullptr;
// Drag state
bool m_isDragging = false;
int m_dragAxis = -1;
ImVec2 m_dragLastPos = {0.f, 0.f};
// For rotation drag: screen-angle around projected pivot center
float m_dragRotateLastAngle = 0.f; ///< atan2 angle of mouse around pivot (radians)
// Saved xform at drag START (never mutated during drag — used for undo)
pxr::GfVec3d m_dragOriginalTranslate = {0.0, 0.0, 0.0};
pxr::GfVec3f m_dragOriginalRotate = {0.f, 0.f, 0.f};
pxr::GfVec3f m_dragOriginalScale = {1.f, 1.f, 1.f};
pxr::UsdGeomXformCommonAPI::RotationOrder m_dragOriginalRotOrder =
pxr::UsdGeomXformCommonAPI::RotationOrderXYZ;
// Working accumulator for the current drag (updated each frame)
pxr::GfVec3d m_dragStartTranslate = {0.0, 0.0, 0.0};
pxr::GfVec3f m_dragStartRotate = {0.f, 0.f, 0.f};
pxr::GfVec3f m_dragStartScale = {1.f, 1.f, 1.f};
// Hover highlight
int m_hoveredAxis = -1;
};
} // namespace UsdLayerManager
+601
View File
@@ -0,0 +1,601 @@
#include "ViewportPanel.h"
#include "../utils/Logger.h"
#include <imgui.h>
#include <algorithm>
#include <string>
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// Construction
// ---------------------------------------------------------------------------
ViewportPanel::ViewportPanel()
{
EnsureTileCount(1); // start with a single tile
}
ViewportPanel::~ViewportPanel() = default;
// ---------------------------------------------------------------------------
// EnsureTileCount / WireCallbacks
// ---------------------------------------------------------------------------
void ViewportPanel::EnsureTileCount(int count)
{
// Grow
while (static_cast<int>(m_tiles.size()) < count) {
int idx = static_cast<int>(m_tiles.size());
m_tiles.push_back(std::make_unique<ViewportTile>());
if (m_stage) m_tiles.back()->SetStage(m_stage);
if (m_iconManager) m_tiles.back()->SetIconManager(m_iconManager);
if (m_commandHistory) m_tiles.back()->SetCommandHistory(m_commandHistory);
m_tiles.back()->SetSelectedPaths(m_selectedSdfPaths, m_selectedPrimPath);
WireCallbacks(idx);
}
// Shrink
while (static_cast<int>(m_tiles.size()) > count)
m_tiles.pop_back();
// Clamp indices
m_focusedTileIndex = std::min(m_focusedTileIndex,
std::max(0, static_cast<int>(m_tiles.size()) - 1));
if (m_maximizedTileIndex >= static_cast<int>(m_tiles.size()))
m_maximizedTileIndex = -1;
}
void ViewportPanel::WireCallbacks(int i)
{
m_tiles[i]->OnPrimPicked = [this](const std::string& path) {
m_selectedPrimPath = path;
m_selectedSdfPaths.clear();
if (!path.empty())
m_selectedSdfPaths.push_back(pxr::SdfPath(path));
BroadcastSelection();
if (OnPrimPicked) OnPrimPicked(path);
};
m_tiles[i]->OnPrimsPickedRect = [this](const std::vector<std::string>& paths) {
m_selectedSdfPaths.clear();
for (const auto& p : paths)
m_selectedSdfPaths.push_back(pxr::SdfPath(p));
m_selectedPrimPath = m_selectedSdfPaths.empty()
? "" : m_selectedSdfPaths.front().GetString();
BroadcastSelection();
if (OnPrimsPickedRect) OnPrimsPickedRect(paths);
};
}
// ---------------------------------------------------------------------------
// BroadcastSelection / UpdateFocusTile
// ---------------------------------------------------------------------------
void ViewportPanel::BroadcastSelection()
{
for (auto& t : m_tiles)
t->SetSelectedPaths(m_selectedSdfPaths, m_selectedPrimPath);
pxr::SdfPath primary = m_selectedSdfPaths.empty()
? pxr::SdfPath() : m_selectedSdfPaths.front();
m_manipulator.SetSelectedPrim(primary);
}
void ViewportPanel::UpdateFocusTile(int idx)
{
if (idx < 0 || idx >= static_cast<int>(m_tiles.size())) return;
m_focusedTileIndex = idx;
}
// ---------------------------------------------------------------------------
// Public setup
// ---------------------------------------------------------------------------
void ViewportPanel::SetStage(pxr::UsdStageRefPtr stage)
{
m_stage = stage;
m_manipulator.SetStage(stage);
for (auto& t : m_tiles) t->SetStage(stage);
m_selectedSdfPaths.clear();
m_selectedPrimPath.clear();
BroadcastSelection();
}
void ViewportPanel::FrameScene()
{
for (auto& t : m_tiles) t->FrameScene();
}
void ViewportPanel::SetCommandHistory(CommandHistory* history)
{
m_commandHistory = history;
m_manipulator.SetCommandHistory(history);
for (auto& t : m_tiles) t->SetCommandHistory(history);
}
void ViewportPanel::SetIconManager(IconManager* icons)
{
m_iconManager = icons;
for (auto& t : m_tiles) t->SetIconManager(icons);
}
void ViewportPanel::SetSelectedPrimPath(const std::string& path)
{
m_selectedPrimPath = path;
m_selectedSdfPaths.clear();
if (!path.empty())
m_selectedSdfPaths.push_back(pxr::SdfPath(path));
BroadcastSelection();
}
// ---------------------------------------------------------------------------
// Forwarding accessors
// ---------------------------------------------------------------------------
ViewportCamera& ViewportPanel::GetCamera()
{
return m_tiles[static_cast<size_t>(m_focusedTileIndex)]->GetCamera();
}
UsdSceneRenderer& ViewportPanel::GetRenderer()
{
return m_tiles[static_cast<size_t>(m_focusedTileIndex)]->GetRenderer();
}
// ---------------------------------------------------------------------------
// SetLayout
// ---------------------------------------------------------------------------
void ViewportPanel::SetLayout(LayoutMode mode)
{
m_layout = mode;
m_maximizedTileIndex = -1;
switch (mode) {
case LayoutMode::Single: EnsureTileCount(1); break;
case LayoutMode::HSplit: EnsureTileCount(2); break;
case LayoutMode::VSplit: EnsureTileCount(2); break;
case LayoutMode::Quad: EnsureTileCount(4); break;
}
}
// ---------------------------------------------------------------------------
// ComputeTileRects
// ---------------------------------------------------------------------------
std::vector<ViewportPanel::TileRect>
ViewportPanel::ComputeTileRects(ImVec2 origin, ImVec2 total) const
{
std::vector<TileRect> rects;
switch (m_layout) {
case LayoutMode::Single:
rects.push_back({ origin, total });
break;
case LayoutMode::HSplit: {
float leftW = total.x * m_splitH;
float rightW = total.x - leftW;
rects.push_back({ origin, ImVec2(leftW, total.y) });
rects.push_back({ ImVec2(origin.x + leftW, origin.y), ImVec2(rightW, total.y) });
break;
}
case LayoutMode::VSplit: {
float topH = total.y * m_splitV;
float bottomH = total.y - topH;
rects.push_back({ origin, ImVec2(total.x, topH) });
rects.push_back({ ImVec2(origin.x, origin.y + topH), ImVec2(total.x, bottomH) });
break;
}
case LayoutMode::Quad: {
float leftW = total.x * m_splitH;
float rightW = total.x - leftW;
float topH = total.y * m_splitV;
float bottomH = total.y - topH;
rects.push_back({ origin, ImVec2(leftW, topH) });
rects.push_back({ ImVec2(origin.x + leftW, origin.y), ImVec2(rightW, topH) });
rects.push_back({ ImVec2(origin.x, origin.y + topH), ImVec2(leftW, bottomH) });
rects.push_back({ ImVec2(origin.x + leftW, origin.y + topH), ImVec2(rightW, bottomH) });
break;
}
}
// In multi-tile layouts inset every tile by 2px on all sides.
// Adjacent tiles then have a 4px gap (2px inset from each side) so both
// the focused border (2px) and the hovered border (1px) are fully visible.
if (m_layout != LayoutMode::Single) {
for (auto& r : rects) {
// r.pos.x += 2.f;
// r.pos.y += 2.f;
// r.size.x -= 4.f;
// r.size.y -= 4.f;
r.pos.x += 2.f;
r.pos.y += 2.f;
r.size.x -= 2.f;
r.size.y -= 2.f;
}
}
return rects;
}
// ---------------------------------------------------------------------------
bool ViewportPanel::IsMouseOverDivider(ImVec2 origin, ImVec2 total) const
{
if (m_layout == LayoutMode::Single) return false;
if (m_maximizedTileIndex >= 0) return false;
if (m_draggingDivH || m_draggingDivV) return true;
const float kDivHalf = 3.0f;
ImVec2 mouse = ImGui::GetMousePos();
if (m_layout == LayoutMode::HSplit || m_layout == LayoutMode::Quad) {
float divX = origin.x + total.x * m_splitH;
if (mouse.x >= divX - kDivHalf && mouse.x <= divX + kDivHalf &&
mouse.y >= origin.y && mouse.y <= origin.y + total.y)
return true;
}
if (m_layout == LayoutMode::VSplit || m_layout == LayoutMode::Quad) {
float divY = origin.y + total.y * m_splitV;
if (mouse.y >= divY - kDivHalf && mouse.y <= divY + kDivHalf &&
mouse.x >= origin.x && mouse.x <= origin.x + total.x)
return true;
}
return false;
}
// ---------------------------------------------------------------------------
// DrawDividers
// ---------------------------------------------------------------------------
void ViewportPanel::DrawDividers(ImVec2 origin, ImVec2 total)
{
const float kDivThick = 6.0f;
const float kDivVisual = 2.0f;
const float kMinFrac = 0.1f;
const float kMaxFrac = 0.9f;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 mousePos = ImGui::GetMousePos();
// Use IsMouseClicked (edge-triggered) instead of IsMouseDown so that a
// divider drag only starts on a fresh press. If LMB is already held
// (e.g. the user is mid-rect-select in a tile) the divider is never
// accidentally triggered when the mouse drifts over the hit-zone.
bool lmbClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool lmbReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
// Vertical divider (HSplit / Quad)
if (m_layout == LayoutMode::HSplit || m_layout == LayoutMode::Quad) {
float divX = origin.x + total.x * m_splitH;
ImVec2 hMin(divX - kDivThick * 0.5f, origin.y);
ImVec2 hMax(divX + kDivThick * 0.5f, origin.y + total.y);
bool hovering = !m_draggingDivV &&
mousePos.x >= hMin.x && mousePos.x <= hMax.x &&
mousePos.y >= hMin.y && mousePos.y <= hMax.y;
if ((hovering || m_draggingDivH) && !m_draggingDivV)
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
if (hovering && lmbClicked && !m_draggingDivH && !m_draggingDivV)
m_draggingDivH = true;
if (m_draggingDivH) {
float f = (mousePos.x - origin.x) / total.x;
m_splitH = std::max(kMinFrac, std::min(kMaxFrac, f));
if (lmbReleased) m_draggingDivH = false;
}
// ImU32 col = (hovering || m_draggingDivH) ? IM_COL32(66,150,250,200) : IM_COL32(80,80,80,180);
ImU32 col = (hovering || m_draggingDivH) ? IM_COL32(250,150,66,200) : IM_COL32(80,80,80,180);
dl->AddLine(ImVec2(divX, origin.y), ImVec2(divX, origin.y + total.y), col, kDivVisual);
}
// Horizontal divider (VSplit / Quad)
if (m_layout == LayoutMode::VSplit || m_layout == LayoutMode::Quad) {
float divY = origin.y + total.y * m_splitV;
ImVec2 hMin(origin.x, divY - kDivThick * 0.5f);
ImVec2 hMax(origin.x + total.x, divY + kDivThick * 0.5f);
bool hovering = !m_draggingDivH &&
mousePos.x >= hMin.x && mousePos.x <= hMax.x &&
mousePos.y >= hMin.y && mousePos.y <= hMax.y;
if ((hovering || m_draggingDivV) && !m_draggingDivH)
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
if (hovering && lmbClicked && !m_draggingDivH && !m_draggingDivV)
m_draggingDivV = true;
if (m_draggingDivV) {
float f = (mousePos.y - origin.y) / total.y;
m_splitV = std::max(kMinFrac, std::min(kMaxFrac, f));
if (lmbReleased) m_draggingDivV = false;
}
// ImU32 col = (hovering || m_draggingDivV) ? IM_COL32(66,150,250,200) : IM_COL32(80,80,80,180);
ImU32 col = (hovering || m_draggingDivV) ? IM_COL32(250,150,66,200) : IM_COL32(80,80,80,180);
dl->AddLine(ImVec2(origin.x, divY), ImVec2(origin.x + total.x, divY), col, kDivVisual);
}
}
// ---------------------------------------------------------------------------
// HandleMaximizeInput
// ---------------------------------------------------------------------------
void ViewportPanel::HandleMaximizeInput(int hoveredTileIndex)
{
ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) return;
if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
if (m_maximizedTileIndex >= 0) {
m_maximizedTileIndex = -1;
m_layout = m_layoutBeforeMaximize;
m_splitH = m_splitHBefore;
m_splitV = m_splitVBefore;
switch (m_layout) {
case LayoutMode::Single: EnsureTileCount(1); break;
case LayoutMode::HSplit: EnsureTileCount(2); break;
case LayoutMode::VSplit: EnsureTileCount(2); break;
case LayoutMode::Quad: EnsureTileCount(4); break;
}
} else if (m_layout != LayoutMode::Single && hoveredTileIndex >= 0) {
m_layoutBeforeMaximize = m_layout;
m_splitHBefore = m_splitH;
m_splitVBefore = m_splitV;
m_maximizedTileIndex = hoveredTileIndex;
UpdateFocusTile(hoveredTileIndex);
}
}
if (m_maximizedTileIndex >= 0 && ImGui::IsKeyPressed(ImGuiKey_Escape)) {
m_maximizedTileIndex = -1;
m_layout = m_layoutBeforeMaximize;
m_splitH = m_splitHBefore;
m_splitV = m_splitVBefore;
switch (m_layout) {
case LayoutMode::Single: EnsureTileCount(1); break;
case LayoutMode::HSplit: EnsureTileCount(2); break;
case LayoutMode::VSplit: EnsureTileCount(2); break;
case LayoutMode::Quad: EnsureTileCount(4); break;
}
}
}
// ---------------------------------------------------------------------------
// RenderGlobalLeftToolbar
// ---------------------------------------------------------------------------
// Draws a single vertical icon-button toolbar on the left edge of the
// viewport content area. Sections (top to bottom):
// [1][H][V][4] — layout mode
// ─────────────
// [Q][W][E][R] — manipulator tool (global, like Maya)
// ─────────────
// [W|O] — transform space toggle
// ---------------------------------------------------------------------------
void ViewportPanel::RenderGlobalLeftToolbar(ImVec2 contentPos, ImVec2 /*contentSize*/)
{
const float kBtnSize = 32.0f;
const float kIconPad = 5.0f;
const float kRounding = 4.0f;
const float kSpacing = 3.0f;
const float kPadX = 9.0f; // left padding inside the strip
const float kPadY = 10.0f; // top padding
const float kSepH = 1.0f; // separator line height
const float kSepGap = 6.0f; // space around separator
const ImVec2 kBtnSz(kBtnSize, kBtnSize);
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 mouse = ImGui::GetMousePos();
bool lmbClk = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
// Current Y cursor
float x = contentPos.x + kPadX;
float y = contentPos.y + kPadY;
// Helper: draw one square icon button, return true if clicked.
// `active` tints the button blue. Falls back to centred text if no icon.
auto DrawBtn = [&](const char* id,
Icon iconEnum,
const char* fallbackLabel,
bool active,
const char* tooltip) -> bool
{
ImVec2 bMin(x, y);
ImVec2 bMax(x + kBtnSize, y + kBtnSize);
bool hov = mouse.x >= bMin.x && mouse.x <= bMax.x &&
mouse.y >= bMin.y && mouse.y <= bMax.y;
bool clicked = hov && lmbClk;
ImU32 bg = active ? IM_COL32( 66, 150, 250, 230) :
hov ? IM_COL32( 70, 70, 70, 220) :
IM_COL32( 32, 32, 32, 178);
dl->AddRectFilled(bMin, bMax, bg, kRounding);
if (active)
dl->AddRect(bMin, bMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f);
if (m_iconManager) {
ImTextureID tex = m_iconManager->Get(iconEnum);
dl->AddImage(ImTextureRef(tex),
ImVec2(bMin.x + kIconPad, bMin.y + kIconPad),
ImVec2(bMax.x - kIconPad, bMax.y - kIconPad));
} else {
ImVec2 ts = ImGui::CalcTextSize(fallbackLabel);
dl->AddText(
ImVec2(bMin.x + (kBtnSize - ts.x) * 0.5f,
bMin.y + (kBtnSize - ts.y) * 0.5f),
IM_COL32(255, 255, 255, 255), fallbackLabel);
}
if (hov) ImGui::SetTooltip("%s", tooltip);
y += kBtnSize + kSpacing;
(void)id;
return clicked;
};
// Helper: thin horizontal separator
auto DrawSep = [&]() {
y += kSepGap;
dl->AddLine(ImVec2(x - 2.f, y), ImVec2(x + kBtnSize + 2.f, y),
IM_COL32(80, 80, 80, 160), kSepH);
y += kSepH + kSepGap;
};
// ── Section 1: Layout mode ───────────────────────────────────────────────
// Use Layout icons if available, otherwise render small Unicode glyphs.
// We don't currently have dedicated layout icons in IconManager so we use
// the fallback text path with descriptive single-character labels.
struct LayoutEntry {
LayoutMode mode;
Icon icon;
const char* label; // fallback text when no icon manager
const char* tooltip;
};
static const LayoutEntry kLayouts[] = {
{ LayoutMode::Single, Icon::LayoutSingle, "1", "Single viewport [1]" },
{ LayoutMode::HSplit, Icon::LayoutHSplit, "H", "Split left|right [H]" },
{ LayoutMode::VSplit, Icon::LayoutVSplit, "V", "Split top/bottom [V]" },
{ LayoutMode::Quad, Icon::LayoutQuad, "4", "4-quadrant grid [4]" },
};
for (const auto& lk : kLayouts) {
bool active = (m_layout == lk.mode);
ImVec2 bMin(x, y);
ImVec2 bMax(x + kBtnSize, y + kBtnSize);
bool hov = mouse.x >= bMin.x && mouse.x <= bMax.x &&
mouse.y >= bMin.y && mouse.y <= bMax.y;
bool clicked = hov && lmbClk;
ImU32 bg = active ? IM_COL32( 66, 150, 250, 230) :
hov ? IM_COL32( 70, 70, 70, 220) :
IM_COL32( 32, 32, 32, 178);
dl->AddRectFilled(bMin, bMax, bg, kRounding);
if (active)
dl->AddRect(bMin, bMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f);
if (m_iconManager) {
ImTextureID tex = m_iconManager->Get(lk.icon);
dl->AddImage(ImTextureRef(tex),
ImVec2(bMin.x + kIconPad, bMin.y + kIconPad),
ImVec2(bMax.x - kIconPad, bMax.y - kIconPad));
} else {
ImVec2 ts = ImGui::CalcTextSize(lk.label);
dl->AddText(
ImVec2(bMin.x + (kBtnSize - ts.x) * 0.5f,
bMin.y + (kBtnSize - ts.y) * 0.5f),
IM_COL32(255, 255, 255, 255), lk.label);
}
if (hov) ImGui::SetTooltip("%s", lk.tooltip);
if (clicked) SetLayout(lk.mode);
y += kBtnSize + kSpacing;
}
DrawSep();
// ── Section 2: Manipulator tool mode (global, Q/W/E/R) ──────────────────
struct ToolEntry {
ManipulatorMode mode;
Icon icon;
const char* label;
const char* tooltip;
};
static const ToolEntry kTools[] = {
{ ManipulatorMode::Select, Icon::ToolSelect, "Q", "Select (Q)" },
{ ManipulatorMode::Move, Icon::ToolMove, "W", "Move (W)" },
{ ManipulatorMode::Rotate, Icon::ToolRotate, "E", "Rotate (E)" },
{ ManipulatorMode::Scale, Icon::ToolScale, "R", "Scale (R)" },
};
ManipulatorMode curMode = m_manipulator.GetMode();
for (const auto& tk : kTools) {
if (DrawBtn(tk.label, tk.icon, tk.label, curMode == tk.mode, tk.tooltip))
m_manipulator.SetMode(tk.mode);
}
DrawSep();
// ── Section 3: Transform space toggle ───────────────────────────────────
bool isWorld = (m_manipulator.GetTransformSpace() == TransformSpace::World);
if (DrawBtn("WO",
isWorld ? Icon::WorldSpace : Icon::LocalSpace,
isWorld ? "W" : "O",
isWorld,
isWorld ? "World space (click → Object)" : "Object space (click → World)"))
{
m_manipulator.SetTransformSpace(isWorld ? TransformSpace::Object
: TransformSpace::World);
}
}
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
void ViewportPanel::Render()
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::Begin("Viewport", nullptr,
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
// Full content area (no top toolbar — layout buttons are now in the left toolbar)
ImVec2 contentPos = ImGui::GetCursorScreenPos();
ImVec2 contentSize = ImGui::GetContentRegionAvail();
// Reserve kToolbarW pixels on the left for the global toolbar.
// Tiles occupy the remaining area to the right.
ImVec2 tilesPos (contentPos.x + kToolbarW, contentPos.y);
ImVec2 tilesSize(contentSize.x - kToolbarW, contentSize.y);
// ── Global keyboard shortcuts (Q/W/E/R — no hover gate, truly global) ────
{
ImGuiIO& io = ImGui::GetIO();
if (!io.WantTextInput) {
if (ImGui::IsKeyPressed(ImGuiKey_Q)) m_manipulator.SetMode(ManipulatorMode::Select);
if (ImGui::IsKeyPressed(ImGuiKey_W)) m_manipulator.SetMode(ManipulatorMode::Move);
if (ImGui::IsKeyPressed(ImGuiKey_E)) m_manipulator.SetMode(ManipulatorMode::Rotate);
if (ImGui::IsKeyPressed(ImGuiKey_R)) m_manipulator.SetMode(ManipulatorMode::Scale);
}
}
// ── Render tiles ──────────────────────────────────────────────────────────
int hoveredTileIndex = -1;
if (m_maximizedTileIndex >= 0 &&
m_maximizedTileIndex < static_cast<int>(m_tiles.size()))
{
// Maximised: tile fills the tile area (not the toolbar strip)
int i = m_maximizedTileIndex;
m_tiles[i]->Render(i, tilesPos, tilesSize, /*isFocused=*/true, m_manipulator,
/*dividerActive=*/false);
if (m_tiles[i]->WasClickedThisFrame()) UpdateFocusTile(i);
if (m_tiles[i]->IsHoveredThisFrame()) hoveredTileIndex = i;
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddText(
ImVec2(tilesPos.x + tilesSize.x - 200.f, tilesPos.y + 5.f),
IM_COL32(255, 200, 0, 160),
"Maximized [Space / Esc] restore");
}
else
{
bool dividerActive = IsMouseOverDivider(tilesPos, tilesSize);
auto rects = ComputeTileRects(tilesPos, tilesSize);
for (int i = 0; i < static_cast<int>(m_tiles.size()); ++i) {
bool focused = (i == m_focusedTileIndex);
m_tiles[i]->Render(i, rects[i].pos, rects[i].size, focused, m_manipulator,
dividerActive);
if (m_tiles[i]->WasClickedThisFrame()) UpdateFocusTile(i);
if (m_tiles[i]->IsHoveredThisFrame()) hoveredTileIndex = i;
}
if (m_layout != LayoutMode::Single)
DrawDividers(tilesPos, tilesSize);
}
// ── Global left toolbar (layout + Q/W/E/R + space) ────────────────────────
// Drawn after tiles so it renders on top; uses raw screen-pos hit-testing
// so it is not inside any tile's BeginChild scope.
RenderGlobalLeftToolbar(contentPos, contentSize);
// ── Space / Escape maximize ───────────────────────────────────────────────
HandleMaximizeInput(hoveredTileIndex);
ImGui::End();
ImGui::PopStyleVar(); // outer WindowPadding
}
} // namespace UsdLayerManager
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include "ViewportTile.h"
#include "TransformManipulator.h"
#include "IconManager.h"
#include "../core/CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <imgui.h>
#include <functional>
#include <memory>
#include <vector>
#include <string>
namespace UsdLayerManager {
/// How many tiles the viewport area is divided into.
enum class LayoutMode {
Single, ///< 1 tile — full area
HSplit, ///< 2 tiles side by side (left | right)
VSplit, ///< 2 tiles top / bottom
Quad, ///< 4 tiles in a 2×2 grid
};
/// Multi-viewport container.
///
/// Owns N ViewportTile instances, a shared TransformManipulator, and the
/// authoritative selection state. Manages layout splitting, draggable
/// dividers, and the Maya-style Space-key maximize / restore.
class ViewportPanel {
public:
ViewportPanel();
~ViewportPanel();
// ── Setup ────────────────────────────────────────────────────────────────
void SetStage(pxr::UsdStageRefPtr stage);
void FrameScene();
void SetCommandHistory(CommandHistory* history);
void SetIconManager(IconManager* icons);
// ── Selection (called by SceneHierarchyPanel) ────────────────────────────
/// Set a single selected prim (clears any multi-selection).
void SetSelectedPrimPath(const std::string& path);
// ── Main render (called from Application::RenderUI) ──────────────────────
void Render();
// ── Pick callbacks (wired by Application after construction) ─────────────
std::function<void(const std::string&)> OnPrimPicked;
std::function<void(const std::vector<std::string>&)> OnPrimsPickedRect;
// ── Forwarding accessors (delegate to focused tile) ──────────────────────
ViewportCamera& GetCamera();
UsdSceneRenderer& GetRenderer();
// ── Layout ───────────────────────────────────────────────────────────────
void SetLayout(LayoutMode mode);
LayoutMode GetLayout() const { return m_layout; }
int GetFocusedTileIndex() const { return m_focusedTileIndex; }
private:
// ── Tile rect helper ─────────────────────────────────────────────────────
struct TileRect { ImVec2 pos; ImVec2 size; };
std::vector<TileRect> ComputeTileRects(ImVec2 origin, ImVec2 total) const;
// ── Render sub-functions ─────────────────────────────────────────────────
/// Draws the global vertical left toolbar (layout buttons + tool mode buttons).
/// Occupies a reserved strip of width kToolbarW on the left of the content area.
void RenderGlobalLeftToolbar(ImVec2 contentPos, ImVec2 contentSize);
void DrawDividers(ImVec2 origin, ImVec2 total);
void HandleMaximizeInput(int hoveredTileIndex);
/// Returns true when the mouse is currently over a divider hit-zone or a
/// divider drag is already in progress. Used to suppress tile rect-selection
/// when the user is resizing tiles.
bool IsMouseOverDivider(ImVec2 origin, ImVec2 total) const;
/// Width (px) of the reserved left toolbar strip.
static constexpr float kToolbarW = 52.0f;
// ── Selection management ─────────────────────────────────────────────────
/// Push the current shared selection into every tile and the manipulator.
void BroadcastSelection();
/// Update the focused tile index and update the gizmo's selected prim.
void UpdateFocusTile(int idx);
// ── Tile setup ───────────────────────────────────────────────────────────
/// (Re)create tiles so that exactly `count` tiles exist, reusing existing
/// ones where possible to preserve camera/settings state.
void EnsureTileCount(int count);
/// Wire pick callbacks for tile at index `i`.
void WireCallbacks(int i);
// ── Tiles ────────────────────────────────────────────────────────────────
std::vector<std::unique_ptr<ViewportTile>> m_tiles;
// ── Shared manipulator ───────────────────────────────────────────────────
TransformManipulator m_manipulator;
// ── Shared selection (authoritative) ────────────────────────────────────
pxr::SdfPathVector m_selectedSdfPaths;
std::string m_selectedPrimPath;
// ── Layout state ─────────────────────────────────────────────────────────
LayoutMode m_layout = LayoutMode::Single;
float m_splitH = 0.5f; ///< Horizontal divider (01); used by HSplit + Quad
float m_splitV = 0.5f; ///< Vertical divider (01); used by VSplit + Quad
// Divider drag state
bool m_draggingDivH = false; ///< Dragging the vertical line (changes m_splitH)
bool m_draggingDivV = false; ///< Dragging the horizontal line (changes m_splitV)
// ── Maximize state ───────────────────────────────────────────────────────
int m_maximizedTileIndex = -1; ///< -1 = not maximised
LayoutMode m_layoutBeforeMaximize = LayoutMode::Single;
float m_splitHBefore = 0.5f;
float m_splitVBefore = 0.5f;
// ── Focus ────────────────────────────────────────────────────────────────
int m_focusedTileIndex = 0;
// ── Shared dependencies forwarded to tiles ───────────────────────────────
pxr::UsdStageRefPtr m_stage;
IconManager* m_iconManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
};
} // namespace UsdLayerManager
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
#pragma once
#include "../core/UsdSceneRenderer.h"
#include "../core/ViewportCamera.h"
#include "../core/CommandHistory.h"
#include "TransformManipulator.h"
#include "IconManager.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <imgui.h>
#include <functional>
#include <memory>
#include <vector>
#include <string>
namespace UsdLayerManager {
/// Named orthographic view directions.
/// When m_orthoView != None the tile renders an orthographic camera locked to
/// that world-space direction; the free-camera orbital state (center + dist)
/// is reused for pan and zoom so each tile keeps an independent view.
enum class OrthoView {
None, ///< Not an ortho view — free camera or USD camera prim
Top,
Bottom,
Front,
Back,
Left,
Right,
};
/// A single viewport tile.
///
/// Owns its own ViewportCamera, UsdSceneRenderer, and all per-tile settings
/// (grid, AA, background colour, bbox mode, render delegate). Selection state
/// and the TransformManipulator are owned by the ViewportPanel container and
/// passed in per frame so they can be shared across tiles.
class ViewportTile {
public:
ViewportTile();
~ViewportTile();
// ── Setup (called once by container) ────────────────────────────────────
void SetStage(pxr::UsdStageRefPtr stage);
void SetIconManager(IconManager* icons) { m_iconManager = icons; }
void SetCommandHistory(CommandHistory* h);
// ── Selection sync ───────────────────────────────────────────────────────
/// Called by the container to broadcast the authoritative selection.
/// Updates the local shadow copy and pushes it into the renderer highlight.
void SetSelectedPaths(const pxr::SdfPathVector& paths,
const std::string& primaryPath);
// ── Per-frame render ─────────────────────────────────────────────────────
/// Render this tile inside an ImGui child window.
///
/// @param tileIndex Unique index used to disambiguate ImGui IDs.
/// @param pos Screen-space top-left of this tile's area.
/// @param size Pixel dimensions of this tile's area.
/// @param isFocused If true the transform gizmo renders here.
/// @param manipulator Shared manipulator owned by the container.
/// @param dividerActive When true a split-handle drag is active (or the
/// mouse is over one), so rect-selection is suppressed.
void Render(int tileIndex, ImVec2 pos, ImVec2 size,
bool isFocused, TransformManipulator& manipulator,
bool dividerActive = false);
// ── Pick callbacks (assigned by container after construction) ────────────
std::function<void(const std::string&)> OnPrimPicked;
std::function<void(const std::vector<std::string>&)> OnPrimsPickedRect;
// ── Per-frame state queries ──────────────────────────────────────────────
/// True when the user clicked LMB inside this tile during the last Render.
bool WasClickedThisFrame() const { return m_wasClickedThisFrame; }
/// True when the mouse was hovering this tile during the last Render.
bool IsHoveredThisFrame() const { return m_wasHoveredThisFrame; }
// ── Forwarding accessors ─────────────────────────────────────────────────
ViewportCamera& GetCamera() { return m_camera; }
UsdSceneRenderer& GetRenderer() { return m_renderer; }
void FrameScene();
private:
// ── Render sub-functions ─────────────────────────────────────────────────
pxr::GfCamera ResolveCamera();
/// Build an orthographic GfCamera from the current center/dist state.
pxr::GfCamera BuildOrthoCamera() const;
void HandleInput(bool isFocused, TransformManipulator& manipulator,
bool dividerActive);
void DrawSelectionRect();
void RenderContextMenu(int tileIndex);
void RenderCompactToolbar(int tileIndex);
void RenderManipulatorOverlay(TransformManipulator& manipulator);
// ── Camera helpers ───────────────────────────────────────────────────────
void RefreshCameraList();
void TrySwitchToFreeCamera();
void InitCameraNavigation();
pxr::GfVec3d ComputeGizmoPivot() const;
// ── Core components ──────────────────────────────────────────────────────
ViewportCamera m_camera;
UsdSceneRenderer m_renderer;
IconManager* m_iconManager = nullptr;
CommandHistory* m_commandHistory = nullptr;
// ── Stage + camera list ──────────────────────────────────────────────────
pxr::UsdStageRefPtr m_stage;
std::vector<pxr::SdfPath> m_cameraPaths;
int m_selectedCameraIndex = 0;
bool m_cameraListDirty = true;
// ── Camera navigation mouse state ────────────────────────────────────────
float m_lastMouseX = 0.f;
float m_lastMouseY = 0.f;
bool m_isOrbiting = false;
bool m_isPanning = false;
bool m_isDollying = false;
// ── Rect selection state ─────────────────────────────────────────────────
bool m_isRectSelecting = false;
bool m_rectDragStarted = false;
ImVec2 m_rectAnchor = {0.f, 0.f};
ImVec2 m_rectCurrent = {0.f, 0.f};
static constexpr float kRectDragThreshold = 5.0f;
// ── Viewport dimensions (updated each Render) ────────────────────────────
int m_viewWidth = 0;
int m_viewHeight = 0;
ImVec2 m_imageScreenPos = {0.f, 0.f};
// ── Selection shadow (synced by container) ───────────────────────────────
pxr::SdfPathVector m_selectedSdfPaths;
std::string m_selectedPrimPath;
// ── GfCamera cache ───────────────────────────────────────────────────────
pxr::GfCamera m_lastComputedGfCamera;
bool m_hasLastGfCamera = false;
// ── Free-camera saved state (before switching to a USD cam prim) ─────────
pxr::GfCamera m_savedFreeCameraState;
bool m_hasSavedFreeCameraState = false;
// ── USD camera prim navigation state ────────────────────────────────────
bool m_isDrivingUsdCamPrim = false;
pxr::SdfPath m_drivenUsdCamPath;
// ── Orthographic view ────────────────────────────────────────────────────
OrthoView m_orthoView = OrthoView::None;
// ── Per-frame interaction flags ──────────────────────────────────────────
bool m_wasClickedThisFrame = false;
bool m_wasHoveredThisFrame = false;
};
} // namespace UsdLayerManager