Init Repo
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 (0–1); used by HSplit + Quad
|
||||
float m_splitV = 0.5f; ///< Vertical divider (0–1); 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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user