metacore+imgui

This commit is contained in:
ayuan9957 2026-03-19 18:01:50 +08:00
parent 8d6e6bc0da
commit 87bedac5e0
20 changed files with 2177 additions and 132 deletions

View File

@ -48,20 +48,40 @@
"METACORE_ENABLE_IMGUIZMO": "ON",
"METACORE_ENABLE_RMLUI": "ON"
}
},
{
"name": "vs2022-integration-release",
"inherits": "base",
"displayName": "VS2022 Integration Release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"METACORE_ENABLE_PANDA3D": "ON",
"METACORE_ENABLE_IMGUI": "ON",
"METACORE_ENABLE_IMGUIZMO": "ON",
"METACORE_ENABLE_RMLUI": "ON"
}
}
],
"buildPresets": [
{
"name": "build-debug",
"configurePreset": "vs2022-debug"
"configurePreset": "vs2022-debug",
"configuration": "Debug"
},
{
"name": "build-release",
"configurePreset": "vs2022-release"
"configurePreset": "vs2022-release",
"configuration": "Release"
},
{
"name": "build-integration-debug",
"configurePreset": "vs2022-integration-debug"
"configurePreset": "vs2022-integration-debug",
"configuration": "Debug"
},
{
"name": "build-integration-release",
"configurePreset": "vs2022-integration-release",
"configuration": "Release"
}
],
"testPresets": [
@ -80,6 +100,14 @@
"output": {
"outputOnFailure": true
}
},
{
"name": "test-integration-release",
"configurePreset": "vs2022-integration-release",
"configuration": "Release",
"output": {
"outputOnFailure": true
}
}
]
}

View File

@ -47,6 +47,33 @@ struct TonemapSettings {
float exposure = 1.0F;
};
struct UiVertex {
std::array<float, 2> position{0.0F, 0.0F};
std::array<float, 2> uv{0.0F, 0.0F};
std::uint32_t color = 0;
};
struct UiDrawCommand {
std::array<float, 4> clip_rect{0.0F, 0.0F, 0.0F, 0.0F};
std::uintptr_t texture_id = 0;
std::size_t element_count = 0;
std::size_t index_offset = 0;
std::size_t vertex_offset = 0;
};
struct UiDrawFrame {
std::array<float, 2> display_pos{0.0F, 0.0F};
std::array<float, 2> display_size{0.0F, 0.0F};
std::array<float, 2> framebuffer_scale{1.0F, 1.0F};
int draw_list_count = 0;
int total_vertex_count = 0;
int total_index_count = 0;
int total_command_count = 0;
std::vector<UiVertex> vertices;
std::vector<std::uint32_t> indices;
std::vector<UiDrawCommand> commands;
};
class IMetaRenderBackend {
public:
virtual ~IMetaRenderBackend() = default;
@ -55,6 +82,7 @@ public:
virtual void shutdown() = 0;
virtual void set_frame_uniforms(const FrameUniforms& uniforms) = 0;
virtual void set_light_uniforms(const LightUniforms& uniforms) = 0;
virtual void set_ui_draw_frame(const UiDrawFrame& frame) = 0;
};
} // namespace metacore::engine::render

View File

