#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 }