diff --git a/CMakePresets.json b/CMakePresets.json index 8ff90ed..bed9052 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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 + } } ] } diff --git a/MetaCore/engine/render/RenderTypes.h b/MetaCore/engine/render/RenderTypes.h index 2dc2239..4f6da8f 100644 --- a/MetaCore/engine/render/RenderTypes.h +++ b/MetaCore/engine/render/RenderTypes.h @@ -47,6 +47,33 @@ struct TonemapSettings { float exposure = 1.0F; }; +struct UiVertex { + std::array position{0.0F, 0.0F}; + std::array uv{0.0F, 0.0F}; + std::uint32_t color = 0; +}; + +struct UiDrawCommand { + std::array 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 display_pos{0.0F, 0.0F}; + std::array display_size{0.0F, 0.0F}; + std::array 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 vertices; + std::vector indices; + std::vector 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 diff --git a/MetaCore/platform/panda/PandaPlatformContext.cpp b/MetaCore/platform/panda/PandaPlatformContext.cpp index 5adea56..c66551b 100644 --- a/MetaCore/platform/panda/PandaPlatformContext.cpp +++ b/MetaCore/platform/panda/PandaPlatformContext.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include 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(); - 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(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(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(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(region); + } + } +#endif + return 0; +} + } // namespace metacore::platform::panda diff --git a/MetaCore/platform/panda/PandaPlatformContext.h b/MetaCore/platform/panda/PandaPlatformContext.h index cc04e2b..7ea0132 100644 --- a/MetaCore/platform/panda/PandaPlatformContext.h +++ b/MetaCore/platform/panda/PandaPlatformContext.h @@ -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 native_state_; }; diff --git a/MetaCore/platform/panda/PandaRenderBackend.cpp b/MetaCore/platform/panda/PandaRenderBackend.cpp index 899e1ba..cca9b0f 100644 --- a/MetaCore/platform/panda/PandaRenderBackend.cpp +++ b/MetaCore/platform/panda/PandaRenderBackend.cpp @@ -1,14 +1,159 @@ #include "MetaCore/platform/panda/PandaRenderBackend.h" +#if METACORE_HAS_PANDA3D +#include +#include +#include +#include +#include +#include +#endif + +#include +#include + 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(); 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(); + } + + WindowFramework* window = reinterpret_cast(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(); + } + + DisplayRegion* region = reinterpret_cast(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 position, + const std::array rotation, + const std::array 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 camera_position, + const std::array 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 diff --git a/MetaCore/platform/panda/PandaRenderBackend.h b/MetaCore/platform/panda/PandaRenderBackend.h index 5d10819..82be9cc 100644 --- a/MetaCore/platform/panda/PandaRenderBackend.h +++ b/MetaCore/platform/panda/PandaRenderBackend.h @@ -1,23 +1,49 @@ #pragma once #include "MetaCore/engine/render/RenderTypes.h" +#include +#include +#include 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 camera_position, std::array focus_position); + void set_demo_selection_transform( + bool has_selection, + std::array position, + std::array rotation, + std::array 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 native_state_{}; }; } // namespace metacore::platform::panda diff --git a/MetaEditor/DearImGuiEditorShell.cpp b/MetaEditor/DearImGuiEditorShell.cpp index b716538..ff131a5 100644 --- a/MetaEditor/DearImGuiEditorShell.cpp +++ b/MetaEditor/DearImGuiEditorShell.cpp @@ -2,9 +2,13 @@ #include "MetaCore/foundation/FileSystem.h" +#include + #if METACORE_HAS_IMGUI #include #include +#include +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 #include #include +#include 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(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 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(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(native_window_width_) : 1280.0F, + native_window_height_ > 0 ? static_cast(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(1)); + + if (native_window_handle_ != nullptr) { + ImGui_ImplWin32_Init(native_window_handle_); + SetPropA(native_window_handle_, kMetaCoreImGuiShellWindowProp, this); + previous_wnd_proc_ = reinterpret_cast( + SetWindowLongPtr(native_window_handle_, GWLP_WNDPROC, reinterpret_cast(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(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(draw_data->TotalVtxCount)); + pending_render_frame_.indices.reserve(static_cast(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(draw_list->IdxBuffer[index_index]) + static_cast(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(cmd.ElemCount), + global_index_offset + static_cast(cmd.IdxOffset), + global_vertex_offset + static_cast(cmd.VtxOffset) + }); + } + global_vertex_offset += static_cast(draw_list->VtxBuffer.Size); + global_index_offset += static_cast(draw_list->IdxBuffer.Size); + } + + pending_render_frame_.total_command_count = static_cast(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(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(native_window_width_) : 1280.0F, + native_window_height_ > 0 ? static_cast(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(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); } diff --git a/MetaEditor/DearImGuiEditorShell.h b/MetaEditor/DearImGuiEditorShell.h index ae6e714..25b9792 100644 --- a/MetaEditor/DearImGuiEditorShell.h +++ b/MetaEditor/DearImGuiEditorShell.h @@ -3,6 +3,9 @@ #include "MetaEditor/IEditorShell.h" #include +#include + +#include 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 diff --git a/MetaEditor/EditorApplication.cpp b/MetaEditor/EditorApplication.cpp index dc4d560..ef7f2d0 100644 --- a/MetaEditor/EditorApplication.cpp +++ b/MetaEditor/EditorApplication.cpp @@ -5,21 +5,128 @@ #include "MetaCore/foundation/Log.h" #include +#include +#include +#include +#include namespace metacore::editor { +namespace { + +using TransformGizmoService = metacore::engine::editor_core::TransformGizmoService; + +std::optional 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 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(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(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 diff --git a/MetaEditor/EditorApplication.h b/MetaEditor/EditorApplication.h index abcecea..fe35934 100644 --- a/MetaEditor/EditorApplication.h +++ b/MetaEditor/EditorApplication.h @@ -21,6 +21,8 @@ #include #include +#include +#include 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 last_snapshot_{}; EditorPresenter presenter_{}; std::unique_ptr editor_shell_{}; + std::array key_down_{}; }; } // namespace metacore::editor diff --git a/MetaEditor/IEditorShell.h b/MetaEditor/IEditorShell.h index afdf366..a233f12 100644 --- a/MetaEditor/IEditorShell.h +++ b/MetaEditor/IEditorShell.h @@ -1,6 +1,7 @@ #pragma once #include "MetaEditor/EditorPresenter.h" +#include "MetaCore/engine/render/RenderTypes.h" #include #include @@ -11,6 +12,17 @@ namespace metacore::editor { struct PendingShellEdits { + struct GizmoStateEdit { + std::string operation; + std::string space; + std::array 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 position{0.0F, 0.0F, 0.0F}; @@ -18,6 +30,7 @@ struct PendingShellEdits { std::array scale{1.0F, 1.0F, 1.0F}; }; + std::optional gizmo_state_edit; std::optional 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; }; diff --git a/MetaEditor/NullEditorShell.cpp b/MetaEditor/NullEditorShell.cpp index 8f67590..215d467 100644 --- a/MetaEditor/NullEditorShell.cpp +++ b/MetaEditor/NullEditorShell.cpp @@ -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); } diff --git a/MetaEditor/NullEditorShell.h b/MetaEditor/NullEditorShell.h index 447cb1e..0614744 100644 --- a/MetaEditor/NullEditorShell.h +++ b/MetaEditor/NullEditorShell.h @@ -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: diff --git a/README.md b/README.md index a93cf88..a70e05d 100644 --- a/README.md +++ b/README.md @@ -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=` + +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. diff --git a/box1.glb b/box1.glb new file mode 100644 index 0000000..06712bc Binary files /dev/null and b/box1.glb differ diff --git a/docs/setup-windows.md b/docs/setup-windows.md index 053f03d..23e1cce 100644 --- a/docs/setup-windows.md +++ b/docs/setup-windows.md @@ -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 diff --git a/tests/MetaCoreTests.cpp b/tests/MetaCoreTests.cpp index 133f94f..1e9fc9c 100644 --- a/tests/MetaCoreTests.cpp +++ b/tests/MetaCoreTests.cpp @@ -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 #include @@ -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; diff --git a/tools/MetaCoreWindowProbe.cpp b/tools/MetaCoreWindowProbe.cpp index b930ab0..ffc1e84 100644 --- a/tools/MetaCoreWindowProbe.cpp +++ b/tools/MetaCoreWindowProbe.cpp @@ -1,5 +1,6 @@ #include #include +#include #if METACORE_HAS_PANDA3D #include @@ -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"); diff --git a/tools/PandaCubeDemo.cpp b/tools/PandaCubeDemo.cpp new file mode 100644 index 0000000..b1077ca --- /dev/null +++ b/tools/PandaCubeDemo.cpp @@ -0,0 +1,222 @@ +#include +#include +#include +#include +#include +#include +#include + +#if METACORE_HAS_PANDA3D +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 +} diff --git a/tools/PandaCubeEditorDemo.cpp b/tools/PandaCubeEditorDemo.cpp new file mode 100644 index 0000000..bb81175 --- /dev/null +++ b/tools/PandaCubeEditorDemo.cpp @@ -0,0 +1,468 @@ +#include +#include +#include +#include +#include +#include +#include + +#if METACORE_HAS_PANDA3D && METACORE_HAS_IMGUI +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(i), -static_cast(half_extent), 0.0F); + segments.draw_to(static_cast(i), static_cast(half_extent), 0.0F); + segments.move_to(-static_cast(half_extent), static_cast(i), 0.0F); + segments.draw_to(static_cast(half_extent), static_cast(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(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(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( + SetWindowLongPtr(g_panda_hwnd, GWLP_WNDPROC, reinterpret_cast(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(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(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 +}