@ -9,6 +9,8 @@
#include <filesystem>
#include <iostream>
#include <cstdint>
#include <string_view>
#include <windows.h>
namespace metacore::platform::panda {
@ -28,17 +30,17 @@ PandaPlatformContext::PandaPlatformContext() = default;
PandaPlatformContext::~PandaPlatformContext() = default;
bool PandaPlatformContext::initialize(const std::string& window_title) {
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: begin\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize enter\n";
foundation::log(foundation::LogLevel::Info, "MetaCore platform init: begin");
window_title_ = window_title;
char* enable_window_env = nullptr;
std::size_t env_size = 0;
_dupenv_s(&enable_window_env, &env_size, "METACORE_ENABLE_PANDA_WINDOW");
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: env read\n" << std::flush;
window_enabled_ = enable_window_env != nullptr && std::string(enable_window_env) == "1";
free(enable_window_env);
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after title copy\n";
close_requested_ = false;
frame_count_ = 0;
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize before env\n";
const char* enable_window_env = std::getenv("METACORE_ENABLE_PANDA_WINDOW");
window_enabled_ = enable_window_env != nullptr && std::string_view(enable_window_env) == "1";
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after env\n";
window_opened_ = false;
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: env parsed\n" << std::flush;
foundation::log(
foundation::LogLevel::Info,
"MetaCore platform init: window bootstrap requested=" + std::string(window_enabled_ ? "true" : "false")
@ -50,27 +52,28 @@ bool PandaPlatformContext::initialize(const std::string& window_title) {
panda_build_date_ = "runtime-probe-deferred";
const auto panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
if (std::filesystem::exists(panda_bin)) {
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: setting DLL dir\n" << std::flush;
foundation::log(foundation::LogLevel::Info, "MetaCore platform init: setting Panda bin path to " + panda_bin.string());
SetDllDirectoryW(panda_bin.wstring().c_str());
}
native_state_ = std::make_unique<NativeState>();
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: native state ready\n" << std::flush;
foundation::log(foundation::LogLevel::Info, "MetaCore platform init: native state allocated");
if (window_enabled_) {
try {
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: before open_framework\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize before open_framework\n";
native_state_->framework.open_framework();
native_state_->framework_open = true;
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after open_framework\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: before WindowProperties\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after open_framework\n";
native_state_->framework.set_window_title("MetaCore Window Probe");
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after framework.set_window_title\n";
WindowProperties props;
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after WindowProperties\n" << std::flush;
props.set_size(1280, 720);
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after set_size\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: before open_window\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after WindowProperties ctor\n";
props.set_size(960, 540);
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after props.set_size\n";
props.set_title("MetaCore Window Probe");
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after props.set_title\n";
native_state_->window = native_state_->framework.open_window(props, 0);
std::cerr << "[MetaCore][Trace] PandaPlatformContext initialize: after open_window\n" << std::flush;
std::cerr << "[MetaCore][Trace] PandaPlatformContext::initialize after open_window\n";
window_opened_ = native_state_->window != nullptr;
if (!window_opened_) {
foundation::log(foundation::LogLevel::Warning, "Panda window bootstrap requested but open_window returned null");
@ -101,6 +104,8 @@ void PandaPlatformContext::shutdown() {
panda_build_date_.clear();
window_enabled_ = false;
window_opened_ = false;
close_requested_ = false;
frame_count_ = 0;
#if METACORE_HAS_PANDA3D
if (native_state_) {
if (native_state_->framework_open) {
@ -119,9 +124,27 @@ void PandaPlatformContext::shutdown() {
}
void PandaPlatformContext::pump_frame() {
++frame_count_;
#if METACORE_HAS_PANDA3D
if (window_opened_ && native_state_ && native_state_->framework_open) {
native_state_->framework.do_frame(nullptr);
if (native_state_->window != nullptr) {
GraphicsWindow* graphics_window = native_state_->window->get_graphics_window();
if (graphics_window != nullptr && graphics_window->is_closed()) {
close_requested_ = true;
}
}
}
#endif
}
void PandaPlatformContext::request_close() {
close_requested_ = true;
#if METACORE_HAS_PANDA3D
if (window_opened_ && native_state_ && native_state_->window != nullptr) {
if (GraphicsWindow* graphics_window = native_state_->window->get_graphics_window(); graphics_window != nullptr) {
graphics_window->request_close();
}
}
#endif
}
@ -158,4 +181,78 @@ bool PandaPlatformContext::window_opened() const {
return window_opened_;
}
bool PandaPlatformContext::should_close() const {
return close_requested_;
}
int PandaPlatformContext::frame_count() const {
return frame_count_;
}
std::uintptr_t PandaPlatformContext::native_window_handle() const {
#if METACORE_HAS_PANDA3D
if (native_state_ && native_state_->window != nullptr) {
if (GraphicsWindow* graphics_window = native_state_->window->get_graphics_window(); graphics_window != nullptr) {
if (WindowHandle* handle = graphics_window->get_window_handle(); handle != nullptr) {
if (WindowHandle::OSHandle* os_handle = handle->get_os_handle(); os_handle != nullptr) {
const auto raw_handle = static_cast<std::uintptr_t>(os_handle->get_int_handle());
if (raw_handle != 0) {
return raw_handle;
}
}
}
}
}
#endif
if (!window_title_.empty()) {
const std::wstring wide_title(window_title_.begin(), window_title_.end());
if (HWND window = FindWindowW(nullptr, wide_title.c_str()); window != nullptr) {
return reinterpret_cast<std::uintptr_t>(window);
}
}
return 0;
}
std::uintptr_t PandaPlatformContext::window_framework_handle() const {
#if METACORE_HAS_PANDA3D
if (native_state_ && native_state_->window != nullptr) {
return reinterpret_cast<std::uintptr_t>(native_state_->window);
}
#endif
return 0;
}
int PandaPlatformContext::window_width() const {
#if METACORE_HAS_PANDA3D
if (native_state_ && native_state_->window != nullptr) {
if (GraphicsOutput* output = native_state_->window->get_graphics_output(); output != nullptr && output->has_size()) {
return output->get_x_size();
}
}
#endif
return 0;
}
int PandaPlatformContext::window_height() const {
#if METACORE_HAS_PANDA3D
if (native_state_ && native_state_->window != nullptr) {
if (GraphicsOutput* output = native_state_->window->get_graphics_output(); output != nullptr && output->has_size()) {
return output->get_y_size();
}
}
#endif
return 0;
}
std::uintptr_t PandaPlatformContext::primary_display_region_handle() const {
#if METACORE_HAS_PANDA3D
if (native_state_ && native_state_->window != nullptr) {
if (DisplayRegion* region = native_state_->window->get_display_region_3d(); region != nullptr) {
return reinterpret_cast<std::uintptr_t>(region);
}
}
#endif
return 0;
}
} // namespace metacore::platform::panda

View File

@ -14,6 +14,7 @@ public:
bool initialize(const std::string& window_title);
void shutdown();
void pump_frame();
void request_close();
[[nodiscard]] bool initialized() const;
[[nodiscard]] const std::string& window_title() const;
@ -23,6 +24,13 @@ public:
[[nodiscard]] const std::string& panda_build_date() const;
[[nodiscard]] bool window_enabled() const;
[[nodiscard]] bool window_opened() const;
[[nodiscard]] bool should_close() const;
[[nodiscard]] int frame_count() const;
[[nodiscard]] std::uintptr_t native_window_handle() const;
[[nodiscard]] std::uintptr_t window_framework_handle() const;
[[nodiscard]] int window_width() const;
[[nodiscard]] int window_height() const;
[[nodiscard]] std::uintptr_t primary_display_region_handle() const;
private:
struct NativeState;
@ -35,6 +43,8 @@ private:
std::string panda_build_date_;
bool window_enabled_ = false;
bool window_opened_ = false;
bool close_requested_ = false;
int frame_count_ = 0;
std::unique_ptr<NativeState> native_state_;
};

View File

@ -1,14 +1,159 @@
#include "MetaCore/platform/panda/PandaRenderBackend.h"
#if METACORE_HAS_PANDA3D
#include <callbackObject.h>
#include <displayRegion.h>
#include <lineSegs.h>
#include <nodePath.h>
#include <windowFramework.h>
#include <luse.h>
#endif
#include <sstream>
#include <iostream>
namespace metacore::platform::panda {
#if METACORE_HAS_PANDA3D
namespace {
class UiDrawCallback final : public CallbackObject {
public:
explicit UiDrawCallback(int* observed_draw_callback_count)
: observed_draw_callback_count_(observed_draw_callback_count) {
}
void do_callback(CallbackData* cbdata) override {
if (observed_draw_callback_count_ != nullptr) {
++(*observed_draw_callback_count_);
}
CallbackObject::do_callback(cbdata);
}
private:
int* observed_draw_callback_count_ = nullptr;
};
NodePath create_wire_cube(const NodePath& parent) {
LineSegs segments("metacore-demo-cube");
segments.set_thickness(2.5F);
segments.set_color(0.85F, 0.9F, 1.0F, 1.0F);
const LPoint3f corners[] = {
{-0.5F, -0.5F, -0.5F},
{0.5F, -0.5F, -0.5F},
{0.5F, 0.5F, -0.5F},
{-0.5F, 0.5F, -0.5F},
{-0.5F, -0.5F, 0.5F},
{0.5F, -0.5F, 0.5F},
{0.5F, 0.5F, 0.5F},
{-0.5F, 0.5F, 0.5F},
};
const int edges[][2] = {
{0, 1}, {1, 2}, {2, 3}, {3, 0},
{4, 5}, {5, 6}, {6, 7}, {7, 4},
{0, 4}, {1, 5}, {2, 6}, {3, 7},
};
for (const auto& edge : edges) {
segments.move_to(corners[edge[0]]);
segments.draw_to(corners[edge[1]]);
}
return parent.attach_new_node(segments.create());
}
NodePath create_gizmo(const NodePath& parent) {
LineSegs segments("metacore-demo-gizmo");
segments.set_thickness(4.0F);
segments.set_color(0.95F, 0.2F, 0.2F, 1.0F);
segments.move_to(0.0F, 0.0F, 0.0F);
segments.draw_to(1.5F, 0.0F, 0.0F);
segments.set_color(0.2F, 0.95F, 0.35F, 1.0F);
segments.move_to(0.0F, 0.0F, 0.0F);
segments.draw_to(0.0F, 1.5F, 0.0F);
segments.set_color(0.25F, 0.55F, 1.0F, 1.0F);
segments.move_to(0.0F, 0.0F, 0.0F);
segments.draw_to(0.0F, 0.0F, 1.5F);
return parent.attach_new_node(segments.create());
}
NodePath create_editor_chrome(const NodePath& parent) {
LineSegs segments("metacore-editor-chrome");
segments.set_thickness(2.0F);
segments.set_color(0.15F, 0.85F, 0.95F, 0.95F);
const auto draw_rect = [&](const float left, const float right, const float bottom, const float top) {
segments.move_to(left, 0.0F, bottom);
segments.draw_to(right, 0.0F, bottom);
segments.draw_to(right, 0.0F, top);
segments.draw_to(left, 0.0F, top);
segments.draw_to(left, 0.0F, bottom);
};
draw_rect(-1.30F, -0.72F, -0.92F, 0.84F);
draw_rect(-0.66F, 0.66F, -0.38F, 0.84F);
draw_rect(0.72F, 1.30F, -0.92F, 0.84F);
draw_rect(-0.66F, 1.30F, -0.92F, -0.45F);
draw_rect(-1.30F, 1.30F, 0.88F, 1.00F);
return parent.attach_new_node(segments.create());
}
} // namespace
#endif
struct PandaRenderBackend::NativeState {
#if METACORE_HAS_PANDA3D
PT(CallbackObject) draw_callback;
DisplayRegion* bound_display_region = nullptr;
WindowFramework* window = nullptr;
NodePath cube_root;
NodePath gizmo_root;
NodePath hud_root;
#endif
int observed_draw_callback_count = 0;
bool draw_callback_bound = false;
};
PandaRenderBackend::PandaRenderBackend() = default;
PandaRenderBackend::~PandaRenderBackend() = default;
bool PandaRenderBackend::initialize() {
initialized_ = true;
native_state_ = std::make_unique<NativeState>();
return true;
}
void PandaRenderBackend::shutdown() {
#if METACORE_HAS_PANDA3D
if (native_state_ && native_state_->bound_display_region != nullptr) {
native_state_->bound_display_region->clear_draw_callback();
native_state_->bound_display_region = nullptr;
}
if (native_state_) {
if (!native_state_->cube_root.is_empty()) {
native_state_->cube_root.remove_node();
}
if (!native_state_->gizmo_root.is_empty()) {
native_state_->gizmo_root.remove_node();
}
if (!native_state_->hud_root.is_empty()) {
native_state_->hud_root.remove_node();
}
native_state_->draw_callback.clear();
native_state_->draw_callback_bound = false;
native_state_->window = nullptr;
}
#endif
native_state_.reset();
initialized_ = false;
submitted_ui_frame_count_ = 0;
ui_draw_frame_ = {};
}
void PandaRenderBackend::set_frame_uniforms(const metacore::engine::render::FrameUniforms& uniforms) {
@ -19,6 +164,165 @@ void PandaRenderBackend::set_light_uniforms(const metacore::engine::render::Ligh
light_uniforms_ = uniforms;
}
void PandaRenderBackend::set_ui_draw_frame(const metacore::engine::render::UiDrawFrame& frame) {
ui_draw_frame_ = frame;
if (!frame.commands.empty() || frame.total_command_count > 0) {
++submitted_ui_frame_count_;
}
}
void PandaRenderBackend::bind_window_framework(const std::uintptr_t window_framework_handle) {
#if METACORE_HAS_PANDA3D
if (!native_state_) {
native_state_ = std::make_unique<NativeState>();
}
WindowFramework* window = reinterpret_cast<WindowFramework*>(window_framework_handle);
if (window == nullptr || native_state_->window == window) {
return;
}
native_state_->window = window;
native_state_->cube_root = create_wire_cube(native_state_->window->get_render());
native_state_->gizmo_root = create_gizmo(native_state_->window->get_render());
native_state_->hud_root = create_editor_chrome(native_state_->window->get_aspect_2d());
native_state_->hud_root.set_bin("fixed", 0);
native_state_->hud_root.set_depth_test(false);
native_state_->hud_root.set_depth_write(false);
#else
(void)window_framework_handle;
#endif
}
void PandaRenderBackend::bind_primary_display_region(const std::uintptr_t display_region_handle) {
#if METACORE_HAS_PANDA3D
if (!native_state_) {
native_state_ = std::make_unique<NativeState>();
}
DisplayRegion* region = reinterpret_cast<DisplayRegion*>(display_region_handle);
if (native_state_->bound_display_region == region && native_state_->draw_callback_bound) {
return;
}
if (native_state_->bound_display_region != nullptr) {
native_state_->bound_display_region->clear_draw_callback();
native_state_->bound_display_region = nullptr;
native_state_->draw_callback.clear();
native_state_->draw_callback_bound = false;
}
if (region != nullptr) {
native_state_->draw_callback = new UiDrawCallback(&native_state_->observed_draw_callback_count);
region->set_draw_callback(native_state_->draw_callback);
native_state_->bound_display_region = region;
native_state_->draw_callback_bound = true;
}
#else
(void)display_region_handle;
#endif
}
void PandaRenderBackend::set_demo_selection_transform(
const bool has_selection,
const std::array<float, 3> position,
const std::array<float, 3> rotation,
const std::array<float, 3> scale
) {
#if METACORE_HAS_PANDA3D
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_selection_transform enter\n";
if (!native_state_ || native_state_->window == nullptr) {
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_selection_transform early return\n";
return;
}
if (has_selection) {
native_state_->cube_root.set_pos(
position[0],
position[1],
position[2]
);
native_state_->cube_root.set_hpr(
rotation[0],
rotation[1],
rotation[2]
);
native_state_->cube_root.set_scale(
scale[0],
scale[1],
scale[2]
);
native_state_->gizmo_root.set_pos(
position[0],
position[1],
position[2]
);
native_state_->gizmo_root.show();
} else {
native_state_->gizmo_root.hide();
}
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_selection_transform exit\n";
#else
(void)has_selection;
(void)position;
(void)rotation;
(void)scale;
#endif
}
void PandaRenderBackend::set_demo_hud_text(const std::string& text) {
#if METACORE_HAS_PANDA3D
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_hud_text enter\n";
if (!native_state_ || native_state_->window == nullptr || native_state_->hud_root.is_empty()) {
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_hud_text early return\n";
return;
}
const bool snap_enabled = text.find("Snap On") != std::string::npos;
const bool rotate_mode = text.find("Gizmo: Rotate") != std::string::npos;
const bool scale_mode = text.find("Gizmo: Scale") != std::string::npos;
if (scale_mode) {
native_state_->hud_root.set_color(0.95F, 0.8F, 0.2F, 1.0F);
} else if (rotate_mode) {
native_state_->hud_root.set_color(0.95F, 0.45F, 0.25F, 1.0F);
} else if (snap_enabled) {
native_state_->hud_root.set_color(0.45F, 0.95F, 0.45F, 1.0F);
} else {
native_state_->hud_root.set_color(0.15F, 0.85F, 0.95F, 0.95F);
}
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_hud_text exit\n";
#else
(void)text;
#endif
}
void PandaRenderBackend::set_demo_camera(
const std::array<float, 3> camera_position,
const std::array<float, 3> focus_position
) {
#if METACORE_HAS_PANDA3D
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_camera enter\n";
if (!native_state_ || native_state_->window == nullptr) {
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_camera early return\n";
return;
}
NodePath camera = native_state_->window->get_camera_group();
camera.set_pos(
camera_position[0],
camera_position[1],
camera_position[2]
);
camera.look_at(
focus_position[0],
focus_position[1],
focus_position[2]
);
std::cerr << "[MetaCore][Trace] PandaRenderBackend::set_demo_camera exit\n";
#else
(void)camera_position;
(void)focus_position;
#endif
}
const metacore::engine::render::FrameUniforms& PandaRenderBackend::frame_uniforms() const {
return frame_uniforms_;
}
@ -27,4 +331,20 @@ const metacore::engine::render::LightUniforms& PandaRenderBackend::light_uniform
return light_uniforms_;
}
const metacore::engine::render::UiDrawFrame& PandaRenderBackend::ui_draw_frame() const {
return ui_draw_frame_;
}
int PandaRenderBackend::submitted_ui_frame_count() const {
return submitted_ui_frame_count_;
}
int PandaRenderBackend::observed_draw_callback_count() const {
return native_state_ ? native_state_->observed_draw_callback_count : 0;
}
bool PandaRenderBackend::draw_callback_bound() const {
return native_state_ && native_state_->draw_callback_bound;
}
} // namespace metacore::platform::panda

View File

@ -1,23 +1,49 @@
#pragma once
#include "MetaCore/engine/render/RenderTypes.h"
#include <memory>
#include <array>
#include <string>
namespace metacore::platform::panda {
class PandaRenderBackend final : public metacore::engine::render::IMetaRenderBackend {
public:
PandaRenderBackend();
~PandaRenderBackend() override;
bool initialize() override;
void shutdown() override;
void set_frame_uniforms(const metacore::engine::render::FrameUniforms& uniforms) override;
void set_light_uniforms(const metacore::engine::render::LightUniforms& uniforms) override;
void set_ui_draw_frame(const metacore::engine::render::UiDrawFrame& frame) override;
void bind_window_framework(std::uintptr_t window_framework_handle);
void bind_primary_display_region(std::uintptr_t display_region_handle);
void set_demo_camera(std::array<float, 3> camera_position, std::array<float, 3> focus_position);
void set_demo_selection_transform(
bool has_selection,
std::array<float, 3> position,
std::array<float, 3> rotation,
std::array<float, 3> scale
);
void set_demo_hud_text(const std::string& text);
[[nodiscard]] const metacore::engine::render::FrameUniforms& frame_uniforms() const;
[[nodiscard]] const metacore::engine::render::LightUniforms& light_uniforms() const;
[[nodiscard]] const metacore::engine::render::UiDrawFrame& ui_draw_frame() const;
[[nodiscard]] int submitted_ui_frame_count() const;
[[nodiscard]] int observed_draw_callback_count() const;
[[nodiscard]] bool draw_callback_bound() const;
private:
struct NativeState;
bool initialized_ = false;
metacore::engine::render::FrameUniforms frame_uniforms_{};
metacore::engine::render::LightUniforms light_uniforms_{};
metacore::engine::render::UiDrawFrame ui_draw_frame_{};
int submitted_ui_frame_count_ = 0;
std::unique_ptr<NativeState> native_state_{};
};
} // namespace metacore::platform::panda

View File

@ -2,9 +2,13 @@
#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
@ -15,11 +19,23 @@
#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") {
@ -92,38 +108,73 @@ std::array<float, 16> make_projection_matrix(float width, float height) {
} // 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";
#if METACORE_HAS_IMGUI
if (headless_frame_enabled_) {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.DisplaySize = ImVec2(1600.0F, 900.0F);
io.DeltaTime = 1.0F / 60.0F;
unsigned char* font_pixels = nullptr;
int font_width = 0;
int font_height = 0;
io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height);
#if METACORE_HAS_IMGUIZMO
ImGuizmo::SetImGuiContext(ImGui::GetCurrentContext());
#endif
ImGui::StyleColorsDark();
}
#endif
initialize_imgui_context();
initialized_ = true;
return true;
}
void DearImGuiEditorShell::shutdown() {
void DearImGuiEditorShell::initialize_imgui_context() {
#if METACORE_HAS_IMGUI
if (headless_frame_enabled_ && ImGui::GetCurrentContext() != nullptr) {
ImGui::DestroyContext();
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;
@ -132,30 +183,38 @@ void DearImGuiEditorShell::shutdown() {
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_ && ImGui::GetCurrentContext() != nullptr) {
render_headless_frame(snapshot);
if (const ImDrawData* draw_data = ImGui::GetDrawData(); draw_data != nullptr) {
last_draw_command_count_ = 0;
last_vertex_count_ = draw_data->TotalVtxCount;
last_index_count_ = draw_data->TotalIdxCount;
last_window_count_ = draw_data->CmdListsCount;
for (int index = 0; index < draw_data->CmdListsCount; ++index) {
last_draw_command_count_ += draw_data->CmdLists[index]->CmdBuffer.Size;
}
} else {
last_draw_command_count_ = 0;
last_vertex_count_ = 0;
last_index_count_ = 0;
last_window_count_ = 0;
}
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;
@ -167,6 +226,119 @@ void DearImGuiEditorShell::consume_snapshot(const EditorFrameSnapshot& snapshot)
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;
@ -194,20 +366,52 @@ void DearImGuiEditorShell::enqueue_headless_transform_edit(const EditorFrameSnap
headless_transform_override_consumed_ = true;
}
void DearImGuiEditorShell::render_headless_frame(const EditorFrameSnapshot& snapshot) {
void DearImGuiEditorShell::render_imgui_frame(const EditorFrameSnapshot& snapshot) {
#if METACORE_HAS_IMGUI
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize = ImVec2(1600.0F, 900.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
);
}
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();
@ -435,6 +639,41 @@ void DearImGuiEditorShell::render_viewport_panel(const EditorFrameSnapshot& snap
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());
@ -511,6 +750,40 @@ void DearImGuiEditorShell::render_viewport_panel(const EditorFrameSnapshot& snap
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
@ -620,6 +893,12 @@ PendingShellEdits DearImGuiEditorShell::consume_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);
}

View File

@ -3,6 +3,9 @@
#include "MetaEditor/IEditorShell.h"
#include <filesystem>
#include <cstdint>
#include <windows.h>
namespace metacore::editor {
@ -18,12 +21,20 @@ public:
[[nodiscard]] std::string describe_workspace() const override;
[[nodiscard]] std::string shell_name() const override;
[[nodiscard]] PendingShellEdits consume_pending_edits() override;
[[nodiscard]] metacore::engine::render::UiDrawFrame consume_render_frame() override;
void configure_platform_window(std::uintptr_t native_window_handle, int width, int height);
void refresh_platform_window_metrics(int width, int height);
void set_debug_export_path(std::filesystem::path path);
private:
friend LRESULT CALLBACK metacore_imgui_shell_wnd_proc(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param);
void initialize_imgui_context();
void shutdown_imgui_context();
void enqueue_headless_gizmo_state_edit(const EditorFrameSnapshot& snapshot);
void enqueue_headless_transform_edit(const EditorFrameSnapshot& snapshot);
void render_headless_frame(const EditorFrameSnapshot& snapshot);
void render_imgui_frame(const EditorFrameSnapshot& snapshot);
void render_dockspace_frame(const EditorFrameSnapshot& snapshot);
void ensure_default_dock_layout(const EditorFrameSnapshot& snapshot, unsigned int dockspace_id);
void render_toolbar(const EditorFrameSnapshot& snapshot);
@ -36,6 +47,8 @@ private:
void render_runtime_preview_panel(const EditorFrameSnapshot& snapshot);
void export_debug_snapshot() const;
void export_viewport_snapshot(std::ostringstream& stream) const;
void capture_render_frame();
void fulfill_texture_requests();
bool initialized_ = false;
EditorFrameSnapshot last_snapshot_{};
@ -46,8 +59,17 @@ private:
int last_window_count_ = 0;
bool headless_frame_enabled_ = false;
bool dock_layout_initialized_ = false;
bool headless_gizmo_override_consumed_ = false;
bool headless_transform_override_consumed_ = false;
bool imgui_context_owned_ = false;
bool win32_backend_enabled_ = false;
HWND native_window_handle_ = nullptr;
int native_window_width_ = 0;
int native_window_height_ = 0;
WNDPROC previous_wnd_proc_ = nullptr;
PendingShellEdits pending_edits_{};
metacore::engine::render::UiDrawFrame pending_render_frame_{};
std::uintptr_t next_mock_texture_id_ = 1;
};
} // namespace metacore::editor

View File

@ -5,21 +5,128 @@
#include "MetaCore/foundation/Log.h"
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <windows.h>
namespace metacore::editor {
namespace {
using TransformGizmoService = metacore::engine::editor_core::TransformGizmoService;
std::optional<TransformGizmoService::Operation> parse_operation(const std::string& operation) {
if (operation == "TRANSLATE") {
return TransformGizmoService::Operation::Translate;
}
if (operation == "ROTATE") {
return TransformGizmoService::Operation::Rotate;
}
if (operation == "SCALE") {
return TransformGizmoService::Operation::Scale;
}
return std::nullopt;
}
std::optional<TransformGizmoService::Space> parse_space(const std::string& space) {
if (space == "LOCAL") {
return TransformGizmoService::Space::Local;
}
if (space == "WORLD") {
return TransformGizmoService::Space::World;
}
return std::nullopt;
}
std::string operation_label(const TransformGizmoService::Operation operation) {
switch (operation) {
case TransformGizmoService::Operation::Translate:
return "Translate";
case TransformGizmoService::Operation::Rotate:
return "Rotate";
case TransformGizmoService::Operation::Scale:
return "Scale";
}
return "Translate";
}
std::string space_label(const TransformGizmoService::Space space) {
return space == TransformGizmoService::Space::World ? "World" : "Local";
}
int parse_positive_int_env(const char* value, const int fallback) {
if (value == nullptr) {
return fallback;
}
try {
const int parsed = std::stoi(value);
return parsed > 0 ? parsed : fallback;
} catch (...) {
return fallback;
}
}
bool has_explicit_env(const char* value) {
return value != nullptr && value[0] != '\0';
}
bool env_enabled(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && std::string(value) == "1";
}
std::string build_panda_hud_text(const EditorFrameSnapshot& snapshot, const int ui_frame_submissions) {
std::ostringstream stream;
stream << "MetaEditor\n";
stream << "Selection: "
<< (snapshot.viewport.has_selection ? snapshot.viewport.selected_object_name : "None") << "\n";
stream << "Gizmo: " << snapshot.viewport.gizmo_operation
<< " / " << snapshot.viewport.gizmo_space
<< " / Snap " << (snapshot.viewport.snap_enabled ? "On" : "Off") << "\n";
stream << "Position: "
<< snapshot.viewport.object_position[0] << ", "
<< snapshot.viewport.object_position[1] << ", "
<< snapshot.viewport.object_position[2] << "\n";
stream << "Panels: Hierarchy | Viewport | Inspector | Console\n";
stream << "Controls: W/E/R mode, Q space, Shift+S snap\n";
stream << "Move: Arrows + PageUp/PageDown | Rotate: Arrows | Scale: +/-\n";
stream << "UI Packets: " << ui_frame_submissions;
return stream.str();
}
} // namespace
EditorApplication::EditorApplication() = default;
bool EditorApplication::initialize(const std::filesystem::path& project_root) {
std::cerr << "[MetaCore][Trace] EditorApplication::initialize enter\n";
project_root_ = project_root;
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after project_root\n";
editor_shell_ = create_default_editor_shell();
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after shell create\n";
const auto shell_debug_path = project_root_ / "Library" / "Layout" / (editor_shell_->shell_name() + std::string("_snapshot.txt"));
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after shell debug path\n";
editor_shell_->set_debug_export_path(shell_debug_path);
std::cerr << "[MetaCore][Trace] EditorApplication::initialize before platform\n";
if (!platform_.initialize("MetaEditor")) {
return false;
}
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after platform\n";
render_backend_.initialize();
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after render backend\n";
render_backend_.bind_window_framework(platform_.window_framework_handle());
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after callback bind gate\n";
if (auto* dearimgui_shell = dynamic_cast<DearImGuiEditorShell*>(editor_shell_.get())) {
dearimgui_shell->configure_platform_window(
platform_.native_window_handle(),
platform_.window_width(),
platform_.window_height()
);
}
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after shell window configure\n";
editor_shell_->initialize();
std::cerr << "[MetaCore][Trace] EditorApplication::initialize after shell initialize\n";
if (!std::filesystem::exists(project_root_ / "MetaCore.project.json")) {
project_service_.create_project(project_root_, "MetaCoreSample");
@ -28,6 +135,7 @@ bool EditorApplication::initialize(const std::filesystem::path& project_root) {
project_service_.load_project(project_root_ / "MetaCore.project.json", project_);
scene_ = scene_service_.create_default_scene();
scene_serializer_.load_scene(project_root_ / "Scenes" / "Main.mcscene.json", scene_);
ensure_demo_scene_content();
asset_database_.load(project_root_ / "Library" / "AssetDB.json");
layout_service_.load(project_root_ / "Library" / "Layout" / "editor_layout.json", layout_state_);
session_.apply_layout_state(layout_state_);
@ -35,6 +143,7 @@ bool EditorApplication::initialize(const std::filesystem::path& project_root) {
if (!scene_.objects.empty()) {
selection_service_.select(scene_.objects.front().id);
}
ensure_demo_scene_content();
session_.set_status_text("Editor initialized");
session_.add_console_message("System", "MetaEditor initialized");
@ -56,85 +165,87 @@ bool EditorApplication::initialize(const std::filesystem::path& project_root) {
"Panda window bootstrap: enabled=" + std::string(platform_.window_enabled() ? "true" : "false") +
" opened=" + std::string(platform_.window_opened() ? "true" : "false")
);
if (platform_.window_opened()) {
foundation::log(
foundation::LogLevel::Info,
"Panda native window: hwnd=0x" +
[&]() {
std::ostringstream stream;
stream << std::hex << platform_.native_window_handle();
return stream.str();
}() +
" size=" + std::to_string(platform_.window_width()) + "x" + std::to_string(platform_.window_height())
);
}
return true;
}
int EditorApplication::run() {
std::cerr << "[MetaCore][Trace] EditorApplication::run enter\n";
foundation::log(foundation::LogLevel::Info, "MetaEditor skeleton run loop started");
const bool has_runtime_ui_object = std::any_of(
scene_.objects.begin(),
scene_.objects.end(),
[](const metacore::engine::scene::SceneObject& object) {
return object.has_ui_document;
}
);
if (!has_runtime_ui_object) {
metacore::engine::editor_core::SceneCommands::create_empty_object(
command_service_,
session_,
selection_service_,
scene_service_,
scene_,
"UI Root"
);
if (selection_service_.selection().has_value()) {
if (const auto object = scene_service_.find_object(scene_, *selection_service_.selection())) {
object->get().has_ui_document = true;
object->get().ui_document.document_path = "Assets/UI/main_menu.rml";
object->get().ui_document.stylesheet_path = "Assets/UI/main_menu.rcss";
}
}
}
metacore::engine::editor_core::SceneCommands::focus_selected_object(
session_,
selection_service_,
viewport_service_
);
std::cerr << "[MetaCore][Trace] EditorApplication::run after start log\n";
ensure_runtime_preview_root();
std::cerr << "[MetaCore][Trace] EditorApplication::run after runtime preview root\n";
metacore::engine::editor_core::SceneCommands::focus_selected_object(session_, selection_service_, viewport_service_);
std::cerr << "[MetaCore][Trace] EditorApplication::run after focus selected\n";
if (const auto selected = inspector_service_.current_selection(scene_, selection_service_)) {
foundation::log(foundation::LogLevel::Info, "Current selection: " + selected->get().name);
}
auto snapshot = presenter_.build_snapshot(scene_, asset_database_, selection_service_, gizmo_service_, viewport_service_, session_);
editor_shell_->consume_snapshot(snapshot);
const PendingShellEdits shell_edits = editor_shell_->consume_pending_edits();
if (shell_edits.transform_edit.has_value() &&
shell_edits.transform_edit->entity_id == snapshot.viewport.selected_object_id) {
const auto requested_transform = metacore::engine::scene::TransformComponent{
shell_edits.transform_edit->position,
shell_edits.transform_edit->rotation,
shell_edits.transform_edit->scale
};
metacore::engine::editor_core::SceneCommands::apply_transform_to_selected_object(
command_service_,
session_,
selection_service_,
scene_service_,
scene_,
requested_transform
);
snapshot = presenter_.build_snapshot(scene_, asset_database_, selection_service_, gizmo_service_, viewport_service_, session_);
const int frame_budget = resolve_frame_budget();
std::cerr << "[MetaCore][Trace] EditorApplication::run after frame budget\n";
for (int frame_index = 0; should_continue_running(frame_index, frame_budget); ++frame_index) {
std::cerr << "[MetaCore][Trace] EditorApplication::run frame begin\n";
if (env_enabled("METACORE_ENABLE_PANDA_UI_DRAW_CALLBACK") && !render_backend_.draw_callback_bound() && frame_index > 0) {
render_backend_.bind_primary_display_region(platform_.primary_display_region_handle());
foundation::log(foundation::LogLevel::Info, "Panda UI draw callback binding requested");
}
poll_window_shortcuts();
std::cerr << "[MetaCore][Trace] EditorApplication::run after shortcuts\n";
if (auto* dearimgui_shell = dynamic_cast<DearImGuiEditorShell*>(editor_shell_.get())) {
dearimgui_shell->refresh_platform_window_metrics(platform_.window_width(), platform_.window_height());
}
auto snapshot = presenter_.build_snapshot(scene_, asset_database_, selection_service_, gizmo_service_, viewport_service_, session_);
std::cerr << "[MetaCore][Trace] EditorApplication::run after snapshot A\n";
editor_shell_->consume_snapshot(snapshot);
std::cerr << "[MetaCore][Trace] EditorApplication::run after shell consume A\n";
render_backend_.set_ui_draw_frame(editor_shell_->consume_render_frame());
std::cerr << "[MetaCore][Trace] EditorApplication::run after ui frame A\n";
render_backend_.set_demo_selection_transform(
snapshot.viewport.has_selection,
snapshot.viewport.object_position,
snapshot.viewport.object_rotation,
snapshot.viewport.object_scale
);
render_backend_.set_demo_camera(snapshot.viewport.camera_position, snapshot.viewport.object_position);
render_backend_.set_demo_hud_text(build_panda_hud_text(snapshot, render_backend_.submitted_ui_frame_count()));
std::cerr << "[MetaCore][Trace] EditorApplication::run after backend demo sync A\n";
last_snapshot_ = snapshot;
apply_shell_edits();
std::cerr << "[MetaCore][Trace] EditorApplication::run after shell edits\n";
snapshot = presenter_.build_snapshot(scene_, asset_database_, selection_service_, gizmo_service_, viewport_service_, session_);
std::cerr << "[MetaCore][Trace] EditorApplication::run after snapshot B\n";
editor_shell_->consume_snapshot(snapshot);
std::cerr << "[MetaCore][Trace] EditorApplication::run after shell consume B\n";
render_backend_.set_ui_draw_frame(editor_shell_->consume_render_frame());
std::cerr << "[MetaCore][Trace] EditorApplication::run after ui frame B\n";
render_backend_.set_demo_selection_transform(
snapshot.viewport.has_selection,
snapshot.viewport.object_position,
snapshot.viewport.object_rotation,
snapshot.viewport.object_scale
);
render_backend_.set_demo_camera(snapshot.viewport.camera_position, snapshot.viewport.object_position);
render_backend_.set_demo_hud_text(build_panda_hud_text(snapshot, render_backend_.submitted_ui_frame_count()));
std::cerr << "[MetaCore][Trace] EditorApplication::run after backend demo sync B\n";
last_snapshot_ = snapshot;
platform_.pump_frame();
std::cerr << "[MetaCore][Trace] EditorApplication::run after pump\n";
}
layout_service_.save(project_root_ / "Library" / "Layout" / "editor_layout.json", session_.to_layout_state());
scene_serializer_.save_scene(project_root_ / "Scenes" / "Main.mcscene.json", scene_);
asset_database_.save(project_root_ / "Library" / "AssetDB.json");
platform_.pump_frame();
foundation::log(foundation::LogLevel::Info, "Hierarchy items: " + std::to_string(snapshot.hierarchy.size()));
foundation::log(foundation::LogLevel::Info, "Project assets: " + std::to_string(snapshot.assets.size()));
foundation::log(foundation::LogLevel::Info, "Inspector target: " + snapshot.inspector.object_name);
foundation::log(foundation::LogLevel::Info, "Scene object count: " + std::to_string(snapshot.scene_object_count));
foundation::log(foundation::LogLevel::Info, "Session status: " + snapshot.status_text);
foundation::log(foundation::LogLevel::Info, "Editor shell: " + editor_shell_->shell_name());
foundation::log(foundation::LogLevel::Info, "Editor shell summary: " + editor_shell_->summarize_snapshot());
foundation::log(foundation::LogLevel::Info, editor_shell_->describe_workspace());
for (const auto& message : snapshot.console) {
foundation::log(foundation::LogLevel::Info, "[" + message.category + "] " + message.text);
persist_editor_state();
if (last_snapshot_.has_value()) {
log_frame_summary(*last_snapshot_);
}
return 0;
@ -149,4 +260,327 @@ void EditorApplication::shutdown() {
foundation::log(foundation::LogLevel::Info, "MetaEditor shutdown complete");
}
void EditorApplication::ensure_runtime_preview_root() {
const bool has_runtime_ui_object = std::any_of(
scene_.objects.begin(),
scene_.objects.end(),
[](const metacore::engine::scene::SceneObject& object) {
return object.has_ui_document;
}
);
if (has_runtime_ui_object) {
return;
}
metacore::engine::editor_core::SceneCommands::create_empty_object(
command_service_,
session_,
selection_service_,
scene_service_,
scene_,
"UI Root"
);
if (selection_service_.selection().has_value()) {
if (const auto object = scene_service_.find_object(scene_, *selection_service_.selection())) {
object->get().has_ui_document = true;
object->get().ui_document.document_path = "Assets/UI/main_menu.rml";
object->get().ui_document.stylesheet_path = "Assets/UI/main_menu.rcss";
}
}
}
void EditorApplication::ensure_demo_scene_content() {
const auto has_named_object = [&](const std::string& name) {
return std::any_of(scene_.objects.begin(), scene_.objects.end(), [&](const auto& object) {
return object.name == name;
});
};
const auto has_primary_camera = std::any_of(scene_.objects.begin(), scene_.objects.end(), [](const auto& object) {
return object.has_camera && object.camera.primary;
});
const auto has_light = std::any_of(scene_.objects.begin(), scene_.objects.end(), [](const auto& object) {
return object.has_light;
});
const auto has_mesh = std::any_of(scene_.objects.begin(), scene_.objects.end(), [](const auto& object) {
return object.has_mesh_renderer;
});
if (!has_primary_camera) {
auto& camera = scene_service_.create_object(scene_, "Main Camera");
camera.has_camera = true;
camera.camera.primary = true;
camera.transform.position = {0.0F, -10.0F, 3.0F};
}
if (!has_light) {
auto& light = scene_service_.create_object(scene_, "Directional Light");
light.has_light = true;
light.light.type = metacore::engine::scene::LightComponent::Type::Directional;
}
if (!has_mesh) {
auto& cube = scene_service_.create_object(scene_, "Demo Cube");
cube.has_mesh_renderer = true;
cube.mesh_renderer.mesh_asset_id = "builtin://wire-cube";
cube.mesh_renderer.material_asset_id = "builtin://editor-default";
cube.transform.position = {0.0F, 0.0F, 0.0F};
cube.transform.scale = {1.0F, 1.0F, 1.0F};
}
if (!has_named_object("UI Root")) {
auto& ui_root = scene_service_.create_object(scene_, "UI Root");
ui_root.has_ui_document = true;
ui_root.ui_document.document_path = "Assets/UI/main_menu.rml";
ui_root.ui_document.stylesheet_path = "Assets/UI/main_menu.rcss";
}
for (const auto& object : scene_.objects) {
if (object.has_mesh_renderer) {
selection_service_.select(object.id);
viewport_service_.focus_selection(object.id);
viewport_service_.set_camera_position({0.0F, -10.0F, 3.0F});
break;
}
}
}
void EditorApplication::apply_shell_edits() {
for (int pass = 0; pass < 2; ++pass) {
const PendingShellEdits shell_edits = editor_shell_->consume_pending_edits();
bool changed = false;
if (shell_edits.gizmo_state_edit.has_value()) {
const auto& edit = *shell_edits.gizmo_state_edit;
if (edit.has_operation) {
if (const auto operation = parse_operation(edit.operation)) {
gizmo_service_.set_operation(*operation);
session_.set_status_text("Gizmo operation: " + operation_label(*operation));
session_.add_console_message("Viewport", "Gizmo operation set to " + operation_label(*operation));
changed = true;
}
}
if (edit.has_space) {
if (const auto space = parse_space(edit.space)) {
gizmo_service_.set_space(*space);
session_.set_status_text("Gizmo space: " + space_label(*space));
session_.add_console_message("Viewport", "Gizmo space set to " + space_label(*space));
changed = true;
}
}
if (edit.has_snap_enabled) {
gizmo_service_.set_snap_enabled(edit.snap_enabled);
session_.set_status_text(std::string("Gizmo snap: ") + (edit.snap_enabled ? "On" : "Off"));
session_.add_console_message("Viewport", std::string("Gizmo snap ") + (edit.snap_enabled ? "enabled" : "disabled"));
changed = true;
}
if (edit.has_snap_values) {
gizmo_service_.set_snap_values(edit.snap_values);
session_.add_console_message(
"Viewport",
"Gizmo snap values updated to " +
std::to_string(edit.snap_values[0]) + ", " +
std::to_string(edit.snap_values[1]) + ", " +
std::to_string(edit.snap_values[2])
);
changed = true;
}
}
if (last_snapshot_.has_value() &&
shell_edits.transform_edit.has_value() &&
shell_edits.transform_edit->entity_id == last_snapshot_->viewport.selected_object_id) {
const auto requested_transform = metacore::engine::scene::TransformComponent{
shell_edits.transform_edit->position,
shell_edits.transform_edit->rotation,
shell_edits.transform_edit->scale
};
metacore::engine::editor_core::SceneCommands::apply_transform_to_selected_object(
command_service_,
session_,
selection_service_,
scene_service_,
scene_,
requested_transform
);
changed = true;
}
if (!changed) {
break;
}
}
}
void EditorApplication::poll_window_shortcuts() {
if (!platform_.window_opened()) {
return;
}
const auto pressed = [&](const int virtual_key) {
const bool is_down = (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
const bool was_down = key_down_[virtual_key];
key_down_[virtual_key] = is_down;
return is_down && !was_down;
};
const auto down = [&](const int virtual_key) {
const bool is_down = (GetAsyncKeyState(virtual_key) & 0x8000) != 0;
key_down_[virtual_key] = is_down;
return is_down;
};
if (pressed('W')) {
gizmo_service_.set_operation(TransformGizmoService::Operation::Translate);
}
if (pressed('E')) {
gizmo_service_.set_operation(TransformGizmoService::Operation::Rotate);
}
if (pressed('R')) {
gizmo_service_.set_operation(TransformGizmoService::Operation::Scale);
}
if (pressed('Q')) {
gizmo_service_.set_space(
gizmo_service_.space() == TransformGizmoService::Space::Local
? TransformGizmoService::Space::World
: TransformGizmoService::Space::Local
);
}
if (down(VK_SHIFT) && pressed('S')) {
gizmo_service_.set_snap_enabled(!gizmo_service_.snap_enabled());
}
const auto selected_id = selection_service_.selection();
if (!selected_id.has_value()) {
return;
}
const auto selected_object = scene_service_.find_object(scene_, *selected_id);
if (!selected_object.has_value()) {
return;
}
auto transform = selected_object->get().transform;
bool changed = false;
const auto step = gizmo_service_.snap_enabled() ? gizmo_service_.snap_values()[0] : 0.1F;
const auto rotate_step = gizmo_service_.snap_enabled() ? gizmo_service_.snap_values()[0] : 5.0F;
const auto scale_step = gizmo_service_.snap_enabled() ? 0.25F : 0.05F;
switch (gizmo_service_.operation()) {
case TransformGizmoService::Operation::Translate:
if (down(VK_LEFT)) {
transform.position[0] -= step;
changed = true;
}
if (down(VK_RIGHT)) {
transform.position[0] += step;
changed = true;
}
if (down(VK_UP)) {
transform.position[1] += step;
changed = true;
}
if (down(VK_DOWN)) {
transform.position[1] -= step;
changed = true;
}
if (down(VK_PRIOR)) {
transform.position[2] += step;
changed = true;
}
if (down(VK_NEXT)) {
transform.position[2] -= step;
changed = true;
}
break;
case TransformGizmoService::Operation::Rotate:
if (down(VK_LEFT)) {
transform.rotation[2] -= rotate_step;
changed = true;
}
if (down(VK_RIGHT)) {
transform.rotation[2] += rotate_step;
changed = true;
}
if (down(VK_UP)) {
transform.rotation[0] += rotate_step;
changed = true;
}
if (down(VK_DOWN)) {
transform.rotation[0] -= rotate_step;
changed = true;
}
break;
case TransformGizmoService::Operation::Scale:
if (down(VK_OEM_PLUS) || down(VK_ADD)) {
for (float& axis : transform.scale) {
axis += scale_step;
}
changed = true;
}
if (down(VK_OEM_MINUS) || down(VK_SUBTRACT)) {
for (float& axis : transform.scale) {
axis = (std::max)(0.1F, axis - scale_step);
}
changed = true;
}
break;
}
if (changed) {
metacore::engine::editor_core::SceneCommands::apply_transform_to_selected_object(
command_service_,
session_,
selection_service_,
scene_service_,
scene_,
transform
);
}
}
void EditorApplication::log_frame_summary(const EditorFrameSnapshot& snapshot) const {
foundation::log(foundation::LogLevel::Info, "Hierarchy items: " + std::to_string(snapshot.hierarchy.size()));
foundation::log(foundation::LogLevel::Info, "Project assets: " + std::to_string(snapshot.assets.size()));
foundation::log(foundation::LogLevel::Info, "Inspector target: " + snapshot.inspector.object_name);
foundation::log(foundation::LogLevel::Info, "Scene object count: " + std::to_string(snapshot.scene_object_count));
foundation::log(foundation::LogLevel::Info, "Session status: " + snapshot.status_text);
foundation::log(foundation::LogLevel::Info, "Editor shell: " + editor_shell_->shell_name());
foundation::log(foundation::LogLevel::Info, "Editor shell summary: " + editor_shell_->summarize_snapshot());
foundation::log(foundation::LogLevel::Info, "Platform frames: " + std::to_string(platform_.frame_count()));
foundation::log(
foundation::LogLevel::Info,
"UI render handoff: submitted=" + std::to_string(render_backend_.submitted_ui_frame_count()) +
" callbackBound=" + std::string(render_backend_.draw_callback_bound() ? "true" : "false") +
" drawCallbacks=" + std::to_string(render_backend_.observed_draw_callback_count())
);
foundation::log(foundation::LogLevel::Info, editor_shell_->describe_workspace());
for (const auto& message : snapshot.console) {
foundation::log(foundation::LogLevel::Info, "[" + message.category + "] " + message.text);
}
}
int EditorApplication::resolve_frame_budget() const {
const char* frame_budget_env = std::getenv("METACORE_EDITOR_FRAME_COUNT");
if (platform_.window_opened()) {
if (!has_explicit_env(frame_budget_env)) {
return -1;
}
return parse_positive_int_env(frame_budget_env, -1);
}
return parse_positive_int_env(frame_budget_env, 1);
}
bool EditorApplication::should_continue_running(const int frame_index, const int frame_budget) const {
if (platform_.should_close()) {
return false;
}
if (frame_budget < 0) {
return true;
}
return frame_index < frame_budget;
}
void EditorApplication::persist_editor_state() const {
layout_service_.save(project_root_ / "Library" / "Layout" / "editor_layout.json", session_.to_layout_state());
scene_serializer_.save_scene(project_root_ / "Scenes" / "Main.mcscene.json", scene_);
asset_database_.save(project_root_ / "Library" / "AssetDB.json");
}
} // namespace metacore::editor

View File

@ -21,6 +21,8 @@
#include <filesystem>
#include <memory>
#include <optional>
#include <array>
namespace metacore::editor {
@ -33,6 +35,15 @@ public:
void shutdown();
private:
void ensure_runtime_preview_root();
void ensure_demo_scene_content();
void apply_shell_edits();
void poll_window_shortcuts();
void log_frame_summary(const EditorFrameSnapshot& snapshot) const;
[[nodiscard]] int resolve_frame_budget() const;
[[nodiscard]] bool should_continue_running(int frame_index, int frame_budget) const;
void persist_editor_state() const;
std::filesystem::path project_root_;
metacore::platform::panda::PandaPlatformContext platform_;
metacore::platform::panda::PandaRenderBackend render_backend_;
@ -51,8 +62,10 @@ private:
metacore::engine::editor_core::EditorLayoutState layout_state_{};
metacore::engine::scene::Project project_{};
metacore::engine::scene::Scene scene_{};
std::optional<EditorFrameSnapshot> last_snapshot_{};
EditorPresenter presenter_{};
std::unique_ptr<IEditorShell> editor_shell_{};
std::array<bool, 256> key_down_{};
};
} // namespace metacore::editor

View File

@ -1,6 +1,7 @@
#pragma once
#include "MetaEditor/EditorPresenter.h"
#include "MetaCore/engine/render/RenderTypes.h"
#include <array>
#include <filesystem>
@ -11,6 +12,17 @@
namespace metacore::editor {
struct PendingShellEdits {
struct GizmoStateEdit {
std::string operation;
std::string space;
std::array<float, 3> snap_values{1.0F, 1.0F, 1.0F};
bool has_operation = false;
bool has_space = false;
bool has_snap_enabled = false;
bool snap_enabled = false;
bool has_snap_values = false;
};
struct TransformEdit {
std::string entity_id;
std::array<float, 3> position{0.0F, 0.0F, 0.0F};
@ -18,6 +30,7 @@ struct PendingShellEdits {
std::array<float, 3> scale{1.0F, 1.0F, 1.0F};
};
std::optional<GizmoStateEdit> gizmo_state_edit;
std::optional<TransformEdit> transform_edit;
};
@ -35,6 +48,7 @@ public:
[[nodiscard]] virtual std::string describe_workspace() const = 0;
[[nodiscard]] virtual std::string shell_name() const = 0;
[[nodiscard]] virtual PendingShellEdits consume_pending_edits() = 0;
[[nodiscard]] virtual metacore::engine::render::UiDrawFrame consume_render_frame() = 0;
virtual void set_debug_export_path(std::filesystem::path path) = 0;
};

View File

@ -94,6 +94,10 @@ PendingShellEdits NullEditorShell::consume_pending_edits() {
return {};
}
metacore::engine::render::UiDrawFrame NullEditorShell::consume_render_frame() {
return {};
}
void NullEditorShell::set_debug_export_path(std::filesystem::path path) {
debug_export_path_ = std::move(path);
}

View File

@ -16,6 +16,7 @@ public:
[[nodiscard]] std::string describe_workspace() const override;
[[nodiscard]] std::string shell_name() const override;
[[nodiscard]] PendingShellEdits consume_pending_edits() override;
[[nodiscard]] metacore::engine::render::UiDrawFrame consume_render_frame() override;
void set_debug_export_path(std::filesystem::path path) override;
private:

View File

@ -75,3 +75,11 @@ $env:METACORE_ENABLE_HEADLESS_IMGUI = "1"
```
This mode is still treated as experimental and is not part of the stable runtime path yet.
Optional runtime toggles:
- enable Panda window bootstrap: `METACORE_ENABLE_PANDA_WINDOW=1`
- limit editor frame count for automation/headless runs: `METACORE_EDITOR_FRAME_COUNT=<n>`
When Panda window bootstrap is enabled and `METACORE_EDITOR_FRAME_COUNT` is not set, `MetaEditor` now keeps running
until the window is closed. For automation, set an explicit frame count to force exit after a bounded number of frames.

BIN
box1.glb Normal file

Binary file not shown.

View File

@ -83,6 +83,8 @@ Current runtime behavior:
- `DearImGuiEditorShell` defaults to a stable non-rendering mode
- experimental headless ImGui frame generation can be enabled with `METACORE_ENABLE_HEADLESS_IMGUI=1`
- the experimental headless ImGui mode is not yet considered stable
- when `METACORE_ENABLE_PANDA_WINDOW=1` and no frame budget is provided, `MetaEditor` stays open until the Panda window closes
- `METACORE_EDITOR_FRAME_COUNT` can be set to bound editor runtime during probes and CI-like runs
## Optional GUI feature toggles

View File

@ -2,10 +2,13 @@
#include "MetaCore/engine/editor_core/EditorSession.h"
#include "MetaCore/engine/editor_core/SceneCommands.h"
#include "MetaCore/engine/editor_core/SelectionService.h"
#include "MetaCore/engine/editor_core/TransformGizmoService.h"
#include "MetaCore/engine/editor_core/ViewportService.h"
#include "MetaCore/engine/scene/Project.h"
#include "MetaCore/engine/scene/Scene.h"
#include "MetaCore/engine/scene/SceneSerializer.h"
#include "MetaCore/platform/panda/PandaPlatformContext.h"
#include "MetaCore/platform/panda/PandaRenderBackend.h"
#include <cstdlib>
#include <filesystem>
@ -29,7 +32,10 @@ int main() {
using metacore::engine::editor_core::EditorSession;
using metacore::engine::editor_core::SceneCommands;
using metacore::engine::editor_core::SelectionService;
using metacore::engine::editor_core::TransformGizmoService;
using metacore::engine::editor_core::ViewportService;
using metacore::platform::panda::PandaPlatformContext;
using metacore::platform::panda::PandaRenderBackend;
using metacore::engine::scene::Project;
using metacore::engine::scene::ProjectService;
using metacore::engine::scene::Scene;
@ -72,6 +78,41 @@ int main() {
commands.redo();
expect(value == 1, "command should redo");
TransformGizmoService gizmo;
gizmo.set_operation(TransformGizmoService::Operation::Rotate);
gizmo.set_space(TransformGizmoService::Space::World);
gizmo.set_snap_enabled(true);
gizmo.set_snap_values({15.0F, 15.0F, 15.0F});
expect(gizmo.operation() == TransformGizmoService::Operation::Rotate, "gizmo operation should persist");
expect(gizmo.space() == TransformGizmoService::Space::World, "gizmo space should persist");
expect(gizmo.snap_enabled(), "gizmo snap should persist");
expect(gizmo.snap_values()[0] == 15.0F, "gizmo snap values should persist");
PandaPlatformContext platform;
expect(platform.initialize("MetaCoreTests"), "platform should initialize");
platform.pump_frame();
platform.pump_frame();
expect(platform.frame_count() == 2, "platform frame count should increment");
platform.request_close();
expect(platform.should_close(), "platform close request should persist");
platform.shutdown();
PandaRenderBackend render_backend;
expect(render_backend.initialize(), "render backend should initialize");
metacore::engine::render::UiDrawFrame ui_frame;
ui_frame.display_size = {1280.0F, 720.0F};
ui_frame.total_command_count = 2;
ui_frame.total_vertex_count = 6;
ui_frame.total_index_count = 9;
ui_frame.vertices.push_back({{10.0F, 20.0F}, {0.0F, 0.0F}, 0xffffffffu});
ui_frame.indices.push_back(0);
ui_frame.commands.push_back({{0.0F, 0.0F, 100.0F, 50.0F}, 0, 3, 0, 0});
render_backend.set_ui_draw_frame(ui_frame);
expect(render_backend.ui_draw_frame().total_command_count == 2, "ui draw frame command count should persist");
expect(render_backend.ui_draw_frame().vertices.size() == 1, "ui draw frame vertices should persist");
expect(render_backend.submitted_ui_frame_count() == 1, "ui draw frame submissions should be tracked");
render_backend.shutdown();
EditorSession session;
SelectionService selection;
ViewportService viewport;

View File

@ -1,5 +1,6 @@
#include <iostream>
#include <string>
#include <cstdlib>
#if METACORE_HAS_PANDA3D
#include <pandaFramework.h>
@ -14,6 +15,19 @@ void log_step(const std::string& message) {
std::cout << "[MetaCoreWindowProbe] " << message << std::endl;
}
int resolve_frame_budget() {
const char* value = std::getenv("METACORE_WINDOW_PROBE_FRAME_COUNT");
if (value == nullptr || value[0] == '\0') {
return -1;
}
try {
const int parsed = std::stoi(value);
return parsed > 0 ? parsed : -1;
} catch (...) {
return -1;
}
}
}
int main() {
@ -49,8 +63,22 @@ int main() {
}
log_step("Window opened successfully");
framework.do_frame(nullptr);
log_step("Frame pumped successfully");
const int frame_budget = resolve_frame_budget();
if (frame_budget < 0) {
log_step("Running until the window is closed manually");
while (window->get_graphics_window() != nullptr && !window->get_graphics_window()->is_closed()) {
framework.do_frame(nullptr);
}
} else {
log_step("Running for " + std::to_string(frame_budget) + " frame(s)");
for (int frame = 0; frame < frame_budget; ++frame) {
framework.do_frame(nullptr);
if (window->get_graphics_window() != nullptr && window->get_graphics_window()->is_closed()) {
break;
}
}
}
log_step("Frame loop finished");
framework.close_window(window);
log_step("Window closed successfully");

222
tools/PandaCubeDemo.cpp Normal file
View File

@ -0,0 +1,222 @@
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <string_view>
#include <cmath>
#if METACORE_HAS_PANDA3D
#include <graphicsWindow.h>
#include <keyboardButton.h>
#include <lineSegs.h>
#include <load_prc_file.h>
#include <luse.h>
#include <nodePath.h>
#include <pandaFramework.h>
#include <transformState.h>
#include <windowProperties.h>
#include <filename.h>
#include <windows.h>
#endif
namespace {
void log_step(const std::string& message) {
std::cout << "[PandaCubeDemo] " << message << std::endl;
}
#if METACORE_HAS_PANDA3D
void configure_runtime_environment() {
const auto panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
const auto panda_etc = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "etc";
if (std::filesystem::exists(panda_bin)) {
SetDllDirectoryW(panda_bin.wstring().c_str());
log_step("DLL search path set to " + panda_bin.string());
const std::wstring existing_path = [&]() {
const DWORD required = GetEnvironmentVariableW(L"PATH", nullptr, 0);
if (required == 0) {
return std::wstring{};
}
std::wstring buffer(required, L'\0');
GetEnvironmentVariableW(L"PATH", buffer.data(), required);
if (!buffer.empty() && buffer.back() == L'\0') {
buffer.pop_back();
}
return buffer;
}();
const std::wstring new_path = panda_bin.wstring() + L";" + existing_path;
SetEnvironmentVariableW(L"PATH", new_path.c_str());
log_step("PATH prefixed with Panda bin");
}
if (std::filesystem::exists(panda_etc)) {
SetEnvironmentVariableW(L"PANDA_PRC_DIR", panda_etc.wstring().c_str());
log_step("PANDA_PRC_DIR set to " + panda_etc.string());
}
wchar_t exe_path_buffer[MAX_PATH];
const DWORD path_length = GetModuleFileNameW(nullptr, exe_path_buffer, MAX_PATH);
if (path_length > 0 && path_length < MAX_PATH) {
std::filesystem::path exe_path(exe_path_buffer);
std::filesystem::current_path(exe_path.parent_path());
log_step("Working directory set to " + exe_path.parent_path().string());
}
}
void configure_local_prc() {
wchar_t exe_path_buffer[MAX_PATH];
const DWORD path_length = GetModuleFileNameW(nullptr, exe_path_buffer, MAX_PATH);
if (path_length == 0 || path_length >= MAX_PATH) {
return;
}
const std::filesystem::path exe_dir = std::filesystem::path(exe_path_buffer).parent_path();
const std::filesystem::path prc_path = exe_dir / "PandaCubeDemo.prc";
const std::filesystem::path panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
const std::string panda_bin_forward = panda_bin.generic_string();
std::ofstream prc_file(prc_path, std::ios::trunc);
prc_file
<< "plugin-path " << panda_bin_forward << "\n"
<< "load-display pandagl\n"
<< "aux-display p3tinydisplay\n"
<< "window-type onscreen\n"
<< "win-size 1280 720\n"
<< "sync-video #f\n"
<< "audio-library-name null\n";
prc_file.close();
log_step("Local PRC written to " + prc_path.string());
load_prc_file(Filename::from_os_specific(prc_path.string()));
log_step("Local PRC loaded");
}
NodePath create_colored_cube(const NodePath& parent) {
LineSegs segments("demo-cube");
segments.set_thickness(2.5F);
segments.set_color(0.85F, 0.9F, 1.0F, 1.0F);
const LPoint3f corners[] = {
{-0.5F, -0.5F, -0.5F},
{0.5F, -0.5F, -0.5F},
{0.5F, 0.5F, -0.5F},
{-0.5F, 0.5F, -0.5F},
{-0.5F, -0.5F, 0.5F},
{0.5F, -0.5F, 0.5F},
{0.5F, 0.5F, 0.5F},
{-0.5F, 0.5F, 0.5F},
};
const int edges[][2] = {
{0, 1}, {1, 2}, {2, 3}, {3, 0},
{4, 5}, {5, 6}, {6, 7}, {7, 4},
{0, 4}, {1, 5}, {2, 6}, {3, 7},
};
for (const auto& edge : edges) {
segments.move_to(corners[edge[0]]);
segments.draw_to(corners[edge[1]]);
}
return parent.attach_new_node(segments.create());
}
float movement_axis(const SHORT positive_key, const SHORT negative_key) {
const float positive = (GetAsyncKeyState(positive_key) & 0x8000) != 0 ? 1.0F : 0.0F;
const float negative = (GetAsyncKeyState(negative_key) & 0x8000) != 0 ? 1.0F : 0.0F;
return positive - negative;
}
#endif
} // namespace
int main() {
#if METACORE_HAS_PANDA3D
log_step("Starting Panda3D C++ cube demo.");
configure_runtime_environment();
configure_local_prc();
PandaFramework framework;
log_step("Calling open_framework()");
framework.open_framework();
log_step("open_framework() succeeded");
framework.set_window_title("Panda3D Cube Demo");
WindowProperties props;
props.set_title("Panda3D Cube Demo");
props.set_size(1280, 720);
log_step("Calling open_window()");
WindowFramework* window = framework.open_window(props, 0);
if (window == nullptr) {
log_step("open_window() returned null");
framework.close_framework();
return 2;
}
log_step("open_window() succeeded");
if (GraphicsWindow* graphics_window = window->get_graphics_window(); graphics_window != nullptr) {
graphics_window->set_clear_color(LColor(0.06F, 0.07F, 0.10F, 1.0F));
}
NodePath render_root = window->get_render();
NodePath cube = create_colored_cube(render_root);
cube.set_scale(1.5F);
NodePath camera = window->get_camera_group();
float camera_x = 0.0F;
float camera_y = -8.0F;
float camera_z = 2.0F;
float heading = 0.0F;
float pitch = -10.0F;
log_step("Controls: WASD move, RF up/down, arrow keys look, Esc closes.");
while (window->get_graphics_window() != nullptr && !window->get_graphics_window()->is_closed()) {
if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) {
break;
}
const float move_speed = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0) ? 0.18F : 0.08F;
const float turn_speed = 1.2F;
heading += movement_axis(VK_RIGHT, VK_LEFT) * turn_speed;
pitch += movement_axis(VK_UP, VK_DOWN) * turn_speed;
if (pitch > 89.0F) {
pitch = 89.0F;
}
if (pitch < -89.0F) {
pitch = -89.0F;
}
const float heading_radians = heading * 3.1415926535F / 180.0F;
const float forward_x = std::sin(heading_radians);
const float forward_y = std::cos(heading_radians);
const float right_x = std::cos(heading_radians);
const float right_y = -std::sin(heading_radians);
const float forward_axis = movement_axis('W', 'S');
const float strafe_axis = movement_axis('D', 'A');
const float vertical_axis = movement_axis('R', 'F');
camera_x += forward_x * forward_axis * move_speed;
camera_y += forward_y * forward_axis * move_speed;
camera_x += right_x * strafe_axis * move_speed;
camera_y += right_y * strafe_axis * move_speed;
camera_z += vertical_axis * move_speed;
camera.set_pos(camera_x, camera_y, camera_z);
camera.set_hpr(heading, pitch, 0.0F);
cube.set_h(cube.get_h() + 0.2F);
framework.do_frame(nullptr);
}
framework.close_window(window);
framework.close_framework();
log_step("Demo closed cleanly.");
return 0;
#else
log_step("Panda3D support is not enabled in this build.");
return 1;
#endif
}

View File

@ -0,0 +1,468 @@
#include <array>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
#include <callbackObject.h>
#include <displayRegion.h>
#include <graphicsWindow.h>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_win32.h>
#include <lineSegs.h>
#include <load_prc_file.h>
#include <luse.h>
#include <nodePath.h>
#include <pandaFramework.h>
#include <windowProperties.h>
#include <windows.h>
#endif
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
#endif
namespace {
void log_step(const std::string& message) {
std::cout << "[PandaCubeEditorDemo] " << message << std::endl;
}
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
struct DemoState {
float camera_x = 0.0F;
float camera_y = -8.0F;
float camera_z = 2.0F;
float heading = 0.0F;
float pitch = -10.0F;
float rotation_speed = 20.0F;
bool auto_rotate = true;
bool show_demo_window = false;
bool overlay_enabled = true;
float model_position[3] = {0.0F, 0.0F, 0.0F};
float model_rotation[3] = {0.0F, 0.0F, 0.0F};
float model_scale[3] = {1.0F, 1.0F, 1.0F};
bool using_fallback_cube = false;
};
WNDPROC g_previous_wnd_proc = nullptr;
ImDrawData* g_pending_draw_data = nullptr;
HWND g_panda_hwnd = nullptr;
PT(DisplayRegion) g_overlay_region = nullptr;
class ImGuiOverlayDrawCallback final : public CallbackObject {
public:
void do_callback(CallbackData* cbdata) override {
(void)cbdata;
if (g_pending_draw_data != nullptr) {
ImGui_ImplOpenGL3_RenderDrawData(g_pending_draw_data);
}
}
};
LRESULT CALLBACK panda_imgui_wnd_proc(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param) {
if (ImGui_ImplWin32_WndProcHandler(hwnd, message, w_param, l_param)) {
return 1;
}
if (g_previous_wnd_proc != nullptr) {
return CallWindowProc(g_previous_wnd_proc, hwnd, message, w_param, l_param);
}
return DefWindowProc(hwnd, message, w_param, l_param);
}
void configure_runtime_environment() {
const auto panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
const auto panda_etc = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "etc";
if (std::filesystem::exists(panda_bin)) {
SetDllDirectoryW(panda_bin.wstring().c_str());
const DWORD required = GetEnvironmentVariableW(L"PATH", nullptr, 0);
std::wstring existing_path(required > 0 ? required - 1 : 0, L'\0');
if (required > 0) {
GetEnvironmentVariableW(L"PATH", existing_path.data(), required);
}
const std::wstring new_path = panda_bin.wstring() + L";" + existing_path;
SetEnvironmentVariableW(L"PATH", new_path.c_str());
}
if (std::filesystem::exists(panda_etc)) {
SetEnvironmentVariableW(L"PANDA_PRC_DIR", panda_etc.wstring().c_str());
}
wchar_t exe_path_buffer[MAX_PATH];
const DWORD path_length = GetModuleFileNameW(nullptr, exe_path_buffer, MAX_PATH);
if (path_length > 0 && path_length < MAX_PATH) {
std::filesystem::current_path(std::filesystem::path(exe_path_buffer).parent_path());
}
}
void configure_local_prc() {
wchar_t exe_path_buffer[MAX_PATH];
const DWORD path_length = GetModuleFileNameW(nullptr, exe_path_buffer, MAX_PATH);
if (path_length == 0 || path_length >= MAX_PATH) {
return;
}
const std::filesystem::path exe_dir = std::filesystem::path(exe_path_buffer).parent_path();
const std::filesystem::path prc_path = exe_dir / "PandaCubeEditorDemo.prc";
const std::filesystem::path panda_bin = std::filesystem::path(METACORE_PANDA3D_ROOT_PATH) / "bin";
std::ofstream prc_file(prc_path, std::ios::trunc);
prc_file
<< "plugin-path " << panda_bin.generic_string() << "\n"
<< "load-display pandagl\n"
<< "aux-display p3tinydisplay\n"
<< "load-file-type p3assimp\n"
<< "load-file-type p3ptloader\n"
<< "window-type onscreen\n"
<< "win-size 1280 720\n"
<< "sync-video #f\n"
<< "audio-library-name null\n";
prc_file.close();
load_prc_file(Filename::from_os_specific(prc_path.string()));
}
NodePath create_wire_cube(const NodePath& parent) {
LineSegs segments("editor-demo-cube");
segments.set_thickness(2.5F);
segments.set_color(0.85F, 0.9F, 1.0F, 1.0F);
const LPoint3f corners[] = {
{-0.5F, -0.5F, -0.5F},
{0.5F, -0.5F, -0.5F},
{0.5F, 0.5F, -0.5F},
{-0.5F, 0.5F, -0.5F},
{-0.5F, -0.5F, 0.5F},
{0.5F, -0.5F, 0.5F},
{0.5F, 0.5F, 0.5F},
{-0.5F, 0.5F, 0.5F},
};
const int edges[][2] = {
{0, 1}, {1, 2}, {2, 3}, {3, 0},
{4, 5}, {5, 6}, {6, 7}, {7, 4},
{0, 4}, {1, 5}, {2, 6}, {3, 7},
};
for (const auto& edge : edges) {
segments.move_to(corners[edge[0]]);
segments.draw_to(corners[edge[1]]);
}
return parent.attach_new_node(segments.create());
}
NodePath create_world_axes(const NodePath& parent) {
LineSegs segments("world-axes");
segments.set_thickness(3.0F);
segments.set_color(0.95F, 0.2F, 0.2F, 1.0F);
segments.move_to(0.0F, 0.0F, 0.0F);
segments.draw_to(2.0F, 0.0F, 0.0F);
segments.set_color(0.2F, 0.95F, 0.35F, 1.0F);
segments.move_to(0.0F, 0.0F, 0.0F);
segments.draw_to(0.0F, 2.0F, 0.0F);
segments.set_color(0.25F, 0.55F, 1.0F, 1.0F);
segments.move_to(0.0F, 0.0F, 0.0F);
segments.draw_to(0.0F, 0.0F, 2.0F);
return parent.attach_new_node(segments.create());
}
NodePath create_ground_grid(const NodePath& parent) {
LineSegs segments("ground-grid");
segments.set_thickness(1.0F);
segments.set_color(0.35F, 0.38F, 0.42F, 0.85F);
constexpr int half_extent = 10;
for (int i = -half_extent; i <= half_extent; ++i) {
segments.move_to(static_cast<float>(i), -static_cast<float>(half_extent), 0.0F);
segments.draw_to(static_cast<float>(i), static_cast<float>(half_extent), 0.0F);
segments.move_to(-static_cast<float>(half_extent), static_cast<float>(i), 0.0F);
segments.draw_to(static_cast<float>(half_extent), static_cast<float>(i), 0.0F);
}
return parent.attach_new_node(segments.create());
}
NodePath load_demo_model(WindowFramework* window, DemoState& state) {
const Filename model_path = Filename::from_os_specific("D:/MetaCore/box1.glb");
NodePath model = window->load_model(window->get_render(), model_path);
if (!model.is_empty()) {
log_step("Loaded model: D:/MetaCore/box1.glb");
state.using_fallback_cube = false;
return model;
}
log_step("Failed to load box1.glb, falling back to wire cube");
state.using_fallback_cube = true;
NodePath fallback = create_wire_cube(window->get_render());
fallback.set_scale(1.5F);
return fallback;
}
void focus_camera_on_model(const NodePath& model, DemoState& state) {
LPoint3 min_point;
LPoint3 max_point;
if (!model.calc_tight_bounds(min_point, max_point)) {
state.camera_x = 0.0F;
state.camera_y = -8.0F;
state.camera_z = 2.0F;
state.model_position[0] = 0.0F;
state.model_position[1] = 0.0F;
state.model_position[2] = 0.0F;
return;
}
const LPoint3 center(
(min_point[0] + max_point[0]) * 0.5F,
(min_point[1] + max_point[1]) * 0.5F,
(min_point[2] + max_point[2]) * 0.5F
);
const LVecBase3 extent = max_point - min_point;
const float max_extent = (std::max)((std::max)(std::fabs(extent[0]), std::fabs(extent[1])), std::fabs(extent[2]));
const float distance = (std::max)(3.0F, max_extent * 3.0F);
state.model_position[0] = -center[0];
state.model_position[1] = -center[1];
state.model_position[2] = -center[2];
state.camera_x = 0.0F;
state.camera_y = -distance;
state.camera_z = (std::max)(1.5F, max_extent * 0.9F);
state.heading = 0.0F;
state.pitch = -10.0F;
log_step(
"Model bounds centered. Extent=" + std::to_string(max_extent) +
" camera_y=" + std::to_string(state.camera_y) +
" camera_z=" + std::to_string(state.camera_z)
);
}
std::uintptr_t native_window_handle(WindowFramework* window) {
if (window == nullptr || window->get_graphics_window() == nullptr) {
return 0;
}
if (WindowHandle* handle = window->get_graphics_window()->get_window_handle(); handle != nullptr) {
if (WindowHandle::OSHandle* os_handle = handle->get_os_handle(); os_handle != nullptr) {
return static_cast<std::uintptr_t>(os_handle->get_int_handle());
}
}
return 0;
}
void render_editor_ui(DemoState& state) {
if (!state.overlay_enabled) {
return;
}
ImGui::SetNextWindowBgAlpha(0.78F);
ImGui::SetNextWindowPos(ImVec2(16.0F, 16.0F), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(360.0F, 0.0F), ImGuiCond_Always);
if (ImGui::Begin("MetaCore Editor Overlay", nullptr,
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings)) {
ImGui::TextUnformatted("Single-window Panda3D + ImGui test");
ImGui::Separator();
ImGui::Checkbox("Overlay Enabled", &state.overlay_enabled);
ImGui::Checkbox("Auto Rotate", &state.auto_rotate);
ImGui::Checkbox("Show ImGui Demo", &state.show_demo_window);
ImGui::Text("Model: %s", state.using_fallback_cube ? "Fallback Wire Cube" : "box1.glb");
ImGui::SliderFloat("Rotation Speed", &state.rotation_speed, 0.0F, 180.0F, "%.1f deg/s");
ImGui::DragFloat3("Model Position", state.model_position, 0.05F);
ImGui::DragFloat3("Model Rotation", state.model_rotation, 1.0F);
ImGui::DragFloat3("Model Scale", state.model_scale, 0.02F, 0.05F, 10.0F);
ImGui::DragFloat3("Camera Position", &state.camera_x, 0.05F);
ImGui::SliderFloat("Heading", &state.heading, -180.0F, 180.0F, "%.1f");
ImGui::SliderFloat("Pitch", &state.pitch, -89.0F, 89.0F, "%.1f");
ImGui::Separator();
ImGui::TextUnformatted("Controls");
ImGui::BulletText("W/A/S/D move");
ImGui::BulletText("R/F up and down");
ImGui::BulletText("Arrow keys look");
ImGui::BulletText("Esc closes");
ImGui::TextUnformatted("This ImGui panel is rendered over the Panda viewport.");
}
ImGui::End();
if (state.show_demo_window) {
ImGui::ShowDemoWindow(&state.show_demo_window);
}
}
#endif
} // namespace
int main() {
#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI
log_step("Starting PandaCubeEditorDemo");
configure_runtime_environment();
configure_local_prc();
PandaFramework framework;
framework.open_framework();
framework.set_window_title("MetaCore Single-Window Editor Demo");
WindowProperties props;
props.set_title("MetaCore Single-Window Editor Demo");
props.set_size(1280, 720);
WindowFramework* panda_window = framework.open_window(props, 0);
if (panda_window == nullptr) {
log_step("Panda window failed to open");
framework.close_framework();
return 2;
}
create_ground_grid(panda_window->get_render());
create_world_axes(panda_window->get_render());
DemoState state;
NodePath model = load_demo_model(panda_window, state);
focus_camera_on_model(model, state);
g_panda_hwnd = reinterpret_cast<HWND>(native_window_handle(panda_window));
if (g_panda_hwnd == nullptr) {
log_step("Failed to resolve Panda window handle");
framework.close_window(panda_window);
framework.close_framework();
return 3;
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
ImGui::StyleColorsDark();
ImGui_ImplWin32_Init(g_panda_hwnd);
ImGui_ImplOpenGL3_Init("#version 130");
g_previous_wnd_proc = reinterpret_cast<WNDPROC>(
SetWindowLongPtr(g_panda_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(panda_imgui_wnd_proc))
);
g_overlay_region = panda_window->get_graphics_output()->make_display_region();
g_overlay_region->set_sort(1000);
g_overlay_region->set_incomplete_render(true);
g_overlay_region->set_clear_color_active(false);
g_overlay_region->set_clear_depth_active(false);
PT(CallbackObject) overlay_callback = new ImGuiOverlayDrawCallback();
g_overlay_region->set_draw_callback(overlay_callback);
DWORD previous_tick = GetTickCount();
bool running = true;
while (running) {
if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) {
running = false;
}
if (panda_window->get_graphics_window() != nullptr && panda_window->get_graphics_window()->is_closed()) {
running = false;
}
const DWORD current_tick = GetTickCount();
const float delta_seconds = static_cast<float>(current_tick - previous_tick) / 1000.0F;
previous_tick = current_tick;
const float move_speed = ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0 ? 5.0F : 2.5F) * delta_seconds;
const float turn_speed = 90.0F * delta_seconds;
const auto key_down = [](const int key) {
return (GetAsyncKeyState(key) & 0x8000) != 0;
};
if (key_down(VK_LEFT)) {
state.heading -= turn_speed;
}
if (key_down(VK_RIGHT)) {
state.heading += turn_speed;
}
if (key_down(VK_UP)) {
state.pitch += turn_speed;
}
if (key_down(VK_DOWN)) {
state.pitch -= turn_speed;
}
if (state.pitch > 89.0F) {
state.pitch = 89.0F;
}
if (state.pitch < -89.0F) {
state.pitch = -89.0F;
}
const float heading_radians = state.heading * 3.1415926535F / 180.0F;
const float forward_x = std::sin(heading_radians);
const float forward_y = std::cos(heading_radians);
const float right_x = std::cos(heading_radians);
const float right_y = -std::sin(heading_radians);
if (key_down('W')) {
state.camera_x += forward_x * move_speed;
state.camera_y += forward_y * move_speed;
}
if (key_down('S')) {
state.camera_x -= forward_x * move_speed;
state.camera_y -= forward_y * move_speed;
}
if (key_down('D')) {
state.camera_x += right_x * move_speed;
state.camera_y += right_y * move_speed;
}
if (key_down('A')) {
state.camera_x -= right_x * move_speed;
state.camera_y -= right_y * move_speed;
}
if (key_down('R')) {
state.camera_z += move_speed;
}
if (key_down('F')) {
state.camera_z -= move_speed;
}
panda_window->get_camera_group().set_pos(state.camera_x, state.camera_y, state.camera_z);
panda_window->get_camera_group().set_hpr(state.heading, state.pitch, 0.0F);
model.set_pos(state.model_position[0], state.model_position[1], state.model_position[2]);
model.set_hpr(state.model_rotation[0], state.model_rotation[1], state.model_rotation[2]);
model.set_scale(state.model_scale[0], state.model_scale[1], state.model_scale[2]);
if (state.auto_rotate) {
state.model_rotation[0] += state.rotation_speed * delta_seconds;
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
render_editor_ui(state);
ImGui::Render();
g_pending_draw_data = ImGui::GetDrawData();
framework.do_frame(nullptr);
}
if (g_overlay_region != nullptr) {
g_overlay_region->clear_draw_callback();
if (panda_window->get_graphics_output() != nullptr) {
panda_window->get_graphics_output()->remove_display_region(g_overlay_region);
}
g_overlay_region = nullptr;
}
g_pending_draw_data = nullptr;
if (g_previous_wnd_proc != nullptr) {
SetWindowLongPtr(g_panda_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_previous_wnd_proc));
g_previous_wnd_proc = nullptr;
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
framework.close_window(panda_window);
framework.close_framework();
return 0;
#else
log_step("Panda3D or Dear ImGui support is not enabled in this build.");
return 1;
#endif
}