701 lines
28 KiB
C++
701 lines
28 KiB
C++
#include "ImGuiLayer.h"
|
|
#include "MorphManager.h"
|
|
#include "AppConfig.h"
|
|
#include "PoseManager.h"
|
|
#include "BoneSelector.h"
|
|
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <cstdio>
|
|
#include <cmath>
|
|
|
|
#include <imgui/imgui.h>
|
|
#include <imgui/imgui_impl_opengl3.h>
|
|
|
|
#include <osg/RenderInfo>
|
|
#include <osg/GraphicsContext>
|
|
#include <osg/Viewport>
|
|
#include <osgViewer/Viewer>
|
|
#include <osgGA/GUIEventAdapter>
|
|
|
|
// ── Draw callback ─────────────────────────────────────────────────────────────
|
|
|
|
struct ImGuiDrawCallback : public osg::Camera::DrawCallback {
|
|
ImGuiLayer* layer;
|
|
mutable bool glInitialized = false;
|
|
|
|
explicit ImGuiDrawCallback(ImGuiLayer* l) : layer(l) {}
|
|
|
|
void operator()(osg::RenderInfo& ri) const override {
|
|
osg::GraphicsContext* gc = ri.getCurrentCamera()->getGraphicsContext();
|
|
osg::Viewport* vp = ri.getCurrentCamera()->getViewport();
|
|
|
|
float w = vp ? static_cast<float>(vp->width())
|
|
: (gc && gc->getTraits() ? static_cast<float>(gc->getTraits()->width) : 0.f);
|
|
float h = vp ? static_cast<float>(vp->height())
|
|
: (gc && gc->getTraits() ? static_cast<float>(gc->getTraits()->height) : 0.f);
|
|
if (w < 1.f || h < 1.f) return;
|
|
|
|
if (!glInitialized) {
|
|
bool ok = ImGui_ImplOpenGL3_Init(nullptr);
|
|
if (!ok) ok = ImGui_ImplOpenGL3_Init("#version 130");
|
|
if (!ok) { std::cerr << "[imgui] GL init failed\n"; return; }
|
|
layer->markGLInitialized();
|
|
glInitialized = true;
|
|
std::cout << "[imgui] OpenGL backend initialized.\n";
|
|
}
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.DisplaySize = ImVec2(w, h);
|
|
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui::NewFrame();
|
|
layer->renderPanel();
|
|
ImGui::Render();
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
}
|
|
};
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
ImGuiLayer::ImGuiLayer(MorphManager* morphMgr, const AppConfig* cfg)
|
|
: m_morphMgr(morphMgr), m_cfg(cfg)
|
|
{}
|
|
|
|
ImGuiLayer::~ImGuiLayer() {
|
|
if (m_glInitialized) ImGui_ImplOpenGL3_Shutdown();
|
|
if (m_contextCreated) ImGui::DestroyContext();
|
|
}
|
|
|
|
void ImGuiLayer::markGLInitialized() { m_glInitialized = true; }
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::init(osgViewer::Viewer* viewer) {
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
m_contextCreated = true;
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
|
|
|
// ── Font ─────────────────────────────────────────────────────────────────
|
|
static const ImWchar glyphRanges[] = {
|
|
0x0020, 0x00FF,
|
|
0x3000, 0x30FF, // Hiragana + Katakana
|
|
0x4E00, 0x9FFF, // CJK common kanji
|
|
0xFF00, 0xFFEF,
|
|
0,
|
|
};
|
|
|
|
std::string fontPath = m_cfg ? m_cfg->getString("ui.font_path") : std::string();
|
|
float fontSize = m_cfg ? m_cfg->getFloat("ui.font_size", 14.f) : 14.f;
|
|
m_panelWidth = m_cfg ? m_cfg->getFloat("ui.panel_width", 380.f) : 380.f;
|
|
|
|
bool fontLoaded = false;
|
|
if (!fontPath.empty()) {
|
|
ImFont* f = io.Fonts->AddFontFromFileTTF(
|
|
fontPath.c_str(), fontSize, nullptr, glyphRanges);
|
|
if (f) {
|
|
std::cout << "[imgui] Font: " << fontPath << "\n";
|
|
fontLoaded = true;
|
|
} else {
|
|
std::cerr << "[imgui] Failed to load font: " << fontPath
|
|
<< "\n Check ui.font_path in assets/config.ini\n";
|
|
}
|
|
}
|
|
if (!fontLoaded) {
|
|
std::cout << "[imgui] Using built-in font (ASCII only).\n";
|
|
io.Fonts->AddFontDefault();
|
|
}
|
|
|
|
// ── Style ────────────────────────────────────────────────────────────────
|
|
ImGui::StyleColorsDark();
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.WindowRounding = 8.f;
|
|
style.FrameRounding = 4.f;
|
|
style.TabRounding = 4.f;
|
|
style.ScrollbarRounding = 4.f;
|
|
style.GrabRounding = 4.f;
|
|
style.WindowPadding = ImVec2(10, 10);
|
|
style.ItemSpacing = ImVec2(8, 5);
|
|
style.WindowMinSize = ImVec2(220.f, 200.f);
|
|
|
|
auto& c = style.Colors;
|
|
c[ImGuiCol_TitleBg] = ImVec4(0.18f, 0.12f, 0.28f, 1.f);
|
|
c[ImGuiCol_TitleBgActive] = ImVec4(0.28f, 0.18f, 0.45f, 1.f);
|
|
c[ImGuiCol_Tab] = ImVec4(0.18f, 0.12f, 0.28f, 1.f);
|
|
c[ImGuiCol_TabHovered] = ImVec4(0.45f, 0.30f, 0.70f, 1.f);
|
|
c[ImGuiCol_TabActive] = ImVec4(0.35f, 0.22f, 0.56f, 1.f);
|
|
c[ImGuiCol_TabUnfocused] = ImVec4(0.14f, 0.09f, 0.22f, 1.f);
|
|
c[ImGuiCol_TabUnfocusedActive]= ImVec4(0.28f, 0.18f, 0.45f, 1.f);
|
|
c[ImGuiCol_ResizeGrip] = ImVec4(0.45f, 0.30f, 0.70f, 0.5f);
|
|
c[ImGuiCol_ResizeGripHovered] = ImVec4(0.65f, 0.45f, 0.90f, 0.8f);
|
|
c[ImGuiCol_ResizeGripActive] = ImVec4(0.80f, 0.60f, 1.00f, 1.0f);
|
|
c[ImGuiCol_Header] = ImVec4(0.28f, 0.18f, 0.45f, 0.6f);
|
|
c[ImGuiCol_HeaderHovered] = ImVec4(0.38f, 0.25f, 0.60f, 0.8f);
|
|
c[ImGuiCol_SliderGrab] = ImVec4(0.65f, 0.45f, 0.90f, 1.f);
|
|
c[ImGuiCol_SliderGrabActive] = ImVec4(0.80f, 0.60f, 1.00f, 1.f);
|
|
c[ImGuiCol_FrameBg] = ImVec4(0.12f, 0.08f, 0.18f, 1.f);
|
|
c[ImGuiCol_FrameBgHovered] = ImVec4(0.22f, 0.15f, 0.32f, 1.f);
|
|
c[ImGuiCol_Button] = ImVec4(0.28f, 0.18f, 0.45f, 1.f);
|
|
c[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.27f, 0.62f, 1.f);
|
|
c[ImGuiCol_CheckMark] = ImVec4(0.80f, 0.60f, 1.00f, 1.f);
|
|
c[ImGuiCol_ScrollbarGrab] = ImVec4(0.45f, 0.30f, 0.70f, 1.f);
|
|
|
|
// ── Camera ───────────────────────────────────────────────────────────────
|
|
osgViewer::Viewer::Windows windows;
|
|
viewer->getWindows(windows);
|
|
if (windows.empty()) { std::cerr << "[imgui] No windows found!\n"; return; }
|
|
|
|
osg::GraphicsContext* gc = windows[0];
|
|
const auto* traits = gc ? gc->getTraits() : nullptr;
|
|
int winW = traits ? traits->width : 1280;
|
|
int winH = traits ? traits->height : 720;
|
|
|
|
m_camera = new osg::Camera;
|
|
m_camera->setName("ImGuiCamera");
|
|
m_camera->setRenderOrder(osg::Camera::POST_RENDER, 100);
|
|
m_camera->setClearMask(0);
|
|
m_camera->setAllowEventFocus(false);
|
|
m_camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
|
|
m_camera->setViewMatrix(osg::Matrix::identity());
|
|
m_camera->setProjectionMatrix(osg::Matrix::ortho2D(0, winW, 0, winH));
|
|
m_camera->setViewport(0, 0, winW, winH);
|
|
m_camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
|
|
m_camera->setGraphicsContext(gc);
|
|
m_camera->setPostDrawCallback(new ImGuiDrawCallback(this));
|
|
|
|
viewer->addSlave(m_camera, false);
|
|
m_viewer = viewer;
|
|
std::cout << "[imgui] Overlay camera attached (" << winW << "x" << winH << ").\n";
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
bool ImGuiLayer::handleEvent(const osgGA::GUIEventAdapter& ea) {
|
|
if (!m_contextCreated) return false;
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
switch (ea.getEventType()) {
|
|
case osgGA::GUIEventAdapter::MOVE:
|
|
case osgGA::GUIEventAdapter::DRAG:
|
|
io.AddMousePosEvent(ea.getX(), io.DisplaySize.y - ea.getY());
|
|
break;
|
|
case osgGA::GUIEventAdapter::PUSH:
|
|
case osgGA::GUIEventAdapter::RELEASE: {
|
|
bool down = ea.getEventType() == osgGA::GUIEventAdapter::PUSH;
|
|
if (ea.getButton() == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON)
|
|
io.AddMouseButtonEvent(0, down);
|
|
else if (ea.getButton() == osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON)
|
|
io.AddMouseButtonEvent(1, down);
|
|
else if (ea.getButton() == osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON)
|
|
io.AddMouseButtonEvent(2, down);
|
|
break;
|
|
}
|
|
case osgGA::GUIEventAdapter::SCROLL:
|
|
io.AddMouseWheelEvent(0.f,
|
|
ea.getScrollingMotion() == osgGA::GUIEventAdapter::SCROLL_UP
|
|
? 1.f : -1.f);
|
|
break;
|
|
case osgGA::GUIEventAdapter::KEYDOWN:
|
|
case osgGA::GUIEventAdapter::KEYUP: {
|
|
bool down = ea.getEventType() == osgGA::GUIEventAdapter::KEYDOWN;
|
|
int key = ea.getKey();
|
|
if (key >= 32 && key < 127 && down)
|
|
io.AddInputCharacter(static_cast<unsigned int>(key));
|
|
if (key == osgGA::GUIEventAdapter::KEY_BackSpace)
|
|
io.AddKeyEvent(ImGuiKey_Backspace, down);
|
|
if (key == osgGA::GUIEventAdapter::KEY_Delete)
|
|
io.AddKeyEvent(ImGuiKey_Delete, down);
|
|
if (key == osgGA::GUIEventAdapter::KEY_Return)
|
|
io.AddKeyEvent(ImGuiKey_Enter, down);
|
|
if (key == osgGA::GUIEventAdapter::KEY_Escape)
|
|
io.AddKeyEvent(ImGuiKey_Escape, down);
|
|
break;
|
|
}
|
|
case osgGA::GUIEventAdapter::RESIZE: {
|
|
int w = ea.getWindowWidth(), h = ea.getWindowHeight();
|
|
io.DisplaySize = ImVec2(static_cast<float>(w), static_cast<float>(h));
|
|
if (m_camera) {
|
|
m_camera->setViewport(0, 0, w, h);
|
|
m_camera->setProjectionMatrix(osg::Matrix::ortho2D(0, w, 0, h));
|
|
}
|
|
break;
|
|
}
|
|
default: break;
|
|
}
|
|
|
|
return io.WantCaptureMouse &&
|
|
(ea.getEventType() == osgGA::GUIEventAdapter::PUSH ||
|
|
ea.getEventType() == osgGA::GUIEventAdapter::RELEASE ||
|
|
ea.getEventType() == osgGA::GUIEventAdapter::MOVE ||
|
|
ea.getEventType() == osgGA::GUIEventAdapter::DRAG ||
|
|
ea.getEventType() == osgGA::GUIEventAdapter::SCROLL);
|
|
}
|
|
|
|
// ── Main panel with tabs ──────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::renderPanel() {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
ImGui::SetNextWindowPos(
|
|
ImVec2(io.DisplaySize.x - m_panelWidth - 10.f, 10.f), ImGuiCond_Once);
|
|
ImGui::SetNextWindowSize(
|
|
ImVec2(m_panelWidth, io.DisplaySize.y - 20.f), ImGuiCond_Once);
|
|
ImGui::SetNextWindowBgAlpha(0.88f);
|
|
ImGui::SetNextWindowSizeConstraints(
|
|
ImVec2(220.f, 200.f),
|
|
ImVec2(io.DisplaySize.x * 0.9f, io.DisplaySize.y));
|
|
|
|
if (!ImGui::Begin("Model Controls", nullptr, ImGuiWindowFlags_NoCollapse)) {
|
|
ImGui::End(); return;
|
|
}
|
|
|
|
m_panelWidth = ImGui::GetWindowWidth();
|
|
|
|
if (ImGui::BeginTabBar("##tabs")) {
|
|
if (ImGui::BeginTabItem("Morphs")) {
|
|
renderMorphTab();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Shaders")) {
|
|
renderShaderTab();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Transform")) {
|
|
renderTransformTab();
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Pose")) {
|
|
renderPoseTab();
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
// ── Morphs tab ────────────────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::renderMorphTab() {
|
|
if (!m_morphMgr) { ImGui::TextDisabled("No model loaded."); return; }
|
|
const auto& names = m_morphMgr->morphNames();
|
|
if (names.empty()) { ImGui::TextDisabled("No morphs found."); return; }
|
|
|
|
// Toolbar
|
|
ImGui::SetNextItemWidth(-90.f);
|
|
ImGui::InputText("Search##morph", m_searchBuf, sizeof(m_searchBuf));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Reset All")) {
|
|
m_morphMgr->resetAll();
|
|
m_searchBuf[0] = '\0';
|
|
}
|
|
ImGui::Checkbox("Active only", &m_showOnlyActive);
|
|
ImGui::Separator();
|
|
|
|
std::string filter(m_searchBuf);
|
|
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
|
|
|
// Two-column layout: slider | name
|
|
// Slider column is fixed at 120px; name gets all the remaining space
|
|
const float sliderW = 120.f;
|
|
const float btnW = 22.f;
|
|
const float nameW = ImGui::GetContentRegionAvail().x - sliderW - btnW
|
|
- ImGui::GetStyle().ItemSpacing.x * 3.f;
|
|
|
|
int visible = 0;
|
|
ImGui::BeginChild("##morphlist", ImVec2(0, 0), false);
|
|
|
|
for (const auto& name : names) {
|
|
float w = m_morphMgr->getWeight(name);
|
|
if (m_showOnlyActive && w < 1e-4f) continue;
|
|
if (!filter.empty()) {
|
|
std::string lname = name;
|
|
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
|
if (lname.find(filter) == std::string::npos) continue;
|
|
}
|
|
++visible;
|
|
|
|
bool isActive = (w > 1e-4f);
|
|
|
|
// Slider
|
|
ImGui::SetNextItemWidth(sliderW);
|
|
std::string sliderID = "##s" + name;
|
|
if (isActive)
|
|
ImGui::PushStyleColor(ImGuiCol_SliderGrab,
|
|
ImVec4(1.0f, 0.75f, 0.3f, 1.f));
|
|
if (ImGui::SliderFloat(sliderID.c_str(), &w, 0.f, 1.f))
|
|
m_morphMgr->setWeight(name, w);
|
|
if (isActive) ImGui::PopStyleColor();
|
|
|
|
// Reset button
|
|
ImGui::SameLine();
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.4f, 0.1f, 0.1f, 1.f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 1.f));
|
|
std::string btnID = "x##" + name;
|
|
if (ImGui::SmallButton(btnID.c_str()))
|
|
m_morphMgr->setWeight(name, 0.f);
|
|
ImGui::PopStyleColor(2);
|
|
|
|
// Name — clipped to available width
|
|
ImGui::SameLine();
|
|
if (isActive)
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.4f, 1.f));
|
|
ImGui::SetNextItemWidth(nameW);
|
|
// PushTextWrapPos clips long names cleanly
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + nameW);
|
|
ImGui::TextUnformatted(name.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
if (isActive) ImGui::PopStyleColor();
|
|
}
|
|
|
|
if (visible == 0)
|
|
ImGui::TextDisabled("No morphs match filter.");
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
// ── Shaders tab ───────────────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::renderShaderTab() {
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("Select a shading mode:");
|
|
ImGui::Spacing();
|
|
|
|
struct ShaderOption {
|
|
const char* id;
|
|
const char* label;
|
|
const char* desc;
|
|
};
|
|
|
|
static const ShaderOption options[] = {
|
|
{ "flat", "Flat / Unlit",
|
|
"Raw texture colours, no lighting.\nUseful for checking UV maps." },
|
|
{ "cel", "Cel Shading",
|
|
"Quantised diffuse bands.\nClean anime look without outlines." },
|
|
{ "toon", "Toon Shading",
|
|
"Cel bands + specular highlight\n+ rim light. Full anime style." },
|
|
};
|
|
|
|
for (auto& opt : options) {
|
|
bool selected = (m_currentShader == opt.id);
|
|
|
|
if (selected)
|
|
ImGui::PushStyleColor(ImGuiCol_Button,
|
|
ImVec4(0.45f, 0.28f, 0.72f, 1.f));
|
|
|
|
float bw = ImGui::GetContentRegionAvail().x;
|
|
if (ImGui::Button(opt.label, ImVec2(bw, 36.f))) {
|
|
m_currentShader = opt.id;
|
|
if (onShaderChange) onShaderChange(opt.id);
|
|
}
|
|
|
|
if (selected) ImGui::PopStyleColor();
|
|
|
|
// Description text, indented
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.7f, 1.f));
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.f);
|
|
ImGui::TextUnformatted(opt.desc);
|
|
ImGui::PopStyleColor();
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float bw = ImGui::GetContentRegionAvail().x;
|
|
if (ImGui::Button("Reload Shaders from Disk", ImVec2(bw, 30.f))) {
|
|
if (onShaderReload) onShaderReload();
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.6f, 1.f));
|
|
ImGui::TextWrapped("Reloads GLSL files without recompiling.\n"
|
|
"Edit assets/shaders/*.vert / *.frag\nthen click.");
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// ── Transform tab ─────────────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::renderTransformTab() {
|
|
ImGui::TextDisabled("Model scale:");
|
|
ImGui::Spacing();
|
|
|
|
// Show current scale value
|
|
ImGui::Text("Current: %.4f", m_scale);
|
|
ImGui::Spacing();
|
|
|
|
// Text input for scale
|
|
// Pre-fill the buffer with the current value if it's empty
|
|
if (m_scaleBuf[0] == '\0')
|
|
snprintf(m_scaleBuf, sizeof(m_scaleBuf), "%.4f", m_scale);
|
|
|
|
ImGui::SetNextItemWidth(-1.f);
|
|
bool entered = ImGui::InputText("##scale", m_scaleBuf, sizeof(m_scaleBuf),
|
|
ImGuiInputTextFlags_EnterReturnsTrue |
|
|
ImGuiInputTextFlags_CharsDecimal);
|
|
|
|
ImGui::Spacing();
|
|
float bw = ImGui::GetContentRegionAvail().x;
|
|
bool clicked = ImGui::Button("Apply Scale", ImVec2(bw, 30.f));
|
|
|
|
if (entered || clicked) {
|
|
try {
|
|
float parsed = std::stof(std::string(m_scaleBuf));
|
|
if (parsed > 0.f) {
|
|
m_scale = parsed;
|
|
if (onScaleChange) onScaleChange(m_scale);
|
|
} else {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1));
|
|
ImGui::TextUnformatted("Scale must be > 0");
|
|
ImGui::PopStyleColor();
|
|
}
|
|
} catch (...) {
|
|
// non-numeric input — just ignore
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("Quick presets:");
|
|
ImGui::Spacing();
|
|
|
|
// Common scale presets in a 3-column grid
|
|
const std::pair<const char*, float> presets[] = {
|
|
{"0.01x", 0.01f}, {"0.1x", 0.1f}, {"0.5x", 0.5f},
|
|
{"1x", 1.0f}, {"2x", 2.0f}, {"10x", 10.0f},
|
|
};
|
|
|
|
int col = 0;
|
|
for (auto& [label, val] : presets) {
|
|
if (col > 0) ImGui::SameLine();
|
|
float colW = (ImGui::GetContentRegionAvail().x
|
|
+ ImGui::GetStyle().ItemSpacing.x * (2 - col)) / (3 - col);
|
|
|
|
bool active = std::abs(m_scale - val) < 1e-4f;
|
|
if (active)
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.28f, 0.72f, 1.f));
|
|
|
|
if (ImGui::Button(label, ImVec2(colW, 28.f))) {
|
|
m_scale = val;
|
|
snprintf(m_scaleBuf, sizeof(m_scaleBuf), "%.4f", m_scale);
|
|
if (onScaleChange) onScaleChange(m_scale);
|
|
}
|
|
|
|
if (active) ImGui::PopStyleColor();
|
|
col = (col + 1) % 3;
|
|
}
|
|
}
|
|
|
|
// ── Pose tab ──────────────────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::renderPoseTab() {
|
|
ImGui::Spacing();
|
|
|
|
// ── Enable toggle ─────────────────────────────────────────────────────────
|
|
if (ImGui::Checkbox("Enable Pose Mode", &m_poseEnabled)) {
|
|
if (onPoseModeToggle) onPoseModeToggle(m_poseEnabled);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(?)");
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("Click bones in viewport to select.\n"
|
|
"Adjust rotation/translation below.");
|
|
|
|
if (!m_poseEnabled) {
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("Enable pose mode to edit bones.");
|
|
return;
|
|
}
|
|
|
|
if (!m_poseMgr || !m_poseMgr->isInitialized()) {
|
|
ImGui::TextDisabled("No skeleton loaded.");
|
|
return;
|
|
}
|
|
|
|
// ── Skeleton visibility ───────────────────────────────────────────────────
|
|
if (ImGui::Checkbox("Show Skeleton", &m_bonesVisible)) {
|
|
if (onBoneVisToggle) onBoneVisToggle(m_bonesVisible);
|
|
}
|
|
|
|
float bw = ImGui::GetContentRegionAvail().x;
|
|
if (ImGui::Button("Reset All Bones", ImVec2(bw, 28.f)))
|
|
m_poseMgr->resetAll();
|
|
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// ── Selected bone gizmo ───────────────────────────────────────────────────
|
|
if (!m_selectedBone.empty()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.85f, 0.3f, 1.f));
|
|
ImGui::Text("Selected: %s", m_selectedBone.c_str());
|
|
ImGui::PopStyleColor();
|
|
|
|
// pose fetched per-control below
|
|
|
|
// Sync euler display from current pose quat
|
|
// Convert quat → euler (degrees) on first selection or change
|
|
// (changed flag removed — pose applied immediately via callback)
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextUnformatted("Rotation (bone-local degrees):");
|
|
ImGui::SetNextItemWidth(-1.f);
|
|
if (ImGui::SliderFloat3("##rot", m_boneEuler, -180.f, 180.f)) {
|
|
// Convert euler degrees to quaternion
|
|
float rx = m_boneEuler[0] * M_PI / 180.f;
|
|
float ry = m_boneEuler[1] * M_PI / 180.f;
|
|
float rz = m_boneEuler[2] * M_PI / 180.f;
|
|
osg::Quat qx(rx, osg::Vec3(1,0,0));
|
|
osg::Quat qy(ry, osg::Vec3(0,1,0));
|
|
osg::Quat qz(rz, osg::Vec3(0,0,1));
|
|
// Local-space: Rx * Ry * Rz — rotates around bone's own axes
|
|
m_poseMgr->setBoneRotation(m_selectedBone, qx * qy * qz);
|
|
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextUnformatted("Translation:");
|
|
ImGui::SetNextItemWidth(-1.f);
|
|
if (ImGui::SliderFloat3("##trans", m_boneTrans, -20.f, 20.f)) {
|
|
m_poseMgr->setBoneTranslation(m_selectedBone,
|
|
osg::Vec3(m_boneTrans[0], m_boneTrans[1], m_boneTrans[2]));
|
|
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Reset This Bone", ImVec2(-1.f, 26.f))) {
|
|
m_poseMgr->resetBone(m_selectedBone);
|
|
memset(m_boneEuler, 0, sizeof(m_boneEuler));
|
|
memset(m_boneTrans, 0, sizeof(m_boneTrans));
|
|
}
|
|
ImGui::Separator();
|
|
} else {
|
|
ImGui::TextDisabled("Click a bone in the viewport\nor select from the tree below.");
|
|
ImGui::Separator();
|
|
}
|
|
|
|
// ── Bone search + tree ────────────────────────────────────────────────────
|
|
ImGui::Spacing();
|
|
ImGui::SetNextItemWidth(-1.f);
|
|
ImGui::InputText("##bonesearch", m_boneSearch, sizeof(m_boneSearch));
|
|
ImGui::Spacing();
|
|
|
|
ImGui::BeginChild("##bonetree", ImVec2(0, 0), false);
|
|
|
|
std::string filter(m_boneSearch);
|
|
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
|
|
|
if (filter.empty()) {
|
|
// Show as tree — find root bones (no parent)
|
|
const auto& allNames = m_poseMgr->boneNames();
|
|
for (const auto& name : allNames) {
|
|
if (m_poseMgr->boneParent(name).empty())
|
|
renderBoneTree(name, 0);
|
|
}
|
|
} else {
|
|
// Flat filtered list
|
|
for (const auto& name : m_poseMgr->boneNames()) {
|
|
std::string lname = name;
|
|
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
|
|
if (lname.find(filter) == std::string::npos) continue;
|
|
|
|
bool sel = (name == m_selectedBone);
|
|
if (sel) ImGui::PushStyleColor(ImGuiCol_Text,
|
|
ImVec4(1.f, 0.85f, 0.3f, 1.f));
|
|
if (ImGui::Selectable(name.c_str(), sel)) {
|
|
m_selectedBone = name;
|
|
if (m_boneSel) m_boneSel->setSelected(name);
|
|
// Sync sliders to current pose
|
|
if (m_poseMgr) {
|
|
const auto& p = m_poseMgr->getBonePose(m_selectedBone);
|
|
if (p.modified) {
|
|
// Extract euler angles from quaternion via rotation matrix
|
|
osg::Matrix rotMat; rotMat.makeRotate(p.rotation);
|
|
// Extract XYZ euler: atan2 from rotation matrix elements
|
|
float ex = std::atan2( rotMat(2,1), rotMat(2,2));
|
|
float ey = std::atan2(-rotMat(2,0),
|
|
std::sqrt(rotMat(2,1)*rotMat(2,1)+rotMat(2,2)*rotMat(2,2)));
|
|
float ez = std::atan2( rotMat(1,0), rotMat(0,0));
|
|
m_boneEuler[0] = ex * 180.f / (float)M_PI;
|
|
m_boneEuler[1] = ey * 180.f / (float)M_PI;
|
|
m_boneEuler[2] = ez * 180.f / (float)M_PI;
|
|
m_boneTrans[0] = p.translation.x();
|
|
m_boneTrans[1] = p.translation.y();
|
|
m_boneTrans[2] = p.translation.z();
|
|
} else {
|
|
memset(m_boneEuler, 0, sizeof(m_boneEuler));
|
|
memset(m_boneTrans, 0, sizeof(m_boneTrans));
|
|
}
|
|
}
|
|
}
|
|
if (sel) ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
void ImGuiLayer::renderBoneTree(const std::string& boneName, int depth) {
|
|
const auto& childMap = m_poseMgr->boneChildren();
|
|
auto it = childMap.find(boneName);
|
|
bool hasChildren = (it != childMap.end() && !it->second.empty());
|
|
bool sel = (boneName == m_selectedBone);
|
|
|
|
// PushID ensures unique IDs even for bones with identical display names
|
|
ImGui::PushID(boneName.c_str());
|
|
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow
|
|
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
|
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf;
|
|
if (sel) flags |= ImGuiTreeNodeFlags_Selected;
|
|
|
|
const auto& pose = m_poseMgr->getBonePose(boneName);
|
|
int colorsPushed = 0;
|
|
if (pose.modified) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.85f, 0.3f, 1.f));
|
|
++colorsPushed;
|
|
}
|
|
|
|
bool open = ImGui::TreeNodeEx("##node", flags, "%s", boneName.c_str());
|
|
|
|
if (colorsPushed) ImGui::PopStyleColor(colorsPushed);
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
m_selectedBone = boneName;
|
|
if (m_boneSel) m_boneSel->setSelected(boneName);
|
|
if (m_poseMgr) {
|
|
const auto& p = m_poseMgr->getBonePose(boneName);
|
|
if (p.modified) {
|
|
// Extract euler angles from quaternion via rotation matrix
|
|
osg::Matrix rotMat; rotMat.makeRotate(p.rotation);
|
|
// Extract XYZ euler: atan2 from rotation matrix elements
|
|
float ex = std::atan2( rotMat(2,1), rotMat(2,2));
|
|
float ey = std::atan2(-rotMat(2,0),
|
|
std::sqrt(rotMat(2,1)*rotMat(2,1)+rotMat(2,2)*rotMat(2,2)));
|
|
float ez = std::atan2( rotMat(1,0), rotMat(0,0));
|
|
m_boneEuler[0] = ex * 180.f / (float)M_PI;
|
|
m_boneEuler[1] = ey * 180.f / (float)M_PI;
|
|
m_boneEuler[2] = ez * 180.f / (float)M_PI;
|
|
m_boneTrans[0] = p.translation.x();
|
|
m_boneTrans[1] = p.translation.y();
|
|
m_boneTrans[2] = p.translation.z();
|
|
} else {
|
|
memset(m_boneEuler, 0, sizeof(m_boneEuler));
|
|
memset(m_boneTrans, 0, sizeof(m_boneTrans));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (open) {
|
|
if (hasChildren)
|
|
for (const auto& child : it->second)
|
|
renderBoneTree(child, depth + 1);
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
} |