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
+59
View File
@@ -0,0 +1,59 @@
#include "CommandHistory.h"
#include "../utils/Logger.h"
namespace UsdLayerManager {
void CommandHistory::Push(std::unique_ptr<ICommand> cmd) {
if (!cmd) return;
try {
cmd->Execute();
} catch (...) {
LOG_ERROR("CommandHistory::Push — Execute() threw an exception; command discarded");
return;
}
m_undoStack.push_back(std::move(cmd));
m_redoStack.clear();
}
void CommandHistory::Undo() {
if (m_undoStack.empty()) return;
auto& cmd = m_undoStack.back();
try {
cmd->Undo();
} catch (...) {
LOG_ERROR("CommandHistory::Undo — Undo() threw an exception; history cleared for safety");
Clear();
return;
}
m_redoStack.push_back(std::move(cmd));
m_undoStack.pop_back();
}
void CommandHistory::Redo() {
if (m_redoStack.empty()) return;
auto& cmd = m_redoStack.back();
try {
cmd->Execute();
} catch (...) {
LOG_ERROR("CommandHistory::Redo — Execute() threw an exception; history cleared for safety");
Clear();
return;
}
m_undoStack.push_back(std::move(cmd));
m_redoStack.pop_back();
}
void CommandHistory::Clear() {
m_undoStack.clear();
m_redoStack.clear();
}
std::string CommandHistory::GetUndoDescription() const {
return m_undoStack.empty() ? std::string() : m_undoStack.back()->GetDescription();
}
std::string CommandHistory::GetRedoDescription() const {
return m_redoStack.empty() ? std::string() : m_redoStack.back()->GetDescription();
}
} // namespace UsdLayerManager
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <memory>
#include <vector>
#include <string>
namespace UsdLayerManager {
/// Pure-virtual interface for all reversible operations.
class ICommand {
public:
virtual ~ICommand() = default;
virtual void Execute() = 0;
virtual void Undo() = 0;
virtual std::string GetDescription() const = 0;
};
/// Application-level undo/redo stack.
///
/// Push(cmd) executes the command, pushes it onto the undo stack,
/// and clears the redo stack. Undo() pops from the undo stack, calls
/// Undo() on the command, and pushes it onto the redo stack (and vice
/// versa for Redo()).
///
/// Call Clear() whenever the USD stage is replaced so stale USD object
/// references (UsdPrim, UsdAttribute, SdfLayerHandle) cannot be dereferenced.
class CommandHistory {
public:
CommandHistory() = default;
~CommandHistory() = default;
/// Execute cmd and push onto undo stack; clears redo stack.
void Push(std::unique_ptr<ICommand> cmd);
/// Undo the top command (no-op if stack is empty).
void Undo();
/// Redo the top undone command (no-op if stack is empty).
void Redo();
/// Clear both stacks (must be called on stage close/open).
void Clear();
bool CanUndo() const { return !m_undoStack.empty(); }
bool CanRedo() const { return !m_redoStack.empty(); }
/// Description of the command that Undo() would reverse, or empty string.
std::string GetUndoDescription() const;
/// Description of the command that Redo() would replay, or empty string.
std::string GetRedoDescription() const;
private:
std::vector<std::unique_ptr<ICommand>> m_undoStack;
std::vector<std::unique_ptr<ICommand>> m_redoStack;
};
} // namespace UsdLayerManager
+215
View File
@@ -0,0 +1,215 @@
#include "LayerManager.h"
#include "../utils/Logger.h"
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/layerUtils.h>
#include <filesystem>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
LayerManager::LayerManager()
: m_stage(nullptr) {
}
LayerManager::~LayerManager() {
}
void LayerManager::SetStage(UsdStageRefPtr stage) {
m_stage = stage;
Refresh();
}
void LayerManager::Refresh() {
BuildLayerList();
}
std::vector<LayerInfo> LayerManager::GetLayerStack() const {
return m_layers;
}
std::vector<LayerInfo> LayerManager::GetSublayers() const {
std::vector<LayerInfo> sublayers;
if (!m_stage) return sublayers;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return sublayers;
for (const auto& info : m_layers) {
if (!info.isRootLayer && !info.isSessionLayer) {
sublayers.push_back(info);
}
}
return sublayers;
}
bool LayerManager::CreateSublayer(const std::string& identifier, int index) {
if (!m_stage) {
LOG_ERROR("No stage to create sublayer on");
return false;
}
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) {
LOG_ERROR("No root layer to add sublayer to");
return false;
}
try {
LOG_INFO("Creating sublayer: " + identifier);
rootLayer->InsertSubLayerPath(identifier, index);
Refresh();
return true;
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to create sublayer: ") + e.what());
return false;
}
}
bool LayerManager::InsertSublayerPath(const std::string& path, int index) {
return CreateSublayer(path, index);
}
bool LayerManager::RemoveSublayer(int index) {
if (!m_stage) return false;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
try {
SdfSubLayerProxy sublayers = rootLayer->GetSubLayerPaths();
if (index < 0 || static_cast<size_t>(index) >= sublayers.size()) {
LOG_ERROR("Sublayer index out of range");
return false;
}
LOG_INFO("Removing sublayer at index: " + std::to_string(index));
sublayers.erase(sublayers.begin() + index);
Refresh();
return true;
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to remove sublayer: ") + e.what());
return false;
}
}
bool LayerManager::MoveSublayerUp(int index) {
if (index <= 0) return false;
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
try {
SdfSubLayerProxy sublayers = rootLayer->GetSubLayerPaths();
if (static_cast<size_t>(index) >= sublayers.size()) return false;
std::string temp = sublayers[index];
sublayers[index] = sublayers[index - 1];
sublayers[index - 1] = temp;
Refresh();
return true;
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to move sublayer: ") + e.what());
return false;
}
}
bool LayerManager::MoveSublayerDown(int index) {
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return false;
try {
SdfSubLayerProxy sublayers = rootLayer->GetSubLayerPaths();
if (index < 0 || static_cast<size_t>(index + 1) >= sublayers.size()) return false;
std::string temp = sublayers[index];
sublayers[index] = sublayers[index + 1];
sublayers[index + 1] = temp;
Refresh();
return true;
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to move sublayer: ") + e.what());
return false;
}
}
void LayerManager::MuteLayer(const std::string& layerIdentifier) {
if (m_stage) {
m_stage->MuteLayer(layerIdentifier);
Refresh();
}
}
void LayerManager::UnmuteLayer(const std::string& layerIdentifier) {
if (m_stage) {
m_stage->UnmuteLayer(layerIdentifier);
Refresh();
}
}
bool LayerManager::IsLayerMuted(const std::string& layerIdentifier) const {
if (m_stage) {
return m_stage->IsLayerMuted(layerIdentifier);
}
return false;
}
std::string LayerManager::ExtractDisplayName(const std::string& identifier) {
// Try to extract a user-friendly name from the identifier
// For file paths, use the filename
std::filesystem::path path(identifier);
if (path.has_filename()) {
std::string filename = path.filename().string();
if (!filename.empty()) {
return filename;
}
}
// For anonymous layers or other identifiers, return a short representation
if (identifier.find("anon:") == 0) {
return "Anonymous Layer";
}
if (identifier.find("session:") == 0) {
return "Session Layer";
}
return identifier;
}
void LayerManager::BuildLayerList() {
m_layers.clear();
if (!m_stage) return;
try {
// Get the full layer stack (root + all sublayers)
SdfLayerHandleVector layerStack = m_stage->GetLayerStack();
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
SdfLayerHandle sessionLayer = m_stage->GetSessionLayer();
for (const auto& layer : layerStack) {
LayerInfo info;
info.layer = layer;
info.identifier = layer->GetIdentifier();
info.displayName = ExtractDisplayName(info.identifier);
info.realPath = layer->GetRealPath();
info.isMuted = layer->IsMuted();
info.isAnonymous = layer->IsAnonymous();
info.isRootLayer = (layer == rootLayer);
info.isSessionLayer = (layer == sessionLayer);
m_layers.push_back(info);
}
LOG_DEBUG("Built layer list with " + std::to_string(m_layers.size()) + " layers");
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to build layer list: ") + e.what());
}
}
} // namespace UsdLayerManager
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
#include <vector>
#include <memory>
namespace UsdLayerManager {
struct LayerInfo {
pxr::SdfLayerHandle layer;
std::string identifier;
std::string displayName;
std::string realPath;
bool isMuted;
bool isAnonymous;
bool isRootLayer;
bool isSessionLayer;
};
class LayerManager {
public:
LayerManager();
~LayerManager();
void SetStage(pxr::UsdStageRefPtr stage);
void Refresh();
// Layer information
std::vector<LayerInfo> GetLayerStack() const;
std::vector<LayerInfo> GetSublayers() const;
int GetLayerCount() const { return static_cast<int>(m_layers.size()); }
// Layer operations
bool CreateSublayer(const std::string& identifier, int index = -1);
bool InsertSublayerPath(const std::string& path, int index = -1);
bool RemoveSublayer(int index);
bool MoveSublayerUp(int index);
bool MoveSublayerDown(int index);
// Muting
void MuteLayer(const std::string& layerIdentifier);
void UnmuteLayer(const std::string& layerIdentifier);
bool IsLayerMuted(const std::string& layerIdentifier) const;
// Utility
static std::string ExtractDisplayName(const std::string& identifier);
private:
void BuildLayerList();
pxr::UsdStageRefPtr m_stage;
std::vector<LayerInfo> m_layers;
};
} // namespace UsdLayerManager
+223
View File
@@ -0,0 +1,223 @@
#include "PropertyManager.h"
#include "LayerManager.h"
#include "../utils/Logger.h"
#include <pxr/usd/usd/prim.h>
#include <pxr/usd/usd/primRange.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/vt/value.h>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
PropertyManager::PropertyManager()
: m_stage(nullptr) {
}
PropertyManager::~PropertyManager() {
}
void PropertyManager::SetStage(UsdStageRefPtr stage) {
m_stage = stage;
m_currentLayer = nullptr;
}
void PropertyManager::SetCurrentLayer(const SdfLayerHandle& layer) {
m_currentLayer = layer;
}
std::vector<PropertyInfo> PropertyManager::GetPrimProperties(const std::string& primPath) {
UsdPrim prim = GetPrim(primPath);
if (!prim.IsValid()) {
return {};
}
return GetPrimProperties(prim);
}
std::vector<PropertyInfo> PropertyManager::GetPrimProperties(const UsdPrim& prim) {
std::vector<PropertyInfo> props;
if (!prim.IsValid()) return props;
CollectProperties(prim, props);
return props;
}
void PropertyManager::CollectProperties(const UsdPrim& prim, std::vector<PropertyInfo>& props) {
for (const auto& prop : prim.GetAttributes()) {
PropertyInfo info;
info.name = prop.GetName().GetString();
info.displayName = info.name;
info.attribute = prop;
info.typeName = prop.GetTypeName().GetAsToken().GetString();
info.hasValue = prop.HasValue();
info.layerStack = GetPropertyLayerStack(prop);
if (info.hasValue) {
info.value = ExtractValue(prop);
}
props.push_back(info);
}
}
PropertyValue PropertyManager::ExtractValue(const UsdAttribute& attr) {
PropertyValue result = std::string("");
auto typeName = attr.GetTypeName();
auto roleName = typeName.GetRole();
if (typeName == SdfValueTypeNames->Bool) {
bool val = false;
attr.Get(&val);
result = val;
} else if (typeName == SdfValueTypeNames->Int) {
int val = 0;
attr.Get(&val);
result = val;
} else if (typeName == SdfValueTypeNames->Float) {
float val = 0.0f;
attr.Get(&val);
result = val;
} else if (typeName == SdfValueTypeNames->Double) {
double val = 0.0;
attr.Get(&val);
result = val;
} else if (typeName == SdfValueTypeNames->Float3 || typeName == SdfValueTypeNames->Vector3f) {
GfVec3f val(0.0f);
attr.Get(&val);
result = val;
} else if (typeName == SdfValueTypeNames->Double3 || typeName == SdfValueTypeNames->Vector3d) {
GfVec3d val(0.0);
attr.Get(&val);
result = val;
} else if (typeName == SdfValueTypeNames->String || typeName == SdfValueTypeNames->Token) {
std::string val;
attr.Get(&val);
result = val;
} else {
// Fallback: get as string representation
VtValue vtVal;
if (attr.Get(&vtVal)) {
result = vtVal.GetTypeName();
}
}
return result;
}
bool PropertyManager::SetPropertyValue(const std::string& primPath,
const std::string& propName,
const PropertyValue& value) {
UsdPrim prim = GetPrim(primPath);
if (!prim.IsValid()) {
LOG_ERROR("Invalid prim path: " + primPath);
return false;
}
UsdAttribute attr = prim.GetAttribute(TfToken(propName));
if (!attr.IsValid()) {
LOG_ERROR("Invalid attribute: " + propName);
return false;
}
// If a current layer is set, create an edit context to target it
try {
if (m_currentLayer) {
UsdEditContext editCtx(m_stage, m_currentLayer);
return ApplyValue(attr, value);
} else {
return ApplyValue(attr, value);
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to set property: ") + e.what());
return false;
}
}
bool PropertyManager::SetPropertyValueInLayer(const std::string& primPath,
const std::string& propName,
const PropertyValue& value,
const SdfLayerHandle& layer) {
UsdPrim prim = GetPrim(primPath);
if (!prim.IsValid()) return false;
UsdAttribute attr = prim.GetAttribute(TfToken(propName));
if (!attr.IsValid()) return false;
try {
UsdEditContext editCtx(m_stage, layer);
return ApplyValue(attr, value);
} catch (const std::exception& e) {
LOG_ERROR(std::string("Failed to set property: ") + e.what());
return false;
}
}
std::string PropertyManager::GetPropertyLayerStack(const UsdAttribute& attr) {
if (!attr.IsValid()) return "";
SdfPropertySpecHandleVector propStack = attr.GetPropertyStack();
std::string result;
for (size_t i = 0; i < propStack.size(); i++) {
if (i > 0) result += " -> ";
SdfLayerHandle layer = propStack[i]->GetLayer();
if (layer) {
result += LayerManager::ExtractDisplayName(layer->GetIdentifier());
}
}
return result;
}
std::vector<std::string> PropertyManager::GetPrimPaths() {
std::vector<std::string> paths;
if (!m_stage) return paths;
paths.push_back(m_stage->GetPseudoRoot().GetPath().GetString());
for (const auto& prim : m_stage->Traverse()) {
paths.push_back(prim.GetPath().GetString());
}
return paths;
}
UsdPrim PropertyManager::GetPrim(const std::string& path) {
if (!m_stage || path.empty()) {
return UsdPrim();
}
SdfPath sdfPath(path);
return m_stage->GetPrimAtPath(sdfPath);
}
bool PropertyManager::ApplyValue(const UsdAttribute& attr, const PropertyValue& value) {
return std::visit([&](auto&& val) -> bool {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, bool>) {
return attr.Set(val);
} else if constexpr (std::is_same_v<T, int>) {
return attr.Set(val);
} else if constexpr (std::is_same_v<T, float>) {
return attr.Set(val);
} else if constexpr (std::is_same_v<T, double>) {
return attr.Set(val);
} else if constexpr (std::is_same_v<T, std::string>) {
return attr.Set(val);
} else if constexpr (std::is_same_v<T, GfVec3f>) {
return attr.Set(val);
} else if constexpr (std::is_same_v<T, GfVec3d>) {
return attr.Set(val);
} else {
return false;
}
}, value);
}
} // namespace UsdLayerManager
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/editContext.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
#include <vector>
#include <memory>
#include <variant>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
using PropertyValue = std::variant<
bool, int, float, double, std::string,
pxr::GfVec3f, pxr::GfVec3d
>;
struct PropertyInfo {
std::string name;
std::string displayName;
std::string typeName;
pxr::UsdAttribute attribute;
bool hasValue;
PropertyValue value;
std::string layerStack;
};
class PropertyManager {
public:
PropertyManager();
~PropertyManager();
void SetStage(UsdStageRefPtr stage);
void SetCurrentLayer(const SdfLayerHandle& layer);
SdfLayerHandle GetCurrentLayer() const { return m_currentLayer; }
// Prim properties
std::vector<PropertyInfo> GetPrimProperties(const std::string& primPath);
std::vector<PropertyInfo> GetPrimProperties(const UsdPrim& prim);
// Property editing
bool SetPropertyValue(const std::string& primPath, const std::string& propName,
const PropertyValue& value);
bool SetPropertyValueInLayer(const std::string& primPath, const std::string& propName,
const PropertyValue& value, const SdfLayerHandle& layer);
// Property info
std::string GetPropertyLayerStack(const UsdAttribute& attr);
// Prim hierarchy
std::vector<std::string> GetPrimPaths();
UsdPrim GetPrim(const std::string& path);
private:
void CollectProperties(const UsdPrim& prim, std::vector<PropertyInfo>& props);
PropertyValue ExtractValue(const UsdAttribute& attr);
bool ApplyValue(const UsdAttribute& attr, const PropertyValue& value);
UsdStageRefPtr m_stage;
SdfLayerHandle m_currentLayer;
};
} // namespace UsdLayerManager
File diff suppressed because it is too large Load Diff
+307
View File
@@ -0,0 +1,307 @@
#pragma once
#include <glad/gl.h>
#include <pxr/usd/usd/stage.h>
#include <pxr/imaging/glf/drawTarget.h>
#include <pxr/usdImaging/usdImagingGL/engine.h>
#include <pxr/imaging/glf/simpleLight.h>
#include <pxr/imaging/glf/simpleMaterial.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/base/gf/vec4f.h>
#include <pxr/base/gf/vec4d.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/matrix4f.h>
#include <pxr/base/gf/range3d.h>
#include <pxr/base/gf/camera.h>
#include <pxr/base/gf/bbox3d.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/usd/sdf/path.h>
#include <vector>
#include <string>
namespace UsdLayerManager {
/// Bounding box display mode for selected prims.
enum class BBoxMode {
None, ///< No bounding boxes drawn
PerObject, ///< One box per selected prim
AllSelection ///< One combined box for the entire selection
};
class UsdSceneRenderer {
public:
UsdSceneRenderer();
~UsdSceneRenderer();
void SetStage(pxr::UsdStageRefPtr stage);
/// Render the current stage into the internal draw target.
void Render(int width, int height);
/// Compute the world-space bounding box of the whole stage.
pxr::GfRange3d ComputeStageBounds();
// -----------------------------------------------------------------------
// Camera state
// -----------------------------------------------------------------------
/// Set free-camera view/proj matrices explicitly.
void SetCameraState(const pxr::GfMatrix4d& viewMatrix,
const pxr::GfMatrix4d& projMatrix);
/// Set free-camera state from a GfCamera (also extracts clip planes).
void SetCameraStateFromGfCamera(const pxr::GfCamera& gfCamera);
/// Set USD prim camera path; renderer resolves it internally.
void SetCameraPath(const pxr::SdfPath& cameraPath);
// -----------------------------------------------------------------------
// Selection
// -----------------------------------------------------------------------
void ClearSelected();
void AddSelected(const pxr::SdfPath& path, int instanceIndex = -1);
/// Replace the entire selection with a set of paths (for multi-select).
void SetSelectedPaths(const pxr::SdfPathVector& paths);
// -----------------------------------------------------------------------
// Picking
// -----------------------------------------------------------------------
/// Single-prim pick: returns true if a hit was found.
bool PickObject(
int mouseX, int mouseY,
int viewWidth, int viewHeight,
pxr::GfVec3d* outHitPoint,
pxr::SdfPath* outHitPrimPath);
/// Rect pick: finds all unique prims whose geometry overlaps the screen rect
/// defined by (x0,y0)-(x1,y1) in viewport pixel coordinates.
/// Returns true if at least one prim was hit.
bool PickObjectsInRect(
int x0, int y0, int x1, int y1,
int viewWidth, int viewHeight,
pxr::SdfPathVector* outHitPaths);
// -----------------------------------------------------------------------
// Overlay drawing (called while draw-target FBO is still bound)
// -----------------------------------------------------------------------
/// Draw XYZ axis gizmo matching stageView.DrawAxis().
void DrawAxis(const pxr::GfMatrix4d& viewProjMatrix, double cameraDist);
/// Draw bounding boxes for the given selected prim paths according to m_bboxMode.
void DrawBoundingBoxes(
const pxr::SdfPathVector& selectedPaths,
const pxr::GfMatrix4d& viewProjMatrix);
/// Draw frustum wireframes for all UsdGeomCamera prims in the stage.
/// selectedPaths — current primary selection (selected camera gets accent colour).
/// activeCameraPath — camera currently driving the viewport (gets cyan).
/// viewportCameraDist — used to scale wireframe size (from ViewportCamera::GetDist()).
void DrawCameraWireframes(
pxr::UsdStageRefPtr stage,
const pxr::SdfPathVector& selectedPaths,
const pxr::SdfPath& activeCameraPath,
const pxr::GfMatrix4d& viewProjMatrix,
double viewportCameraDist);
/// Test a screen-space mouse position against all camera wireframe segments.
/// Returns true and sets *outCameraPath if a camera wireframe is within
/// kCameraPickRadius (10 px) of mousePos. Prioritises the closest hit.
/// mouseX/Y and imagePosX/Y are absolute screen coordinates.
bool PickCameraAtPoint(
pxr::UsdStageRefPtr stage,
float mouseX, float mouseY,
const pxr::GfMatrix4d& viewProjMatrix,
float imagePosX, float imagePosY,
int viewW, int viewH,
double viewportCameraDist,
pxr::SdfPath* outCameraPath);
// -----------------------------------------------------------------------
// Draw-target FBO helpers (for external overlays, e.g. TransformManipulator)
// -----------------------------------------------------------------------
/// Bind the internal offscreen FBO and set the GL viewport.
/// Must be paired with UnbindDrawTarget() after drawing.
void BindDrawTarget();
/// Unbind the internal offscreen FBO.
void UnbindDrawTarget();
// -----------------------------------------------------------------------
// Render delegate
// -----------------------------------------------------------------------
/// Return all available renderer plugin IDs (e.g. HdStormRendererPlugin).
static std::vector<pxr::TfToken> GetRendererPlugins();
/// Return the ID of the currently active renderer plugin.
pxr::TfToken GetCurrentRendererId() const;
/// Return the human-readable display name for a given plugin ID.
static std::string GetRendererDisplayName(const pxr::TfToken& pluginId);
/// Switch to a different render delegate. Returns false if the plugin is
/// unknown or the switch fails. The renderer is re-initialised if needed.
bool SetRendererPlugin(const pxr::TfToken& pluginId);
// -----------------------------------------------------------------------
// View settings
// -----------------------------------------------------------------------
bool ShowGrid() const { return m_showGrid; }
void SetShowGrid(bool show) { m_showGrid = show; }
bool GetAAEnabled() const { return m_aaEnabled; }
void SetAAEnabled(bool enabled) { m_aaEnabled = enabled; }
const pxr::GfVec3f& GetBackgroundColor() const { return m_backgroundColor; }
void SetBackgroundColor(const pxr::GfVec3f& c) { m_backgroundColor = c; }
void SetForceRefresh(bool val) {
m_forceRefresh = m_forceRefresh || val;
if (val) m_cameraCacheDirty = true;
}
// -----------------------------------------------------------------------
// Bounding box display
// -----------------------------------------------------------------------
BBoxMode GetBBoxMode() const { return m_bboxMode; }
void SetBBoxMode(BBoxMode mode) { m_bboxMode = mode; }
const pxr::GfVec4f& GetBBoxColor() const { return m_bboxColor; }
void SetBBoxColor(const pxr::GfVec4f& c) { m_bboxColor = c; }
// -----------------------------------------------------------------------
// Lighting settings (mirrors stageView.py viewSettings)
// -----------------------------------------------------------------------
/// Camera headlight: single point light at the camera position (default ON).
bool GetAmbientLightOnly() const { return m_ambientLightOnly; }
void SetAmbientLightOnly(bool val) { m_ambientLightOnly = val; }
/// Dome environment light (default OFF).
bool GetDomeLightEnabled() const { return m_domeLightEnabled; }
void SetDomeLightEnabled(bool val) { m_domeLightEnabled = val; }
/// Default material ambient (kA, default 0.2 — matches viewSettingsDataModel.py).
float GetDefaultMaterialAmbient() const { return m_defaultMaterialAmbient; }
void SetDefaultMaterialAmbient(float v) { m_defaultMaterialAmbient = v; }
/// Default material specular (kS, default 0.1 — matches viewSettingsDataModel.py).
float GetDefaultMaterialSpecular() const { return m_defaultMaterialSpecular; }
void SetDefaultMaterialSpecular(float v) { m_defaultMaterialSpecular = v; }
// -----------------------------------------------------------------------
// Output
// -----------------------------------------------------------------------
uint32_t GetColorTextureID();
// For unit tests
pxr::GlfDrawTargetRefPtr GetDrawTargetForTest() const { return m_drawTarget; }
private:
void InitRenderer();
void InitGridResources();
void RebuildGridVBO(); ///< (Re)build grid line geometry after up-axis or size change.
void DestroyGridResources();
void RenderGrid(int width, int height);
void InitAxisResources();
void DestroyAxisResources();
void InitBBoxResources();
void DestroyBBoxResources();
void InitCamWireResources();
void DestroyCamWireResources();
/// Draw a single axis-aligned box from a GfRange3d.
void DrawBox(const pxr::GfRange3d& range, const pxr::GfMatrix4f& mvp);
/// Build camera wireframe line segments (GL_LINES vertex pairs) in world space
/// for a single camera given its resolved GfCamera and display scale.
/// outVerts is appended with interleaved XYZ floats (2 verts per segment).
void BuildCameraWireframeLines(const pxr::GfCamera& gfCam,
double scale,
std::vector<float>& outVerts);
/// Project a world-space point to absolute screen coordinates (x=imagePosX+pixelX, etc.).
/// Returns false when the point is behind the camera.
static bool WorldToScreen(const pxr::GfVec3d& world,
const pxr::GfMatrix4d& viewProj,
int viewW, int viewH,
float imagePosX, float imagePosY,
float& outX, float& outY);
/// 2-D point-to-segment distance (all values in screen pixels).
static float PointToSegmentDist(float px, float py,
float ax, float ay,
float bx, float by);
pxr::UsdStageRefPtr m_stage;
std::shared_ptr<pxr::UsdImagingGLEngine> m_renderer;
pxr::TfToken m_currentRendererPlugin; ///< Active plugin ID (empty = default)
pxr::GlfDrawTargetRefPtr m_drawTarget;
pxr::UsdImagingGLRenderParams m_renderParams;
pxr::GfMatrix4d m_viewMatrix;
pxr::GfMatrix4d m_projMatrix;
pxr::SdfPath m_cameraPath;
bool m_useCameraPath;
// Camera frustum used for picking (set when camera state is provided via GfCamera)
pxr::GfFrustum m_cameraFrustum;
bool m_hasCameraFrustum = false;
// Clip planes extracted from GfCamera (passed to renderParams)
std::vector<pxr::GfVec4d> m_clipPlanes;
bool m_showGrid;
bool m_aaEnabled; ///< GL_LINE_SMOOTH anti-aliasing for all line overlays
pxr::GfVec3f m_backgroundColor;
// Lighting settings (mirrors stageView.py viewSettings)
bool m_ambientLightOnly; // camera headlight
bool m_domeLightEnabled; // dome/IBL light
bool m_stageIsZup; // used for dome light rotation
float m_defaultMaterialAmbient; // kA
float m_defaultMaterialSpecular;// kS
bool m_rendererInitialized;
bool m_forceRefresh;
int m_diagFrameCount;
// --- Grid GL resources (line geometry, reuses m_axisProgram / m_axisUniformMVP) ---
GLuint m_gridVAO;
GLuint m_gridVBO;
// Draw-call ranges inside the VBO (vertex index, count pairs for GL_LINES)
GLint m_gridMinorFirst, m_gridMinorCount; ///< 1-unit minor lines
GLint m_gridMajorFirst, m_gridMajorCount; ///< 10-unit major lines
GLint m_gridAxisAFirst, m_gridAxisACount; ///< X-axis (red, at b=0)
GLint m_gridAxisBFirst, m_gridAxisBCount; ///< Z/Y-axis(blue/green, at a=0)
float m_gridHalfSize; ///< Half-extent (default 50 → 100×100 grid)
// --- Axis GLSL resources (stageView.DrawAxis port) ---
GLuint m_axisVAO;
GLuint m_axisVBO;
GLuint m_axisProgram;
GLint m_axisUniformMVP;
GLint m_axisUniformColor;
// --- Bounding box GLSL resources ---
BBoxMode m_bboxMode;
pxr::GfVec4f m_bboxColor; // default: white
GLuint m_bboxVAO;
GLuint m_bboxVBO; // dynamic: updated per box
GLuint m_bboxProgram;
GLint m_bboxUniformMVP;
GLint m_bboxUniformColor;
// --- Camera wireframe GL resources (dedicated, separate from bbox) ---
GLuint m_camWireVAO = 0;
GLuint m_camWireVBO = 0; ///< dynamic VBO; reallocated per frame as needed
// --- Camera wireframe cache ---
pxr::SdfPathVector m_cachedCameraPaths;
bool m_cameraCacheDirty = true;
};
} // namespace UsdLayerManager
+248
View File
@@ -0,0 +1,248 @@
#include "UsdStageManager.h"
#include "../utils/Logger.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/usd/sdf/primSpec.h>
#include <pxr/usd/sdf/copyUtils.h>
PXR_NAMESPACE_USING_DIRECTIVE
namespace UsdLayerManager {
/// Merge all root-prim specs from the session layer into the root layer so
/// they are preserved when the stage is saved. The session layer is cleared
/// afterwards so that the opinions are not double-applied when the file is
/// reopened. No-op if the session layer is empty.
static void MergeSessionLayerIntoRoot(UsdStageRefPtr stage) {
if (!stage) return;
SdfLayerHandle sessionLayer = stage->GetSessionLayer();
SdfLayerHandle rootLayer = stage->GetRootLayer();
if (!sessionLayer || !rootLayer) return;
if (sessionLayer->IsEmpty()) return;
LOG_INFO("Merging session layer into root layer before save");
// Iterate the immediate children of the pseudo-root in the session layer
// (these are all root-level prims that have opinions there).
SdfPrimSpecHandle pseudoRoot = sessionLayer->GetPseudoRoot();
if (!pseudoRoot) return;
for (const SdfPrimSpecHandle& primSpec : pseudoRoot->GetNameChildren()) {
if (!primSpec) continue;
const SdfPath& primPath = primSpec->GetPath();
if (!SdfCopySpec(sessionLayer, primPath, rootLayer, primPath)) {
LOG_ERROR("MergeSessionLayer: SdfCopySpec failed for " + primPath.GetString());
}
}
sessionLayer->Clear();
LOG_INFO("Session layer merged and cleared");
}
UsdStageManager::UsdStageManager()
: m_stage(nullptr) {
}
UsdStageManager::~UsdStageManager() {
CloseStage();
}
bool UsdStageManager::OpenStage(const std::string& filePath) {
LOG_INFO("Opening USD stage: " + filePath);
try {
// Close existing stage if any
CloseStage();
// Open the stage
m_stage = UsdStage::Open(filePath);
if (!m_stage) {
SetError("Failed to open USD stage: " + filePath);
return false;
}
LOG_INFO("Successfully opened USD stage: " + filePath);
return true;
} catch (const std::exception& e) {
SetError(std::string("Exception while opening stage: ") + e.what());
return false;
}
}
bool UsdStageManager::CreateNewStage(const std::string& filePath) {
LOG_INFO("Creating new USD stage: " + filePath);
try {
// Close existing stage if any
CloseStage();
// Create new stage
m_stage = UsdStage::CreateNew(filePath);
if (!m_stage) {
SetError("Failed to create new USD stage: " + filePath);
return false;
}
LOG_INFO("Successfully created new USD stage: " + filePath);
return true;
} catch (const std::exception& e) {
SetError(std::string("Exception while creating stage: ") + e.what());
return false;
}
}
bool UsdStageManager::CreateInMemoryStage() {
LOG_INFO("Creating in-memory USD stage");
try {
// Close existing stage if any
CloseStage();
// Create in-memory stage
m_stage = UsdStage::CreateInMemory();
if (!m_stage) {
SetError("Failed to create in-memory USD stage");
return false;
}
LOG_INFO("Successfully created in-memory USD stage");
return true;
} catch (const std::exception& e) {
SetError(std::string("Exception while creating in-memory stage: ") + e.what());
return false;
}
}
void UsdStageManager::CloseStage() {
if (m_stage) {
LOG_INFO("Closing USD stage");
m_stage.Reset();
m_stage = nullptr;
}
}
bool UsdStageManager::SaveStage() {
if (!m_stage) {
SetError("No stage to save");
return false;
}
try {
LOG_INFO("Saving USD stage");
// Persist any session-layer opinions into the root layer so they are
// not lost when the file is saved (the session layer is anonymous and
// cannot be saved by UsdStage::Save() itself).
MergeSessionLayerIntoRoot(m_stage);
m_stage->Save();
LOG_INFO("Successfully saved USD stage");
return true;
} catch (const std::exception& e) {
SetError(std::string("Exception while saving stage: ") + e.what());
return false;
}
}
bool UsdStageManager::SaveStageAs(const std::string& filePath) {
if (!m_stage) {
SetError("No stage to save");
return false;
}
try {
LOG_INFO("Saving USD stage as: " + filePath);
// Merge session-layer opinions into the root layer so they survive
// the copy. The session layer is anonymous and cannot be exported
// directly; its content must live in the root layer to be persisted.
MergeSessionLayerIntoRoot(m_stage);
// Export the root layer to the new path. Unlike UsdStage::Export()
// (which flattens all sublayers into one file), SdfLayer::Export()
// writes only the root layer's own opinions while preserving its
// subLayerPaths references. This keeps the sublayer structure intact.
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) {
SetError("No root layer to export");
return false;
}
if (!rootLayer->Export(filePath)) {
SetError("Failed to export root layer to: " + filePath);
return false;
}
// Reopen the stage from the newly written file so the application
// reflects the saved path from this point on.
m_stage = UsdStage::Open(filePath);
if (!m_stage) {
SetError("Failed to reopen stage after save as: " + filePath);
return false;
}
LOG_INFO("Successfully saved USD stage as: " + filePath);
return true;
} catch (const std::exception& e) {
SetError(std::string("Exception while saving stage as: ") + e.what());
return false;
}
}
std::string UsdStageManager::GetRootLayerPath() const {
if (!m_stage) {
return "";
}
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) {
return "";
}
return rootLayer->GetRealPath();
}
std::string UsdStageManager::GetRootLayerIdentifier() const {
if (!m_stage) {
return "";
}
SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) {
return "";
}
return rootLayer->GetIdentifier();
}
SdfLayerHandle UsdStageManager::GetRootLayer() const {
if (!m_stage) {
return nullptr;
}
return m_stage->GetRootLayer();
}
SdfLayerHandle UsdStageManager::GetSessionLayer() const {
if (!m_stage) {
return nullptr;
}
return m_stage->GetSessionLayer();
}
void UsdStageManager::SetError(const std::string& error) {
m_lastError = error;
LOG_ERROR(error);
}
} // namespace UsdLayerManager
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
#include <memory>
namespace UsdLayerManager {
class UsdStageManager {
public:
UsdStageManager();
~UsdStageManager();
// Stage lifecycle management
bool OpenStage(const std::string& filePath);
bool CreateNewStage(const std::string& filePath);
bool CreateInMemoryStage();
void CloseStage();
// Stage operations
bool SaveStage();
bool SaveStageAs(const std::string& filePath);
// Stage access
pxr::UsdStageRefPtr GetStage() const { return m_stage; }
bool HasStage() const { return m_stage != nullptr; }
// Stage information
std::string GetRootLayerPath() const;
std::string GetRootLayerIdentifier() const;
pxr::SdfLayerHandle GetRootLayer() const;
pxr::SdfLayerHandle GetSessionLayer() const;
// Error handling
std::string GetLastError() const { return m_lastError; }
private:
void SetError(const std::string& error);
pxr::UsdStageRefPtr m_stage;
std::string m_lastError;
};
} // namespace UsdLayerManager
+384
View File
@@ -0,0 +1,384 @@
#include "ViewportCamera.h"
#include <pxr/usd/usdGeom/metrics.h>
#include <pxr/usd/usdGeom/tokens.h>
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/imaging/cameraUtil/conformWindow.h>
#include <cmath>
#include <algorithm>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// Helper: rotation matrix around `axis` by `angleDeg` degrees
// ---------------------------------------------------------------------------
static pxr::GfMatrix4d RotMatrix(const pxr::GfVec3d& axis, double angleDeg)
{
return pxr::GfMatrix4d(1.0).SetRotate(pxr::GfRotation(axis, angleDeg));
}
// ===========================================================================
// Construction
// ===========================================================================
ViewportCamera::ViewportCamera()
: m_cameraTransformDirty(true)
, m_rotTheta(0.0)
, m_rotPhi(0.0)
, m_rotPsi(0.0)
, m_center(0.0, 0.0, 0.0)
, m_dist(100.0)
, m_selSize(10.0)
, m_isZUp(false)
, m_YZUpMatrix(1.0)
, m_YZUpInvMatrix(1.0)
, m_hasClosestVisibleDist(false)
, m_closestVisibleDist(0.0)
, m_lastFramedDist(100.0)
, m_lastFramedClosestDist(0.0)
, m_overrideNear(-1.0)
, m_overrideFar(-1.0)
, m_mode(CameraMode::Free)
{
// Default: perspective camera, vertical FOV = 60°, square aspect ratio.
m_camera.SetPerspectiveFromAspectRatioAndFieldOfView(
1.0f, 60.0f, pxr::GfCamera::FOVVertical);
m_camera.SetFocusDistance(static_cast<float>(m_dist));
ResetClippingPlanes();
}
// ===========================================================================
// Stage
// ===========================================================================
void ViewportCamera::SetStage(pxr::UsdStageRefPtr stage)
{
m_stage = stage;
m_isZUp = stage &&
(pxr::UsdGeomGetStageUpAxis(stage) == pxr::UsdGeomTokens->z);
if (m_isZUp) {
// GfCamera.Y_UP_TO_Z_UP_MATRIX: rotate -90° around X axis
m_YZUpMatrix = pxr::GfMatrix4d(1.0).SetRotate(
pxr::GfRotation(pxr::GfVec3d::XAxis(), -90.0));
m_YZUpInvMatrix = m_YZUpMatrix.GetInverse();
} else {
m_YZUpMatrix = pxr::GfMatrix4d(1.0);
m_YZUpInvMatrix = pxr::GfMatrix4d(1.0);
}
m_cameraTransformDirty = true;
}
// ===========================================================================
// Private: rebuild camera transform from orbital parameters
// Mirrors FreeCamera._pushToCameraTransform()
// ===========================================================================
void ViewportCamera::PushToCameraTransform()
{
if (!m_cameraTransformDirty) return;
// camera-to-world transform (same as FreeCamera._pushToCameraTransform):
// T(dist*Z) * R(-psi,Z) * R(-phi,X) * R(-theta,Y) * YZUpInv * T(center)
pxr::GfMatrix4d xform =
pxr::GfMatrix4d(1.0).SetTranslate(pxr::GfVec3d::ZAxis() * m_dist)
* RotMatrix(pxr::GfVec3d::ZAxis(), -m_rotPsi)
* RotMatrix(pxr::GfVec3d::XAxis(), -m_rotPhi)
* RotMatrix(pxr::GfVec3d::YAxis(), -m_rotTheta)
* m_YZUpInvMatrix
* pxr::GfMatrix4d(1.0).SetTranslate(m_center);
m_camera.SetTransform(xform);
m_camera.SetFocusDistance(static_cast<float>(m_dist));
m_cameraTransformDirty = false;
}
// Mirrors FreeCamera._pullFromCameraTransform()
void ViewportCamera::PullFromCameraTransform()
{
pxr::GfFrustum frustum = m_camera.GetFrustum();
m_dist = static_cast<double>(m_camera.GetFocusDistance());
m_selSize = m_dist / 10.0;
m_center = frustum.GetPosition() + m_dist * frustum.ComputeViewDirection();
pxr::GfMatrix4d camTransform = m_camera.GetTransform() * m_YZUpMatrix;
camTransform.Orthonormalize();
pxr::GfRotation rotation = camTransform.ExtractRotation();
// Decompose: Y → theta, X → phi, Z → psi
pxr::GfVec3d angles = rotation.Decompose(
pxr::GfVec3d::YAxis(),
pxr::GfVec3d::XAxis(),
pxr::GfVec3d::ZAxis());
m_rotTheta = -angles[0];
m_rotPhi = -angles[1];
m_rotPsi = -angles[2];
m_cameraTransformDirty = true;
}
// ===========================================================================
// Free Camera Operations
// ===========================================================================
void ViewportCamera::Tumble(double dTheta, double dPhi)
{
m_rotTheta += dTheta;
m_rotPhi += dPhi;
m_cameraTransformDirty = true;
}
void ViewportCamera::AdjustDistance(double scaleFactor)
{
// Mirrors FreeCamera.AdjustDistance(): prevents getting stuck near dist≈0.
if (scaleFactor > 1.0 && m_dist < 2.0) {
double selBasedIncr = m_selSize / 25.0;
scaleFactor -= 1.0;
m_dist += std::min(selBasedIncr, scaleFactor);
} else {
m_dist *= scaleFactor;
}
m_dist = std::max(m_dist, 0.001); // never let dist reach zero
// Keep closest-visible-distance estimate in sync with new dist
if (m_hasClosestVisibleDist) {
if (m_dist > m_lastFramedDist) {
m_closestVisibleDist = m_lastFramedClosestDist;
} else {
m_closestVisibleDist = m_lastFramedClosestDist
- m_lastFramedDist
+ m_dist;
}
}
m_cameraTransformDirty = true;
}
void ViewportCamera::Truck(double deltaRight, double deltaUp)
{
PushToCameraTransform();
pxr::GfFrustum frustum = m_camera.GetFrustum();
pxr::GfVec3d camUp = frustum.ComputeUpVector();
pxr::GfVec3d camRight = pxr::GfCross(frustum.ComputeViewDirection(), camUp);
m_center += deltaRight * camRight + deltaUp * camUp;
m_cameraTransformDirty = true;
}
double ViewportCamera::ComputePixelsToWorldFactor(double viewportHeight)
{
PushToCameraTransform();
pxr::GfFrustum frustum = m_camera.GetFrustum();
double frustumHeight = frustum.GetWindow().GetSize()[1];
return frustumHeight * m_dist / std::max(viewportHeight, 1.0);
}
void ViewportCamera::FrameSelection(const pxr::GfBBox3d& selBBox, double frameFit)
{
m_hasClosestVisibleDist = false;
m_center = selBBox.ComputeCentroid();
pxr::GfRange3d selRange = selBBox.ComputeAlignedRange();
pxr::GfVec3d sz = selRange.GetSize();
m_selSize = std::max({ sz[0], sz[1], sz[2] });
// Distance calculation from FreeCamera.frameSelection()
double fovRad = GetFOV() * M_PI / 180.0;
double halfFovRad = std::max(fovRad * 0.5, 0.00872665); // at least ~0.5°
double lengthToFit = m_selSize * frameFit * 0.5;
m_dist = lengthToFit / std::tan(halfFovRad);
// Prevent camera from intersecting the bounding box
if (m_dist < kDefaultNear + m_selSize * 0.5) {
m_dist = kDefaultNear + lengthToFit;
}
m_cameraTransformDirty = true;
}
// ===========================================================================
// Clipping Planes
// ===========================================================================
std::pair<double,double> ViewportCamera::RangeOfBoxAlongRay(
const pxr::GfRay& camRay, const pxr::GfBBox3d& bbox) const
{
double maxDist = -1e38;
double minDist = 1e38;
const pxr::GfRange3d& boxRange = bbox.GetRange();
const pxr::GfMatrix4d& boxXform = bbox.GetMatrix();
for (int i = 0; i < 8; ++i) {
pxr::GfVec3d corner = boxXform.Transform(boxRange.GetCorner(i));
double t = 0.0;
camRay.FindClosestPoint(corner, &t);
maxDist = std::max(maxDist, t);
minDist = std::min(minDist, t);
}
minDist = (minDist < kDefaultNear) ? kDefaultNear : minDist * 0.99;
maxDist *= 1.01;
return { minDist, maxDist };
}
void ViewportCamera::SetClippingPlanes(const pxr::GfBBox3d& stageBBox)
{
double computedNear, computedFar;
if (stageBBox.GetRange().IsEmpty()) {
computedNear = kDefaultNear;
computedFar = kDefaultFar;
} else {
pxr::GfFrustum frustum = m_camera.GetFrustum();
pxr::GfVec3d camPos = frustum.GetPosition();
pxr::GfRay camRay(camPos, frustum.ComputeViewDirection());
auto boxRange = RangeOfBoxAlongRay(camRay, stageBBox);
computedNear = boxRange.first;
computedFar = boxRange.second;
double precisionNear = computedFar / kMaxGoodZResolution;
if (m_hasClosestVisibleDist) {
double halfClose = m_closestVisibleDist / 2.0;
if (m_closestVisibleDist < m_lastFramedClosestDist) {
halfClose = std::max({ precisionNear, halfClose, computedNear });
}
if (halfClose < computedNear) {
computedNear = halfClose;
} else if (precisionNear > computedNear) {
computedNear = std::min((precisionNear + halfClose) / 2.0, halfClose);
}
}
}
double nearVal = (m_overrideNear > 0.0) ? m_overrideNear : computedNear;
double farVal = (m_overrideFar > 0.0) ? m_overrideFar : computedFar;
farVal = std::max(nearVal + 1.0, farVal);
m_camera.SetClippingRange(pxr::GfRange1f(
static_cast<float>(nearVal), static_cast<float>(farVal)));
}
void ViewportCamera::ResetClippingPlanes()
{
double nearVal = (m_overrideNear > 0.0) ? m_overrideNear : kDefaultNear;
double farVal = (m_overrideFar > 0.0) ? m_overrideFar : kDefaultFar;
m_camera.SetClippingRange(pxr::GfRange1f(
static_cast<float>(nearVal), static_cast<float>(farVal)));
}
// ===========================================================================
// Camera Resolution
// ===========================================================================
pxr::GfCamera ViewportCamera::ComputeGfCamera(
const pxr::GfBBox3d& stageBBox, bool autoClip)
{
PushToCameraTransform();
if (autoClip) {
SetClippingPlanes(stageBBox);
} else {
ResetClippingPlanes();
}
return m_camera;
}
void ViewportCamera::SetClosestVisibleDistFromPoint(const pxr::GfVec3d& point)
{
PushToCameraTransform();
pxr::GfFrustum frustum = m_camera.GetFrustum();
pxr::GfVec3d camPos = frustum.GetPosition();
pxr::GfRay camRay(camPos, frustum.ComputeViewDirection());
double t = 0.0;
camRay.FindClosestPoint(point, &t);
m_closestVisibleDist = t;
m_hasClosestVisibleDist = true;
m_lastFramedDist = m_dist;
m_lastFramedClosestDist = m_closestVisibleDist;
}
// ===========================================================================
// Matrix Accessors
// ===========================================================================
pxr::GfMatrix4d ViewportCamera::GetViewMatrix()
{
PushToCameraTransform();
return m_camera.GetFrustum().ComputeViewMatrix();
}
pxr::GfMatrix4d ViewportCamera::GetProjectionMatrix()
{
PushToCameraTransform();
return m_camera.GetFrustum().ComputeProjectionMatrix();
}
// ===========================================================================
// Compatibility Accessors
// ===========================================================================
pxr::GfVec3d ViewportCamera::GetEye()
{
PushToCameraTransform();
return m_camera.GetFrustum().GetPosition();
}
// ===========================================================================
// Camera Mode
// ===========================================================================
void ViewportCamera::SetUsdCamera(const pxr::SdfPath& cameraPath)
{
m_mode = CameraMode::UsdCamera;
m_usdCameraPath = cameraPath;
}
void ViewportCamera::SwitchToFreeCamera(const pxr::GfCamera* lastGfCamera)
{
if (m_mode == CameraMode::Free) return;
if (lastGfCamera) {
// Initialize free-camera state from the last rendered GfCamera
// (mirrors FreeCamera.FromGfCamera)
m_camera = *lastGfCamera;
PullFromCameraTransform();
}
m_mode = CameraMode::Free;
m_usdCameraPath = pxr::SdfPath();
m_cameraTransformDirty = true;
}
// ===========================================================================
// Camera Settings
// ===========================================================================
double ViewportCamera::GetFOV() const
{
return static_cast<double>(
m_camera.GetFieldOfView(pxr::GfCamera::FOVVertical));
}
void ViewportCamera::SetFOV(double fov)
{
m_camera.SetPerspectiveFromAspectRatioAndFieldOfView(
m_camera.GetAspectRatio(),
static_cast<float>(fov),
pxr::GfCamera::FOVVertical);
}
double ViewportCamera::GetAspectRatio() const
{
return static_cast<double>(m_camera.GetAspectRatio());
}
void ViewportCamera::SetAspectRatio(double aspect)
{
m_camera.SetPerspectiveFromAspectRatioAndFieldOfView(
static_cast<float>(aspect),
m_camera.GetFieldOfView(pxr::GfCamera::FOVVertical),
pxr::GfCamera::FOVVertical);
}
} // namespace UsdLayerManager
+175
View File
@@ -0,0 +1,175 @@
#pragma once
#include <pxr/base/gf/camera.h>
#include <pxr/base/gf/matrix4d.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/bbox3d.h>
#include <pxr/base/gf/range3d.h>
#include <pxr/base/gf/ray.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/usd/stage.h>
#include <utility>
namespace UsdLayerManager {
/// Free camera ported from Pixar's usdview FreeCamera (freeCamera.py).
/// Supports Tumble/Truck/AdjustDistance with Z-up stage handling,
/// automatic near/far clipping plane computation from scene bbox,
/// and USD prim-camera passthrough mode.
class ViewportCamera {
public:
enum class CameraMode { Free, UsdCamera };
static constexpr double kDefaultNear = 1.0;
static constexpr double kDefaultFar = 2000000.0;
static constexpr double kMaxSafeZResolution = 1e6;
static constexpr double kMaxGoodZResolution = 5e4;
ViewportCamera();
// -----------------------------------------------------------------------
// Stage (sets Z-up flag and YZUp matrices)
// -----------------------------------------------------------------------
void SetStage(pxr::UsdStageRefPtr stage);
// -----------------------------------------------------------------------
// Free Camera Operations (mirrors FreeCamera.py)
// -----------------------------------------------------------------------
/// Orbit (tumble) around center by dTheta (horizontal) and dPhi (vertical) degrees.
void Tumble(double dTheta, double dPhi);
/// Scale distance from center by scaleFactor. Prevents getting stuck near zero.
void AdjustDistance(double scaleFactor);
/// Pan (truck) in camera-local right/up directions, world-unit deltas.
void Truck(double deltaRight, double deltaUp);
/// Returns pixels-to-world factor for correct Truck() scaling.
double ComputePixelsToWorldFactor(double viewportHeight);
/// Frame a bounding box. frameFit=1.1 gives ~10% margin (usdview default).
void FrameSelection(const pxr::GfBBox3d& selBBox, double frameFit = 1.1);
// -----------------------------------------------------------------------
// Camera Resolution: returns GfCamera with updated transform + clipping
// -----------------------------------------------------------------------
/// Returns the GfCamera with up-to-date transform.
/// If autoClip=true, near/far are computed from stageBBox for best precision.
pxr::GfCamera ComputeGfCamera(const pxr::GfBBox3d& stageBBox,
bool autoClip = false);
// -----------------------------------------------------------------------
// Convenience Matrix Accessors
// -----------------------------------------------------------------------
pxr::GfMatrix4d GetViewMatrix();
pxr::GfMatrix4d GetProjectionMatrix();
// -----------------------------------------------------------------------
// Camera Mode
// -----------------------------------------------------------------------
CameraMode GetMode() const { return m_mode; }
const pxr::SdfPath& GetUsdCameraPath() const { return m_usdCameraPath; }
/// Switch to USD prim camera mode (renderer will call SetCameraPath).
void SetUsdCamera(const pxr::SdfPath& cameraPath);
/// Switch back to free camera, optionally initializing state from lastGfCamera.
void SwitchToFreeCamera(const pxr::GfCamera* lastGfCamera = nullptr);
// -----------------------------------------------------------------------
// Camera Settings
// -----------------------------------------------------------------------
double GetFOV() const; ///< Vertical FOV in degrees
void SetFOV(double fov);
double GetAspectRatio() const;
void SetAspectRatio(double aspect);
/// Hint for autoClip: the closest visible geometry point from pick result.
void SetClosestVisibleDistFromPoint(const pxr::GfVec3d& point);
// -----------------------------------------------------------------------
// State Queries
// -----------------------------------------------------------------------
double GetDist() const { return m_dist; }
bool IsZUp() const { return m_isZUp; }
// -----------------------------------------------------------------------
// Compatibility accessors (for tests and legacy callers)
// -----------------------------------------------------------------------
/// Returns camera position in world space.
pxr::GfVec3d GetEye();
/// Returns the look-at focal point (center of orbit).
const pxr::GfVec3d& GetFocalPoint() const { return m_center; }
/// Set the look-at focal point directly.
/// Used by orthographic view panning to move the center in correct
/// view-space directions without going through the perspective Truck() path.
void SetFocalPoint(const pxr::GfVec3d& center) {
m_center = center;
m_cameraTransformDirty = true;
}
/// Returns the current near-clip distance.
double GetNearClip() const {
return static_cast<double>(m_camera.GetClippingRange().GetMin());
}
/// Returns the current far-clip distance.
double GetFarClip() const {
return static_cast<double>(m_camera.GetClippingRange().GetMax());
}
/// Legacy: frame a GfRange3d (wraps it in a unit-matrix GfBBox3d).
void FrameBoundingBox(const pxr::GfRange3d& range, double frameFit = 1.1) {
FrameSelection(pxr::GfBBox3d(range), frameFit);
}
/// Legacy: orbit alias (matches old Orbit(deltaYaw, deltaPitch) signature).
void Orbit(double deltaYaw, double deltaPitch) {
Tumble(deltaYaw, deltaPitch);
}
private:
void PushToCameraTransform();
void PullFromCameraTransform();
void SetClippingPlanes(const pxr::GfBBox3d& stageBBox);
void ResetClippingPlanes();
std::pair<double,double> RangeOfBoxAlongRay(
const pxr::GfRay& camRay, const pxr::GfBBox3d& bbox) const;
// Core camera object (owns aperture/projection/clipping/transform state)
pxr::GfCamera m_camera;
bool m_cameraTransformDirty;
// Orbital / tumble state
double m_rotTheta; // horizontal orbit (degrees, around Y)
double m_rotPhi; // vertical orbit (degrees, around X)
double m_rotPsi; // roll (degrees, usually 0)
pxr::GfVec3d m_center; // look-at center in world space
double m_dist; // distance camera → center
double m_selSize; // extent of last framed selection
// Stage up-axis handling
bool m_isZUp;
pxr::GfMatrix4d m_YZUpMatrix; // Y-up → Z-up (rotate -90° around X)
pxr::GfMatrix4d m_YZUpInvMatrix; // Z-up → Y-up (inverse)
// Auto-clip state
bool m_hasClosestVisibleDist;
double m_closestVisibleDist;
double m_lastFramedDist;
double m_lastFramedClosestDist;
double m_overrideNear; // ≤0 means "no override"
double m_overrideFar; // ≤0 means "no override"
// Camera mode
CameraMode m_mode;
pxr::SdfPath m_usdCameraPath;
pxr::UsdStageRefPtr m_stage;
};
} // namespace UsdLayerManager
+43
View File
@@ -0,0 +1,43 @@
#include "AddReferenceCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/references.h>
#include <pxr/base/tf/token.h>
namespace UsdLayerManager {
AddReferenceCommand::AddReferenceCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& xformPath,
const std::string& refFilePath)
: m_stage(stage)
, m_xformPath(xformPath)
, m_refFilePath(refFilePath)
, m_description("Add Reference " + refFilePath)
{
}
void AddReferenceCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdPrim xformPrim = m_stage->DefinePrim(m_xformPath, pxr::TfToken("Xform"));
if (!xformPrim.IsValid()) {
LOG_ERROR("AddReferenceCommand: failed to define Xform at " + m_xformPath.GetString());
return;
}
if (!xformPrim.GetReferences().AddReference(m_refFilePath))
LOG_ERROR("AddReferenceCommand: failed to add reference " + m_refFilePath);
} catch (const std::exception& e) {
LOG_ERROR(std::string("AddReferenceCommand::Execute error: ") + e.what());
}
}
void AddReferenceCommand::Undo() {
if (!m_stage) return;
try {
if (!m_stage->RemovePrim(m_xformPath))
LOG_ERROR("AddReferenceCommand::Undo: failed to remove prim " + m_xformPath.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("AddReferenceCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/reference.h>
#include <string>
namespace UsdLayerManager {
/// Wraps adding an Xform prim + reference (stage-level "Add Reference..." action).
class AddReferenceCommand : public ICommand {
public:
AddReferenceCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& xformPath,
const std::string& refFilePath);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_xformPath;
std::string m_refFilePath;
std::string m_description;
};
} // namespace UsdLayerManager
+14
View File
@@ -0,0 +1,14 @@
#include "AttributeSetCommand.h"
namespace UsdLayerManager {
AttributeSetCommand::AttributeSetCommand(std::string description,
std::function<void()> executeFunc,
std::function<void()> undoFunc)
: m_description(std::move(description))
, m_executeFunc(std::move(executeFunc))
, m_undoFunc(std::move(undoFunc))
{
}
} // namespace UsdLayerManager
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "../CommandHistory.h"
#include <functional>
#include <string>
namespace UsdLayerManager {
/// Type-agnostic attribute set command.
/// Stores execute and undo as std::function closures capturing the typed values.
class AttributeSetCommand : public ICommand {
public:
AttributeSetCommand(std::string description,
std::function<void()> executeFunc,
std::function<void()> undoFunc);
void Execute() override { if (m_executeFunc) m_executeFunc(); }
void Undo() override { if (m_undoFunc) m_undoFunc(); }
std::string GetDescription() const override { return m_description; }
private:
std::string m_description;
std::function<void()> m_executeFunc;
std::function<void()> m_undoFunc;
};
} // namespace UsdLayerManager
+38
View File
@@ -0,0 +1,38 @@
#include "CreatePrimCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/prim.h>
namespace UsdLayerManager {
CreatePrimCommand::CreatePrimCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
const pxr::TfToken& typeName)
: m_stage(stage)
, m_primPath(primPath)
, m_typeName(typeName)
, m_description("Create " + typeName.GetString() + " " + primPath.GetString())
{
}
void CreatePrimCommand::Execute() {
if (!m_stage) return;
try {
pxr::UsdPrim prim = m_stage->DefinePrim(m_primPath, m_typeName);
if (!prim.IsValid())
LOG_ERROR("CreatePrimCommand: failed to define prim " + m_primPath.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("CreatePrimCommand::Execute error: ") + e.what());
}
}
void CreatePrimCommand::Undo() {
if (!m_stage) return;
try {
if (!m_stage->RemovePrim(m_primPath))
LOG_ERROR("CreatePrimCommand: failed to remove prim " + m_primPath.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("CreatePrimCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/base/tf/token.h>
#include <string>
namespace UsdLayerManager {
class CreatePrimCommand : public ICommand {
public:
CreatePrimCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
const pxr::TfToken& typeName);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_primPath;
pxr::TfToken m_typeName;
std::string m_description;
};
} // namespace UsdLayerManager
+69
View File
@@ -0,0 +1,69 @@
#include "DeletePrimCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/sdf/copyUtils.h>
#include <pxr/usd/sdf/primSpec.h>
namespace UsdLayerManager {
DeletePrimCommand::DeletePrimCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath)
: m_stage(stage)
, m_primPath(primPath)
, m_description("Delete " + primPath.GetString())
{
if (!stage) return;
pxr::SdfLayerHandle rootLayer = stage->GetRootLayer();
if (!rootLayer) return;
// Only capture if the spec exists on the root layer.
if (!rootLayer->HasSpec(primPath)) {
LOG_ERROR("DeletePrimCommand: no spec found for " + primPath.GetString());
return;
}
m_savedLayer = pxr::SdfLayer::CreateAnonymous(".usda");
if (!m_savedLayer) return;
// Ensure the parent path hierarchy exists in the saved layer.
pxr::SdfPath parentPath = primPath.GetParentPath();
while (!parentPath.IsAbsoluteRootPath() && !m_savedLayer->HasSpec(parentPath)) {
pxr::SdfPrimSpec::New(m_savedLayer,
parentPath.GetName(),
pxr::SdfSpecifierOver);
parentPath = parentPath.GetParentPath();
}
if (pxr::SdfCopySpec(rootLayer, primPath, m_savedLayer, primPath)) {
m_specWasSaved = true;
} else {
LOG_ERROR("DeletePrimCommand: SdfCopySpec failed for " + primPath.GetString());
}
}
void DeletePrimCommand::Execute() {
if (!m_stage) return;
try {
if (!m_stage->RemovePrim(m_primPath))
LOG_ERROR("DeletePrimCommand: RemovePrim failed for " + m_primPath.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("DeletePrimCommand::Execute error: ") + e.what());
}
}
void DeletePrimCommand::Undo() {
if (!m_stage || !m_specWasSaved || !m_savedLayer) {
LOG_ERROR("DeletePrimCommand::Undo: cannot restore — spec was not saved");
return;
}
pxr::SdfLayerHandle rootLayer = m_stage->GetRootLayer();
if (!rootLayer) return;
try {
if (!pxr::SdfCopySpec(m_savedLayer, m_primPath, rootLayer, m_primPath))
LOG_ERROR("DeletePrimCommand::Undo: SdfCopySpec failed for " + m_primPath.GetString());
} catch (const std::exception& e) {
LOG_ERROR(std::string("DeletePrimCommand::Undo error: ") + e.what());
}
}
} // namespace UsdLayerManager
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <string>
namespace UsdLayerManager {
/// Saves the prim spec via SdfCopySpec into an anonymous in-memory layer
/// before deletion; restores it on Undo.
class DeletePrimCommand : public ICommand {
public:
/// Captures the current spec of @p primPath from the root layer into an
/// anonymous layer. Must be constructed BEFORE the prim is removed.
DeletePrimCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& primPath);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_primPath;
pxr::SdfLayerRefPtr m_savedLayer; ///< anonymous layer holding the spec snapshot
std::string m_description;
bool m_specWasSaved = false;
};
} // namespace UsdLayerManager
+104
View File
@@ -0,0 +1,104 @@
#include "LayerCommands.h"
#include "../../utils/Logger.h"
namespace UsdLayerManager {
// ─── LayerCreateCommand ───────────────────────────────────────────────────────
LayerCreateCommand::LayerCreateCommand(LayerManager* mgr, std::string path, int index)
: m_mgr(mgr)
, m_path(std::move(path))
, m_index(index)
, m_description("Create Layer " + m_path)
{
}
void LayerCreateCommand::Execute() {
if (!m_mgr) return;
// Record how many sublayers exist before insert to compute the real index used.
auto before = m_mgr->GetLayerStack().size();
m_mgr->CreateSublayer(m_path, m_index);
auto after = m_mgr->GetLayerStack().size();
// If a layer was actually inserted, compute its index.
if (after > before) {
auto layers = m_mgr->GetLayerStack();
for (int i = 0; i < static_cast<int>(layers.size()); ++i) {
if (layers[i].identifier == m_path || layers[i].displayName == m_path) {
m_insertedIndex = i;
break;
}
}
}
}
void LayerCreateCommand::Undo() {
if (!m_mgr) return;
if (m_insertedIndex >= 0) {
m_mgr->RemoveSublayer(m_insertedIndex);
}
}
// ─── LayerRemoveCommand ───────────────────────────────────────────────────────
LayerRemoveCommand::LayerRemoveCommand(LayerManager* mgr, int index)
: m_mgr(mgr)
, m_index(index)
, m_description("Remove Layer")
{
if (!mgr) return;
auto layers = mgr->GetLayerStack();
if (index >= 0 && index < static_cast<int>(layers.size())) {
m_savedPath = layers[index].identifier;
m_description = "Remove Layer " + layers[index].displayName;
}
}
void LayerRemoveCommand::Execute() {
if (!m_mgr) return;
m_mgr->RemoveSublayer(m_index);
}
void LayerRemoveCommand::Undo() {
if (!m_mgr || m_savedPath.empty()) return;
// Re-insert at the original index.
m_mgr->InsertSublayerPath(m_savedPath, m_index);
}
// ─── LayerReorderCommand ──────────────────────────────────────────────────────
LayerReorderCommand::LayerReorderCommand(LayerManager* mgr,
std::vector<std::string> before,
std::vector<std::string> after)
: m_mgr(mgr)
, m_before(std::move(before))
, m_after(std::move(after))
, m_description("Reorder Layers")
{
}
void LayerReorderCommand::Execute() { ApplyOrder(m_after); }
void LayerReorderCommand::Undo() { ApplyOrder(m_before); }
void LayerReorderCommand::ApplyOrder(const std::vector<std::string>& order) {
if (!m_mgr) return;
// Remove all sublayers and re-insert in the desired order.
// We only control sublayers — root and session are fixed.
// First gather current sublayer indices (non-root, non-session).
auto layers = m_mgr->GetLayerStack();
// Count sublayers and remove them from highest index down.
std::vector<int> sublayerIndices;
for (int i = 0; i < static_cast<int>(layers.size()); ++i) {
if (!layers[i].isRootLayer && !layers[i].isSessionLayer)
sublayerIndices.push_back(i);
}
// Remove from back to front to preserve indices.
for (int i = static_cast<int>(sublayerIndices.size()) - 1; i >= 0; --i)
m_mgr->RemoveSublayer(sublayerIndices[i]);
// Re-add in desired order.
for (const auto& path : order)
m_mgr->InsertSublayerPath(path, -1);
}
} // namespace UsdLayerManager
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "../CommandHistory.h"
#include "../LayerManager.h"
#include <string>
namespace UsdLayerManager {
class LayerCreateCommand : public ICommand {
public:
LayerCreateCommand(LayerManager* mgr, std::string path, int index = -1);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
LayerManager* m_mgr;
std::string m_path;
int m_index;
int m_insertedIndex = -1;
std::string m_description;
};
class LayerRemoveCommand : public ICommand {
public:
LayerRemoveCommand(LayerManager* mgr, int index);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
LayerManager* m_mgr;
int m_index;
std::string m_savedPath;
std::string m_description;
};
class LayerReorderCommand : public ICommand {
public:
LayerReorderCommand(LayerManager* mgr, std::vector<std::string> before, std::vector<std::string> after);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
void ApplyOrder(const std::vector<std::string>& order);
LayerManager* m_mgr;
std::vector<std::string> m_before;
std::vector<std::string> m_after;
std::string m_description;
};
} // namespace UsdLayerManager
@@ -0,0 +1,42 @@
#include "ReplaceReferenceCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/references.h>
namespace UsdLayerManager {
ReplaceReferenceCommand::ReplaceReferenceCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
const pxr::SdfReference& oldRef,
const pxr::SdfReference& newRef)
: m_stage(stage)
, m_primPath(primPath)
, m_oldRef(oldRef)
, m_newRef(newRef)
, m_description("Replace Reference on " + primPath.GetString())
{
}
void ReplaceReferenceCommand::Execute() { Apply(m_oldRef, m_newRef); }
void ReplaceReferenceCommand::Undo() { Apply(m_newRef, m_oldRef); }
void ReplaceReferenceCommand::Apply(const pxr::SdfReference& toRemove,
const pxr::SdfReference& toAdd)
{
if (!m_stage) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (!prim.IsValid()) {
LOG_ERROR("ReplaceReferenceCommand::Apply: prim not valid " + m_primPath.GetString());
return;
}
try {
pxr::UsdReferences refs = prim.GetReferences();
if (!refs.RemoveReference(toRemove))
LOG_ERROR("ReplaceReferenceCommand: failed to remove reference");
if (!refs.AddReference(toAdd))
LOG_ERROR("ReplaceReferenceCommand: failed to add reference");
} catch (const std::exception& e) {
LOG_ERROR(std::string("ReplaceReferenceCommand::Apply error: ") + e.what());
}
}
} // namespace UsdLayerManager
@@ -0,0 +1,32 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/reference.h>
#include <string>
namespace UsdLayerManager {
class ReplaceReferenceCommand : public ICommand {
public:
ReplaceReferenceCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
const pxr::SdfReference& oldRef,
const pxr::SdfReference& newRef);
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
void Apply(const pxr::SdfReference& toRemove, const pxr::SdfReference& toAdd);
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_primPath;
pxr::SdfReference m_oldRef;
pxr::SdfReference m_newRef;
std::string m_description;
};
} // namespace UsdLayerManager
+67
View File
@@ -0,0 +1,67 @@
#include "TransformCommand.h"
#include "../../utils/Logger.h"
#include <pxr/usd/usd/editContext.h>
namespace UsdLayerManager {
TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
pxr::SdfLayerHandle editLayer,
const pxr::GfVec3d& oldTranslate,
const pxr::GfVec3f& oldRotate,
const pxr::GfVec3f& oldScale,
const pxr::GfVec3d& newTranslate,
const pxr::GfVec3f& newRotate,
const pxr::GfVec3f& newScale,
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder,
std::string description)
: m_stage(stage)
, m_primPath(primPath)
, m_editLayer(editLayer)
, m_oldTranslate(oldTranslate)
, m_oldRotate(oldRotate)
, m_oldScale(oldScale)
, m_newTranslate(newTranslate)
, m_newRotate(newRotate)
, m_newScale(newScale)
, m_rotOrder(rotOrder)
, m_description(std::move(description))
{
}
void TransformCommand::Execute() {
Apply(m_newTranslate, m_newRotate, m_newScale);
}
void TransformCommand::Undo() {
Apply(m_oldTranslate, m_oldRotate, m_oldScale);
}
void TransformCommand::Apply(const pxr::GfVec3d& t,
const pxr::GfVec3f& r,
const pxr::GfVec3f& s)
{
if (!m_stage || m_primPath.IsEmpty()) return;
pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath);
if (!prim) return;
pxr::UsdGeomXformCommonAPI api(prim);
if (!api) return;
try {
if (m_editLayer) {
pxr::UsdEditContext ec(m_stage, m_editLayer);
api.SetTranslate(t, pxr::UsdTimeCode::Default());
api.SetRotate(r, m_rotOrder, pxr::UsdTimeCode::Default());
api.SetScale(s, pxr::UsdTimeCode::Default());
} else {
api.SetTranslate(t, pxr::UsdTimeCode::Default());
api.SetRotate(r, m_rotOrder, pxr::UsdTimeCode::Default());
api.SetScale(s, pxr::UsdTimeCode::Default());
}
} catch (const std::exception& e) {
LOG_ERROR(std::string("TransformCommand::Apply error: ") + e.what());
}
}
} // namespace UsdLayerManager
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include "../CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/sdf/path.h>
#include <pxr/usd/sdf/layer.h>
#include <pxr/base/gf/vec3d.h>
#include <pxr/base/gf/vec3f.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
#include <string>
namespace UsdLayerManager {
/// Reverses a complete TRS edit on a single prim (from gizmo drag or
/// Property panel field commit). Stores the prim path, edit-target layer,
/// and pre/post translate/rotate/scale triples.
class TransformCommand : public ICommand {
public:
TransformCommand(pxr::UsdStageRefPtr stage,
const pxr::SdfPath& primPath,
pxr::SdfLayerHandle editLayer,
const pxr::GfVec3d& oldTranslate,
const pxr::GfVec3f& oldRotate,
const pxr::GfVec3f& oldScale,
const pxr::GfVec3d& newTranslate,
const pxr::GfVec3f& newRotate,
const pxr::GfVec3f& newScale,
pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder,
std::string description = "Transform");
void Execute() override;
void Undo() override;
std::string GetDescription() const override { return m_description; }
private:
void Apply(const pxr::GfVec3d& t,
const pxr::GfVec3f& r,
const pxr::GfVec3f& s);
pxr::UsdStageRefPtr m_stage;
pxr::SdfPath m_primPath;
pxr::SdfLayerHandle m_editLayer;
pxr::GfVec3d m_oldTranslate;
pxr::GfVec3f m_oldRotate;
pxr::GfVec3f m_oldScale;
pxr::GfVec3d m_newTranslate;
pxr::GfVec3f m_newRotate;
pxr::GfVec3f m_newScale;
pxr::UsdGeomXformCommonAPI::RotationOrder m_rotOrder;
std::string m_description;
};
} // namespace UsdLayerManager
+76
View File
@@ -0,0 +1,76 @@
#include "ui/Application.h"
#include "utils/Logger.h"
#include <pxr/base/plug/registry.h>
#include <exception>
#include <Windows.h>
#include <string>
static void SetUsdPluginPath() {
char exePath[MAX_PATH];
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
std::string exeDir(exePath);
size_t lastSlash = exeDir.find_last_of("\\/");
if (lastSlash != std::string::npos) {
exeDir = exeDir.substr(0, lastSlash);
}
std::string pluginPath = exeDir + "\\usd";
SetEnvironmentVariableA("PXR_PLUGINPATH_NAME", pluginPath.c_str());
std::vector<std::string> pluginPaths;
WIN32_FIND_DATAA findData;
std::string searchPattern = pluginPath + "\\*";
HANDLE hFind = FindFirstFileA(searchPattern.c_str(), &findData);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
std::string dirName(findData.cFileName);
if (dirName != "." && dirName != "..") {
std::string plugInfoPath = pluginPath + "\\" + dirName + "\\resources\\plugInfo.json";
DWORD attrs = GetFileAttributesA(plugInfoPath.c_str());
if (attrs != INVALID_FILE_ATTRIBUTES && !(attrs & FILE_ATTRIBUTE_DIRECTORY)) {
pluginPaths.push_back(plugInfoPath);
}
}
}
} while (FindNextFileA(hFind, &findData));
FindClose(hFind);
}
if (!pluginPaths.empty()) {
auto& registry = pxr::PlugRegistry::GetInstance();
auto registered = registry.RegisterPlugins(pluginPaths);
LOG_INFO("Registered " + std::to_string(registered.size()) + " USD plugins from " + pluginPath);
} else {
LOG_WARNING("No USD plugins found at " + pluginPath);
}
}
int main(int argc, char* argv[]) {
try {
UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info);
LOG_INFO("=== USD Layer Manager Starting ===");
SetUsdPluginPath();
UsdLayerManager::Application app;
if (!app.Initialize("USD Layer Manager", 1280, 720)) {
LOG_ERROR("Failed to initialize application");
return 1;
}
app.Run();
app.Shutdown();
LOG_INFO("=== USD Layer Manager Exiting ===");
return 0;
} catch (const std::exception& e) {
LOG_ERROR(std::string("Unhandled exception: ") + e.what());
return 1;
} catch (...) {
LOG_ERROR("Unknown exception occurred");
return 1;
}
}
+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
+59
View File
@@ -0,0 +1,59 @@
#include "FileDialog.h"
#include "Logger.h"
#include <commdlg.h>
#include <vector>
namespace UsdLayerManager {
std::string FileDialog::OpenFile(const char* filter, const char* title, HWND owner) {
std::vector<char> filename(MAX_PATH_LENGTH, 0);
OPENFILENAMEA ofn = {};
ofn.lStructSize = sizeof(OPENFILENAMEA);
ofn.hwndOwner = owner;
ofn.lpstrFilter = filter;
ofn.lpstrFile = filename.data();
ofn.nMaxFile = MAX_PATH_LENGTH;
ofn.lpstrTitle = title;
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
if (GetOpenFileNameA(&ofn)) {
return std::string(filename.data());
}
// User cancelled or error occurred
DWORD error = CommDlgExtendedError();
if (error != 0) {
LOG_ERROR("File dialog error code: " + std::to_string(error));
}
return "";
}
std::string FileDialog::SaveFile(const char* filter, const char* title, const char* defaultExt, HWND owner) {
std::vector<char> filename(MAX_PATH_LENGTH, 0);
OPENFILENAMEA ofn = {};
ofn.lStructSize = sizeof(OPENFILENAMEA);
ofn.hwndOwner = owner;
ofn.lpstrFilter = filter;
ofn.lpstrFile = filename.data();
ofn.nMaxFile = MAX_PATH_LENGTH;
ofn.lpstrTitle = title;
ofn.lpstrDefExt = defaultExt;
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_NOCHANGEDIR;
if (GetSaveFileNameA(&ofn)) {
return std::string(filename.data());
}
// User cancelled or error occurred
DWORD error = CommDlgExtendedError();
if (error != 0) {
LOG_ERROR("File dialog error code: " + std::to_string(error));
}
return "";
}
} // namespace UsdLayerManager
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <Windows.h>
namespace UsdLayerManager {
class FileDialog {
public:
// Open file dialog
static std::string OpenFile(
const char* filter = "USD Files (*.usd;*.usda;*.usdc)\0*.usd;*.usda;*.usdc\0All Files (*.*)\0*.*\0",
const char* title = "Open USD File",
HWND owner = nullptr
);
// Save file dialog
static std::string SaveFile(
const char* filter = "USD Files (*.usd;*.usda;*.usdc)\0*.usd;*.usda;*.usdc\0All Files (*.*)\0*.*\0",
const char* title = "Save USD File",
const char* defaultExt = "usd",
HWND owner = nullptr
);
private:
static const int MAX_PATH_LENGTH = 4096;
};
} // namespace UsdLayerManager
+47
View File
@@ -0,0 +1,47 @@
#include "GLExt.h"
#include "Logger.h"
#ifdef _WIN32
# include <Windows.h>
#endif
namespace UsdLayerManager {
namespace GL {
#ifdef _WIN32
// wglGetProcAddress only resolves extension / ARB functions.
// Core functions (OpenGL 1.x) live in opengl32.dll and must be
// fetched via GetProcAddress. This two-stage loader covers both.
static GLADapiproc WinGLLoader(const char* name) {
GLADapiproc proc = reinterpret_cast<GLADapiproc>(wglGetProcAddress(name));
if (!proc) {
HMODULE hMod = GetModuleHandleA("opengl32.dll");
if (hMod) {
proc = reinterpret_cast<GLADapiproc>(GetProcAddress(hMod, name));
}
}
return proc;
}
#endif
bool InitExtensions() {
// Must be called after wglMakeCurrent so the context is current.
#ifdef _WIN32
int version = gladLoadGL(WinGLLoader);
#else
int version = 0; // supply a platform loader for non-Windows
#endif
if (version == 0) {
LOG_ERROR("gladLoadGL failed - could not load OpenGL functions");
return false;
}
LOG_INFO("OpenGL loaded via glad (GL "
+ std::to_string(GLAD_VERSION_MAJOR(version)) + "."
+ std::to_string(GLAD_VERSION_MINOR(version)) + ")");
return true;
}
} // namespace GL
} // namespace UsdLayerManager
+15
View File
@@ -0,0 +1,15 @@
#pragma once
// glad must be included before any other OpenGL headers.
// It provides all GL core 3.3 functions and constants.
#include <glad/gl.h>
namespace UsdLayerManager {
namespace GL {
// Initializes the glad OpenGL function loader.
// Must be called after a valid OpenGL context has been made current.
bool InitExtensions();
} // namespace GL
} // namespace UsdLayerManager
+96
View File
@@ -0,0 +1,96 @@
#include "Logger.h"
#include <iomanip>
namespace UsdLayerManager {
Logger& Logger::Instance() {
static Logger instance;
return instance;
}
Logger::Logger()
: m_logLevel(LogLevel::Info) {
}
Logger::~Logger() {
if (m_logFile.is_open()) {
m_logFile.close();
}
}
void Logger::SetLogLevel(LogLevel level) {
std::lock_guard<std::mutex> lock(m_mutex);
m_logLevel = level;
}
void Logger::SetLogFile(const std::string& filename) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_logFile.is_open()) {
m_logFile.close();
}
m_logFile.open(filename, std::ios::out | std::ios::app);
if (!m_logFile.is_open()) {
std::cerr << "Failed to open log file: " << filename << std::endl;
}
}
void Logger::Debug(const std::string& message) {
Log(LogLevel::Debug, message);
}
void Logger::Info(const std::string& message) {
Log(LogLevel::Info, message);
}
void Logger::Warning(const std::string& message) {
Log(LogLevel::Warning, message);
}
void Logger::Error(const std::string& message) {
Log(LogLevel::Error, message);
}
void Logger::Log(LogLevel level, const std::string& message) {
if (level < m_logLevel) {
return;
}
std::lock_guard<std::mutex> lock(m_mutex);
std::string timestamp = GetTimestamp();
std::string levelStr = LevelToString(level);
std::string logMessage = "[" + timestamp + "] [" + levelStr + "] " + message;
// Output to console
if (level == LogLevel::Error) {
std::cerr << logMessage << std::endl;
} else {
std::cout << logMessage << std::endl;
}
// Output to file if open
if (m_logFile.is_open()) {
m_logFile << logMessage << std::endl;
m_logFile.flush();
}
}
std::string Logger::GetTimestamp() {
auto now = std::time(nullptr);
auto tm = *std::localtime(&now);
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
std::string Logger::LevelToString(LogLevel level) {
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
default: return "UNKNOWN";
}
}
} // namespace UsdLayerManager
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <string>
#include <iostream>
#include <sstream>
#include <fstream>
#include <mutex>
#include <ctime>
namespace UsdLayerManager {
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
class Logger {
public:
static Logger& Instance();
void SetLogLevel(LogLevel level);
void SetLogFile(const std::string& filename);
void Debug(const std::string& message);
void Info(const std::string& message);
void Warning(const std::string& message);
void Error(const std::string& message);
template<typename... Args>
void Debug(const std::string& format, Args... args) {
Log(LogLevel::Debug, FormatString(format, args...));
}
template<typename... Args>
void Info(const std::string& format, Args... args) {
Log(LogLevel::Info, FormatString(format, args...));
}
template<typename... Args>
void Warning(const std::string& format, Args... args) {
Log(LogLevel::Warning, FormatString(format, args...));
}
template<typename... Args>
void Error(const std::string& format, Args... args) {
Log(LogLevel::Error, FormatString(format, args...));
}
private:
Logger();
~Logger();
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
void Log(LogLevel level, const std::string& message);
std::string GetTimestamp();
std::string LevelToString(LogLevel level);
template<typename T>
std::string FormatString(const std::string& format, T value) {
std::ostringstream oss;
oss << format << value;
return oss.str();
}
template<typename T, typename... Args>
std::string FormatString(const std::string& format, T value, Args... args) {
std::ostringstream oss;
oss << format << value;
return FormatString(oss.str(), args...);
}
LogLevel m_logLevel;
std::ofstream m_logFile;
std::mutex m_mutex;
};
// Convenience macros
#define LOG_DEBUG(msg) UsdLayerManager::Logger::Instance().Debug(msg)
#define LOG_INFO(msg) UsdLayerManager::Logger::Instance().Info(msg)
#define LOG_WARNING(msg) UsdLayerManager::Logger::Instance().Warning(msg)
#define LOG_ERROR(msg) UsdLayerManager::Logger::Instance().Error(msg)
} // namespace UsdLayerManager
+53
View File
@@ -0,0 +1,53 @@
#include "PathUtils.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <unistd.h>
#include <limits.h>
#endif
#include <algorithm>
namespace UsdLayerManager {
std::string ExeDir()
{
#ifdef _WIN32
wchar_t wpath[MAX_PATH] = {};
DWORD len = ::GetModuleFileNameW(nullptr, wpath, MAX_PATH);
if (len == 0 || len >= MAX_PATH)
return "./";
// Convert UTF-16 → UTF-8
int needed = ::WideCharToMultiByte(CP_UTF8, 0, wpath, static_cast<int>(len),
nullptr, 0, nullptr, nullptr);
std::string path(static_cast<size_t>(needed), '\0');
::WideCharToMultiByte(CP_UTF8, 0, wpath, static_cast<int>(len),
path.data(), needed, nullptr, nullptr);
#else
char buf[PATH_MAX] = {};
ssize_t len = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
if (len <= 0)
return "./";
std::string path(buf, static_cast<size_t>(len));
#endif
// Normalise to forward slashes and strip the filename
std::replace(path.begin(), path.end(), '\\', '/');
auto slash = path.rfind('/');
if (slash != std::string::npos)
path = path.substr(0, slash + 1); // keep trailing '/'
else
path = "./";
return path;
}
std::string ResourcePath(const std::string& relativePath)
{
return ExeDir() + relativePath;
}
} // namespace UsdLayerManager
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <string>
namespace UsdLayerManager {
/// Return the directory that contains the running executable, with a
/// trailing path separator. On Windows this uses GetModuleFileNameW.
/// Falls back to "./" if the path cannot be determined.
///
/// Usage:
/// std::string iconDir = ExeDir() + "resources/icons";
std::string ExeDir();
/// Concatenate the exe directory with a relative path.
/// ResourcePath("resources/icons") == ExeDir() + "resources/icons"
std::string ResourcePath(const std::string& relativePath);
} // namespace UsdLayerManager