MetaCore/MetaEditor/DearImGuiEditorShell.cpp
2026-03-19 18:01:50 +08:00

963 lines
36 KiB
C++

#include "MetaEditor/DearImGuiEditorShell.h"
#include "MetaCore/foundation/FileSystem.h"
#include <windows.h>
#if METACORE_HAS_IMGUI
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_impl_win32.h>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
#endif
#if METACORE_HAS_IMGUIZMO
#include <ImGuizmo.h>
#endif
#include <cmath>
#include <cstdlib>
#include <string_view>
#include <sstream>
#include <utility>
namespace metacore::editor {
namespace {
constexpr const char* kMetaCoreImGuiShellWindowProp = "MetaCoreDearImGuiEditorShell";
std::string uppercase(std::string value) {
for (char& ch : value) {
if (ch >= 'a' && ch <= 'z') {
ch = static_cast<char>(ch - 'a' + 'A');
}
}
return value;
}
#if METACORE_HAS_IMGUIZMO
ImGuizmo::OPERATION to_imguizmo_operation(const std::string& operation) {
if (operation == "Rotate") {
return ImGuizmo::ROTATE;
}
if (operation == "Scale") {
return ImGuizmo::SCALE;
}
return ImGuizmo::TRANSLATE;
}
ImGuizmo::MODE to_imguizmo_mode(const std::string& space) {
return space == "World" ? ImGuizmo::WORLD : ImGuizmo::LOCAL;
}
std::array<float, 16> make_transform_matrix(const EditorFrameSnapshot::ViewportSnapshot& viewport) {
const float rx = viewport.object_rotation[0] * 0.01745329252F;
const float ry = viewport.object_rotation[1] * 0.01745329252F;
const float rz = viewport.object_rotation[2] * 0.01745329252F;
const float cx = std::cos(rx);
const float sx = std::sin(rx);
const float cy = std::cos(ry);
const float sy = std::sin(ry);
const float cz = std::cos(rz);
const float sz = std::sin(rz);
const float r00 = cz * cy;
const float r01 = cz * sy * sx - sz * cx;
const float r02 = cz * sy * cx + sz * sx;
const float r10 = sz * cy;
const float r11 = sz * sy * sx + cz * cx;
const float r12 = sz * sy * cx - cz * sx;
const float r20 = -sy;
const float r21 = cy * sx;
const float r22 = cy * cx;
return {
r00 * viewport.object_scale[0], r10 * viewport.object_scale[0], r20 * viewport.object_scale[0], 0.0F,
r01 * viewport.object_scale[1], r11 * viewport.object_scale[1], r21 * viewport.object_scale[1], 0.0F,
r02 * viewport.object_scale[2], r12 * viewport.object_scale[2], r22 * viewport.object_scale[2], 0.0F,
viewport.object_position[0], viewport.object_position[1], viewport.object_position[2], 1.0F
};
}
std::array<float, 16> make_view_matrix(const std::array<float, 3>& camera_position) {
return {
1.0F, 0.0F, 0.0F, 0.0F,
0.0F, 1.0F, 0.0F, 0.0F,
0.0F, 0.0F, 1.0F, 0.0F,
-camera_position[0], -camera_position[1], -camera_position[2], 1.0F
};
}
std::array<float, 16> make_projection_matrix(float width, float height) {
const float aspect = height > 0.0F ? width / height : 1.0F;
const float fov = 60.0F * 0.01745329252F;
const float f = 1.0F / std::tan(fov * 0.5F);
const float near_plane = 0.1F;
const float far_plane = 1000.0F;
return {
f / aspect, 0.0F, 0.0F, 0.0F,
0.0F, f, 0.0F, 0.0F,
0.0F, 0.0F, far_plane / (far_plane - near_plane), 1.0F,
0.0F, 0.0F, (-near_plane * far_plane) / (far_plane - near_plane), 0.0F
};
}
#endif
} // namespace
#if METACORE_HAS_IMGUI
LRESULT CALLBACK metacore_imgui_shell_wnd_proc(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param) {
if (ImGui_ImplWin32_WndProcHandler(hwnd, message, w_param, l_param)) {
return 1;
}
auto* shell = reinterpret_cast<DearImGuiEditorShell*>(GetPropA(hwnd, kMetaCoreImGuiShellWindowProp));
if (shell != nullptr && shell->previous_wnd_proc_ != nullptr) {
return CallWindowProc(shell->previous_wnd_proc_, hwnd, message, w_param, l_param);
}
return DefWindowProc(hwnd, message, w_param, l_param);
}
#endif
bool DearImGuiEditorShell::initialize() {
const char* headless_frame_env = std::getenv("METACORE_ENABLE_HEADLESS_IMGUI");
headless_frame_enabled_ = headless_frame_env != nullptr && std::string(headless_frame_env) == "1";
initialize_imgui_context();
initialized_ = true;
return true;
}
void DearImGuiEditorShell::initialize_imgui_context() {
#if METACORE_HAS_IMGUI
const bool should_create_context = headless_frame_enabled_ || native_window_handle_ != nullptr;
if (!should_create_context || ImGui::GetCurrentContext() != nullptr) {
return;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
imgui_context_owned_ = true;
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.DeltaTime = 1.0F / 60.0F;
if (headless_frame_enabled_) {
io.DisplaySize = ImVec2(1600.0F, 900.0F);
} else {
io.DisplaySize = ImVec2(
native_window_width_ > 0 ? static_cast<float>(native_window_width_) : 1280.0F,
native_window_height_ > 0 ? static_cast<float>(native_window_height_) : 720.0F
);
}
unsigned char* font_pixels = nullptr;
int font_width = 0;
int font_height = 0;
io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height);
io.Fonts->SetTexID(static_cast<ImTextureID>(1));
if (native_window_handle_ != nullptr) {
ImGui_ImplWin32_Init(native_window_handle_);
SetPropA(native_window_handle_, kMetaCoreImGuiShellWindowProp, this);
previous_wnd_proc_ = reinterpret_cast<WNDPROC>(
SetWindowLongPtr(native_window_handle_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(metacore_imgui_shell_wnd_proc))
);
win32_backend_enabled_ = true;
}
#if METACORE_HAS_IMGUIZMO
ImGuizmo::SetImGuiContext(ImGui::GetCurrentContext());
#endif
ImGui::StyleColorsDark();
#endif
}
void DearImGuiEditorShell::shutdown() {
shutdown_imgui_context();
initialized_ = false;
last_snapshot_ = {};
last_draw_command_count_ = 0;
last_vertex_count_ = 0;
last_index_count_ = 0;
last_window_count_ = 0;
headless_frame_enabled_ = false;
dock_layout_initialized_ = false;
headless_gizmo_override_consumed_ = false;
headless_transform_override_consumed_ = false;
}
void DearImGuiEditorShell::shutdown_imgui_context() {
#if METACORE_HAS_IMGUI
if (win32_backend_enabled_ && native_window_handle_ != nullptr) {
SetPropA(native_window_handle_, kMetaCoreImGuiShellWindowProp, nullptr);
if (previous_wnd_proc_ != nullptr) {
SetWindowLongPtr(native_window_handle_, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(previous_wnd_proc_));
previous_wnd_proc_ = nullptr;
}
ImGui_ImplWin32_Shutdown();
win32_backend_enabled_ = false;
}
if (imgui_context_owned_ && ImGui::GetCurrentContext() != nullptr) {
ImGui::DestroyContext();
}
#endif
imgui_context_owned_ = false;
}
void DearImGuiEditorShell::consume_snapshot(const EditorFrameSnapshot& snapshot) {
last_snapshot_ = snapshot;
enqueue_headless_gizmo_state_edit(snapshot);
enqueue_headless_transform_edit(snapshot);
#if METACORE_HAS_IMGUI
if ((headless_frame_enabled_ || win32_backend_enabled_) && ImGui::GetCurrentContext() != nullptr) {
render_imgui_frame(snapshot);
capture_render_frame();
} else {
last_draw_command_count_ = 0;
last_vertex_count_ = 0;
last_index_count_ = 0;
last_window_count_ = 0;
}
#endif
export_debug_snapshot();
}
void DearImGuiEditorShell::capture_render_frame() {
#if METACORE_HAS_IMGUI
pending_render_frame_ = {};
if (const ImDrawData* draw_data = ImGui::GetDrawData(); draw_data != nullptr) {
pending_render_frame_.display_pos = {draw_data->DisplayPos.x, draw_data->DisplayPos.y};
pending_render_frame_.display_size = {draw_data->DisplaySize.x, draw_data->DisplaySize.y};
pending_render_frame_.framebuffer_scale = {draw_data->FramebufferScale.x, draw_data->FramebufferScale.y};
pending_render_frame_.draw_list_count = draw_data->CmdListsCount;
pending_render_frame_.total_vertex_count = draw_data->TotalVtxCount;
pending_render_frame_.total_index_count = draw_data->TotalIdxCount;
pending_render_frame_.vertices.reserve(static_cast<std::size_t>(draw_data->TotalVtxCount));
pending_render_frame_.indices.reserve(static_cast<std::size_t>(draw_data->TotalIdxCount));
std::size_t global_vertex_offset = 0;
std::size_t global_index_offset = 0;
for (int list_index = 0; list_index < draw_data->CmdListsCount; ++list_index) {
const ImDrawList* draw_list = draw_data->CmdLists[list_index];
for (int vertex_index = 0; vertex_index < draw_list->VtxBuffer.Size; ++vertex_index) {
const ImDrawVert& vertex = draw_list->VtxBuffer[vertex_index];
pending_render_frame_.vertices.push_back({
{vertex.pos.x, vertex.pos.y},
{vertex.uv.x, vertex.uv.y},
vertex.col
});
}
for (int index_index = 0; index_index < draw_list->IdxBuffer.Size; ++index_index) {
pending_render_frame_.indices.push_back(static_cast<std::uint32_t>(draw_list->IdxBuffer[index_index]) + static_cast<std::uint32_t>(global_vertex_offset));
}
for (int cmd_index = 0; cmd_index < draw_list->CmdBuffer.Size; ++cmd_index) {
const ImDrawCmd& cmd = draw_list->CmdBuffer[cmd_index];
if (cmd.UserCallback != nullptr) {
continue;
}
pending_render_frame_.commands.push_back({
{cmd.ClipRect.x, cmd.ClipRect.y, cmd.ClipRect.z, cmd.ClipRect.w},
0,
static_cast<std::size_t>(cmd.ElemCount),
global_index_offset + static_cast<std::size_t>(cmd.IdxOffset),
global_vertex_offset + static_cast<std::size_t>(cmd.VtxOffset)
});
}
global_vertex_offset += static_cast<std::size_t>(draw_list->VtxBuffer.Size);
global_index_offset += static_cast<std::size_t>(draw_list->IdxBuffer.Size);
}
pending_render_frame_.total_command_count = static_cast<int>(pending_render_frame_.commands.size());
last_draw_command_count_ = pending_render_frame_.total_command_count;
last_vertex_count_ = pending_render_frame_.total_vertex_count;
last_index_count_ = pending_render_frame_.total_index_count;
last_window_count_ = pending_render_frame_.draw_list_count;
return;
}
#endif
last_draw_command_count_ = 0;
last_vertex_count_ = 0;
last_index_count_ = 0;
last_window_count_ = 0;
}
void DearImGuiEditorShell::configure_platform_window(std::uintptr_t native_window_handle, const int width, const int height) {
native_window_handle_ = reinterpret_cast<HWND>(native_window_handle);
native_window_width_ = width;
native_window_height_ = height;
}
void DearImGuiEditorShell::refresh_platform_window_metrics(const int width, const int height) {
native_window_width_ = width;
native_window_height_ = height;
}
void DearImGuiEditorShell::enqueue_headless_gizmo_state_edit(const EditorFrameSnapshot& snapshot) {
(void)snapshot;
if (!headless_frame_enabled_ || headless_gizmo_override_consumed_) {
return;
}
PendingShellEdits::GizmoStateEdit edit;
bool has_edit = false;
if (const char* operation_env = std::getenv("METACORE_HEADLESS_GIZMO_OPERATION")) {
edit.has_operation = true;
edit.operation = uppercase(operation_env);
has_edit = true;
}
if (const char* space_env = std::getenv("METACORE_HEADLESS_GIZMO_SPACE")) {
edit.has_space = true;
edit.space = uppercase(space_env);
has_edit = true;
}
if (const char* snap_env = std::getenv("METACORE_HEADLESS_GIZMO_SNAP")) {
edit.has_snap_enabled = true;
edit.snap_enabled = std::string(snap_env) == "1";
has_edit = true;
}
if (const char* snap_values_env = std::getenv("METACORE_HEADLESS_GIZMO_SNAP_VALUES")) {
std::istringstream stream(snap_values_env);
float x = 1.0F;
float y = 1.0F;
float z = 1.0F;
if (stream >> x >> y >> z) {
edit.has_snap_values = true;
edit.snap_values = {x, y, z};
has_edit = true;
}
}
if (has_edit) {
pending_edits_.gizmo_state_edit = edit;
headless_gizmo_override_consumed_ = true;
}
}
void DearImGuiEditorShell::enqueue_headless_transform_edit(const EditorFrameSnapshot& snapshot) {
if (!headless_frame_enabled_ || !snapshot.viewport.has_selection || headless_transform_override_consumed_) {
return;
}
const char* translate_env = std::getenv("METACORE_HEADLESS_GIZMO_TRANSLATE");
if (translate_env == nullptr) {
return;
}
std::istringstream stream(translate_env);
float x = snapshot.viewport.object_position[0];
float y = snapshot.viewport.object_position[1];
float z = snapshot.viewport.object_position[2];
if (!(stream >> x >> y >> z)) {
return;
}
PendingShellEdits::TransformEdit edit;
edit.entity_id = snapshot.viewport.selected_object_id;
edit.position = {x, y, z};
edit.rotation = snapshot.viewport.object_rotation;
edit.scale = snapshot.viewport.object_scale;
pending_edits_.transform_edit = edit;
headless_transform_override_consumed_ = true;
}
void DearImGuiEditorShell::render_imgui_frame(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
ImGuiIO& io = ImGui::GetIO();
if (headless_frame_enabled_) {
io.DisplaySize = ImVec2(1600.0F, 900.0F);
} else {
io.DisplaySize = ImVec2(
native_window_width_ > 0 ? static_cast<float>(native_window_width_) : 1280.0F,
native_window_height_ > 0 ? static_cast<float>(native_window_height_) : 720.0F
);
}
io.DeltaTime = 1.0F / 60.0F;
if (win32_backend_enabled_) {
ImGui_ImplWin32_NewFrame();
}
ImGui::NewFrame();
render_dockspace_frame(snapshot);
ImGui::Render();
fulfill_texture_requests();
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::fulfill_texture_requests() {
#if METACORE_HAS_IMGUI
ImDrawData* draw_data = ImGui::GetDrawData();
if (draw_data == nullptr || draw_data->Textures == nullptr) {
return;
}
for (ImTextureData* texture : *draw_data->Textures) {
if (texture == nullptr) {
continue;
}
if (texture->Status == ImTextureStatus_WantCreate) {
texture->SetTexID(static_cast<ImTextureID>(next_mock_texture_id_++));
texture->SetStatus(ImTextureStatus_OK);
} else if (texture->Status == ImTextureStatus_WantDestroy) {
texture->SetStatus(ImTextureStatus_Destroyed);
}
}
#endif
}
void DearImGuiEditorShell::render_dockspace_frame(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::SetNextWindowViewport(viewport->ID);
constexpr ImGuiWindowFlags host_window_flags =
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus |
ImGuiWindowFlags_MenuBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F));
ImGui::Begin("MetaCore DockSpace", nullptr, host_window_flags);
ImGui::PopStyleVar(3);
if (ImGui::BeginMenuBar()) {
for (const auto& menu : snapshot.shell.menus) {
if (ImGui::BeginMenu(menu.menu_name.c_str())) {
for (const auto& action : menu.actions) {
ImGui::MenuItem(action.label.c_str(), action.shortcut.empty() ? nullptr : action.shortcut.c_str(), false, action.enabled);
}
ImGui::EndMenu();
}
}
ImGui::EndMenuBar();
}
ImGuiID dockspace_id = ImGui::GetID("MetaCoreEditorDockspace");
ImGui::DockSpace(dockspace_id, ImVec2(0.0F, 0.0F), ImGuiDockNodeFlags_PassthruCentralNode);
ImGui::End();
ensure_default_dock_layout(snapshot, dockspace_id);
render_toolbar(snapshot);
for (const auto& section : snapshot.shell.sections) {
for (const auto& panel : section.panels) {
render_panel(panel, snapshot);
}
}
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::ensure_default_dock_layout(const EditorFrameSnapshot& snapshot, unsigned int dockspace_id) {
#if METACORE_HAS_IMGUI
if (dock_layout_initialized_) {
return;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->WorkSize);
ImGuiID left_id = 0;
ImGuiID right_id = 0;
ImGuiID bottom_id = 0;
ImGuiID center_id = dockspace_id;
ImGuiID preview_id = 0;
ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Left, 0.22F, &left_id, &center_id);
ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Right, 0.24F, &right_id, &center_id);
ImGui::DockBuilderSplitNode(center_id, ImGuiDir_Down, 0.26F, &bottom_id, &center_id);
ImGui::DockBuilderSplitNode(bottom_id, ImGuiDir_Down, 0.45F, &preview_id, &bottom_id);
for (const auto& section : snapshot.shell.sections) {
for (const auto& panel : section.panels) {
if (!panel.visible) {
continue;
}
ImGuiID target_id = center_id;
switch (panel.region) {
case metacore::engine::editor_core::DockRegion::LeftSidebar:
target_id = left_id;
break;
case metacore::engine::editor_core::DockRegion::RightSidebar:
target_id = right_id;
break;
case metacore::engine::editor_core::DockRegion::BottomConsole:
target_id = bottom_id;
break;
case metacore::engine::editor_core::DockRegion::BottomRuntimePreview:
target_id = preview_id;
break;
case metacore::engine::editor_core::DockRegion::CenterViewport:
target_id = center_id;
break;
case metacore::engine::editor_core::DockRegion::TopMenuBar:
case metacore::engine::editor_core::DockRegion::TopToolbar:
continue;
}
ImGui::DockBuilderDockWindow(panel.title.c_str(), target_id);
}
}
ImGui::DockBuilderFinish(dockspace_id);
dock_layout_initialized_ = true;
#else
(void)snapshot;
(void)dockspace_id;
#endif
}
void DearImGuiEditorShell::render_toolbar(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
if (!snapshot.toolbar_visible) {
return;
}
if (ImGui::Begin("Toolbar", nullptr, ImGuiWindowFlags_NoCollapse)) {
for (std::size_t group_index = 0; group_index < snapshot.shell.toolbar_groups.size(); ++group_index) {
const auto& group = snapshot.shell.toolbar_groups[group_index];
if (group_index > 0) {
ImGui::Separator();
}
ImGui::TextUnformatted(group.name.c_str());
for (const auto& action : group.actions) {
ImGui::SameLine();
ImGui::BeginDisabled(!action.enabled);
ImGui::Button(action.label.c_str());
ImGui::EndDisabled();
}
}
}
ImGui::End();
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_panel(const metacore::engine::editor_core::DockPanel& panel, const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
if (!panel.visible) {
return;
}
if (panel.id == "hierarchy" && !snapshot.hierarchy_visible) {
return;
}
if (panel.id == "project" && !snapshot.project_visible) {
return;
}
if (panel.id == "inspector" && !snapshot.inspector_visible) {
return;
}
if (panel.id == "console" && !snapshot.console_visible) {
return;
}
if (panel.id == "runtime_preview" && !snapshot.runtime_preview_visible) {
return;
}
const bool began = ImGui::Begin(panel.title.c_str(), nullptr, ImGuiWindowFlags_NoCollapse);
if (began) {
if (panel.id == "hierarchy") {
render_hierarchy_panel(snapshot);
} else if (panel.id == "project") {
render_project_panel(snapshot);
} else if (panel.id == "viewport") {
render_viewport_panel(snapshot);
} else if (panel.id == "inspector") {
render_inspector_panel(snapshot);
} else if (panel.id == "console") {
render_console_panel(snapshot);
} else if (panel.id == "runtime_preview") {
render_runtime_preview_panel(snapshot);
} else if (panel.id == "main_menu") {
ImGui::TextUnformatted("MetaCore menu host");
} else if (panel.id == "toolbar") {
ImGui::TextUnformatted("MetaCore toolbar host");
} else {
ImGui::Text("Panel: %s", panel.id.c_str());
}
}
ImGui::End();
#else
(void)panel;
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_hierarchy_panel(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
for (const auto& item : snapshot.hierarchy) {
ImGui::BulletText("%s", item.name.c_str());
}
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_project_panel(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
for (const auto& asset : snapshot.assets) {
ImGui::BulletText("%s", asset.relative_path.c_str());
}
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_viewport_panel(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
ImGui::Text("Scene Objects: %zu", snapshot.scene_object_count);
ImGui::Text(
"Camera: (%.1f, %.1f, %.1f)",
snapshot.viewport.camera_position[0],
snapshot.viewport.camera_position[1],
snapshot.viewport.camera_position[2]
);
if (!snapshot.viewport.focus_target_id.empty()) {
ImGui::Text("Focus Target: %s", snapshot.viewport.focus_target_id.c_str());
}
ImGui::Separator();
ImGui::TextUnformatted(snapshot.status_text.c_str());
ImGui::Spacing();
ImGui::TextUnformatted("MetaCore Scene View");
if (ImGui::Button("Translate")) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_operation = true;
edit.operation = "TRANSLATE";
pending_edits_.gizmo_state_edit = edit;
}
ImGui::SameLine();
if (ImGui::Button("Rotate")) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_operation = true;
edit.operation = "ROTATE";
pending_edits_.gizmo_state_edit = edit;
}
ImGui::SameLine();
if (ImGui::Button("Scale")) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_operation = true;
edit.operation = "SCALE";
pending_edits_.gizmo_state_edit = edit;
}
if (ImGui::Button(snapshot.viewport.gizmo_space == "Local" ? "Space: Local" : "Space: World")) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_space = true;
edit.space = snapshot.viewport.gizmo_space == "Local" ? "WORLD" : "LOCAL";
pending_edits_.gizmo_state_edit = edit;
}
ImGui::SameLine();
bool snap_enabled = snapshot.viewport.snap_enabled;
if (ImGui::Checkbox("Snap", &snap_enabled)) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_snap_enabled = true;
edit.snap_enabled = snap_enabled;
pending_edits_.gizmo_state_edit = edit;
}
if (snapshot.viewport.has_selection) {
ImGui::Text("Selection: %s", snapshot.viewport.selected_object_name.c_str());
ImGui::Text("Operation: %s", snapshot.viewport.gizmo_operation.c_str());
ImGui::SameLine();
ImGui::Text("Space: %s", snapshot.viewport.gizmo_space.c_str());
if (snapshot.viewport.snap_enabled) {
ImGui::Text(
"Snap: %.2f / %.2f / %.2f",
snapshot.viewport.snap_values[0],
snapshot.viewport.snap_values[1],
snapshot.viewport.snap_values[2]
);
} else {
ImGui::TextUnformatted("Snap: Off");
}
ImGui::Text(
"Transform P(%.2f, %.2f, %.2f) R(%.2f, %.2f, %.2f) S(%.2f, %.2f, %.2f)",
snapshot.viewport.object_position[0],
snapshot.viewport.object_position[1],
snapshot.viewport.object_position[2],
snapshot.viewport.object_rotation[0],
snapshot.viewport.object_rotation[1],
snapshot.viewport.object_rotation[2],
snapshot.viewport.object_scale[0],
snapshot.viewport.object_scale[1],
snapshot.viewport.object_scale[2]
);
} else {
ImGui::TextUnformatted("Selection: none");
}
const ImVec2 viewport_min = ImGui::GetCursorScreenPos();
const ImVec2 viewport_size = ImVec2(
ImGui::GetContentRegionAvail().x,
(ImGui::GetContentRegionAvail().y > 240.0F) ? ImGui::GetContentRegionAvail().y : 240.0F
);
ImGui::InvisibleButton("MetaCoreViewportCanvas", viewport_size);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddRectFilled(viewport_min, ImVec2(viewport_min.x + viewport_size.x, viewport_min.y + viewport_size.y), IM_COL32(26, 29, 36, 255));
draw_list->AddRect(ImVec2(viewport_min.x, viewport_min.y), ImVec2(viewport_min.x + viewport_size.x, viewport_min.y + viewport_size.y), IM_COL32(82, 91, 107, 255));
draw_list->AddText(ImVec2(viewport_min.x + 14.0F, viewport_min.y + 14.0F), IM_COL32(220, 226, 235, 255), "Viewport backend handoff surface");
#if METACORE_HAS_IMGUIZMO
if (snapshot.viewport.has_selection) {
auto model = make_transform_matrix(snapshot.viewport);
const auto view = make_view_matrix(snapshot.viewport.camera_position);
const auto projection = make_projection_matrix(viewport_size.x, viewport_size.y);
ImGuizmo::SetDrawlist(draw_list);
ImGuizmo::SetRect(viewport_min.x, viewport_min.y, viewport_size.x, viewport_size.y);
ImGuizmo::Manipulate(
view.data(),
projection.data(),
to_imguizmo_operation(snapshot.viewport.gizmo_operation),
to_imguizmo_mode(snapshot.viewport.gizmo_space),
model.data(),
nullptr,
snapshot.viewport.snap_enabled ? snapshot.viewport.snap_values.data() : nullptr
);
if (ImGuizmo::IsUsing()) {
float translation[3]{};
float rotation[3]{};
float scale[3]{};
ImGuizmo::DecomposeMatrixToComponents(model.data(), translation, rotation, scale);
PendingShellEdits::TransformEdit edit;
edit.entity_id = snapshot.viewport.selected_object_id;
edit.position = {translation[0], translation[1], translation[2]};
edit.rotation = {rotation[0], rotation[1], rotation[2]};
edit.scale = {scale[0], scale[1], scale[2]};
pending_edits_.transform_edit = edit;
}
ImGui::TextUnformatted("ImGuizmo bridge active");
} else {
ImGui::TextUnformatted("ImGuizmo bridge ready");
}
#endif
ImGuiIO& io = ImGui::GetIO();
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_operation = true;
edit.operation = "TRANSLATE";
pending_edits_.gizmo_state_edit = edit;
}
if (ImGui::IsKeyPressed(ImGuiKey_E, false)) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_operation = true;
edit.operation = "ROTATE";
pending_edits_.gizmo_state_edit = edit;
}
if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_operation = true;
edit.operation = "SCALE";
pending_edits_.gizmo_state_edit = edit;
}
if (ImGui::IsKeyPressed(ImGuiKey_Q, false)) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_space = true;
edit.space = snapshot.viewport.gizmo_space == "Local" ? "WORLD" : "LOCAL";
pending_edits_.gizmo_state_edit = edit;
}
if (io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_S, false)) {
PendingShellEdits::GizmoStateEdit edit;
edit.has_snap_enabled = true;
edit.snap_enabled = !snapshot.viewport.snap_enabled;
pending_edits_.gizmo_state_edit = edit;
}
}
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_inspector_panel(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
ImGui::TextUnformatted(snapshot.inspector.object_name.c_str());
ImGui::Separator();
for (const auto& field : snapshot.inspector.fields) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(field.label.c_str());
ImGui::SameLine(180.0F);
ImGui::TextUnformatted(field.value.c_str());
}
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_console_panel(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
for (const auto& message : snapshot.console) {
ImGui::Text("[%s] %s", message.category.c_str(), message.text.c_str());
}
#else
(void)snapshot;
#endif
}
void DearImGuiEditorShell::render_runtime_preview_panel(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
std::string_view document_path = "None";
for (const auto& field : snapshot.inspector.fields) {
if (field.label == "Document") {
document_path = field.value;
break;
}
}
ImGui::TextUnformatted("MetaCore Runtime UI Preview");
ImGui::Separator();
ImGui::Text("Document: %.*s", static_cast<int>(document_path.size()), document_path.data());
ImGui::Text("Console messages: %zu", snapshot.console.size());
#else
(void)snapshot;
#endif
}
bool DearImGuiEditorShell::initialized() const {
return initialized_;
}
const EditorFrameSnapshot& DearImGuiEditorShell::last_snapshot() const {
return last_snapshot_;
}
std::string DearImGuiEditorShell::summarize_snapshot() const {
std::ostringstream stream;
stream << "Shell=" << shell_name()
<< ", Menus=" << last_snapshot_.shell.menus.size()
<< ", ToolbarGroups=" << last_snapshot_.shell.toolbar_groups.size()
<< ", Sections=" << last_snapshot_.shell.sections.size()
<< ", Hierarchy=" << last_snapshot_.hierarchy.size()
<< ", Assets=" << last_snapshot_.assets.size()
<< ", Inspector=" << last_snapshot_.inspector.object_name
<< ", DrawCommands=" << last_draw_command_count_
<< ", DrawVertices=" << last_vertex_count_
<< ", DrawIndices=" << last_index_count_
<< ", DrawLists=" << last_window_count_;
return stream.str();
}
std::string DearImGuiEditorShell::describe_workspace() const {
std::ostringstream stream;
stream << "DearImGuiEditorShell composition";
for (const auto& menu : last_snapshot_.shell.menus) {
stream << "\n [Menu] " << menu.menu_name << " actions=" << menu.actions.size();
for (const auto& action : menu.actions) {
stream << "\n - " << action.label;
if (!action.shortcut.empty()) {
stream << " [" << action.shortcut << "]";
}
}
}
for (const auto& group : last_snapshot_.shell.toolbar_groups) {
stream << "\n [Toolbar] " << group.name << " actions=" << group.actions.size();
for (const auto& action : group.actions) {
stream << "\n - " << action.label;
}
}
for (const auto& section : last_snapshot_.shell.sections) {
stream << "\n [Section] panels=" << section.panels.size();
for (const auto& panel : section.panels) {
stream << "\n - " << panel.title << " (" << panel.id << ")";
}
}
return stream.str();
}
std::string DearImGuiEditorShell::shell_name() const {
return "DearImGuiEditorShell";
}
PendingShellEdits DearImGuiEditorShell::consume_pending_edits() {
PendingShellEdits edits = pending_edits_;
pending_edits_ = {};
return edits;
}
metacore::engine::render::UiDrawFrame DearImGuiEditorShell::consume_render_frame() {
auto frame = std::move(pending_render_frame_);
pending_render_frame_ = {};
return frame;
}
void DearImGuiEditorShell::set_debug_export_path(std::filesystem::path path) {
debug_export_path_ = std::move(path);
}
void DearImGuiEditorShell::export_debug_snapshot() const {
if (debug_export_path_.empty()) {
return;
}
std::ostringstream stream;
stream << summarize_snapshot() << "\n";
stream << describe_workspace() << "\n";
stream << "[Inspector] " << last_snapshot_.inspector.object_name << "\n";
for (const auto& field : last_snapshot_.inspector.fields) {
stream << " - " << field.label << ": " << field.value << "\n";
}
stream << "[Console]\n";
for (const auto& message : last_snapshot_.console) {
stream << " - [" << message.category << "] " << message.text << "\n";
}
export_viewport_snapshot(stream);
stream << "[DrawData]\n";
stream << " - CommandCount: " << last_draw_command_count_ << "\n";
stream << " - VertexCount: " << last_vertex_count_ << "\n";
stream << " - IndexCount: " << last_index_count_ << "\n";
stream << " - DrawLists: " << last_window_count_ << "\n";
foundation::write_text_file(debug_export_path_, stream.str());
}
void DearImGuiEditorShell::export_viewport_snapshot(std::ostringstream& stream) const {
stream << "[Viewport]\n";
stream << " - Selection: "
<< (last_snapshot_.viewport.has_selection ? last_snapshot_.viewport.selected_object_name : std::string("None")) << "\n";
stream << " - SelectionId: " << last_snapshot_.viewport.selected_object_id << "\n";
stream << " - Operation: " << last_snapshot_.viewport.gizmo_operation << "\n";
stream << " - Space: " << last_snapshot_.viewport.gizmo_space << "\n";
stream << " - SnapEnabled: " << (last_snapshot_.viewport.snap_enabled ? "true" : "false") << "\n";
stream << " - SnapValues: "
<< last_snapshot_.viewport.snap_values[0] << ", "
<< last_snapshot_.viewport.snap_values[1] << ", "
<< last_snapshot_.viewport.snap_values[2] << "\n";
stream << " - Camera: "
<< last_snapshot_.viewport.camera_position[0] << ", "
<< last_snapshot_.viewport.camera_position[1] << ", "
<< last_snapshot_.viewport.camera_position[2] << "\n";
stream << " - Position: "
<< last_snapshot_.viewport.object_position[0] << ", "
<< last_snapshot_.viewport.object_position[1] << ", "
<< last_snapshot_.viewport.object_position[2] << "\n";
stream << " - Rotation: "
<< last_snapshot_.viewport.object_rotation[0] << ", "
<< last_snapshot_.viewport.object_rotation[1] << ", "
<< last_snapshot_.viewport.object_rotation[2] << "\n";
stream << " - Scale: "
<< last_snapshot_.viewport.object_scale[0] << ", "
<< last_snapshot_.viewport.object_scale[1] << ", "
<< last_snapshot_.viewport.object_scale[2] << "\n";
stream << " - FocusTarget: " << last_snapshot_.viewport.focus_target_id << "\n";
}
} // namespace metacore::editor