#include "gui.h" #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include // drags in the system GL headers we need below #include #include static void glfw_error_callback(int error, const char* description) { std::fprintf(stderr, "GLFW error %d: %s\n", error, description); } // Analog-style rpm tach: a 270-degree arc face (gap at the bottom), // tick marks in thousands, a red zone from redline to max, and a needle. // Draws in-place at the current cursor position, then advances the // cursor past it like any other widget. static void DrawTachGauge(float rpm, float max_rpm, float redline_rpm, float diameter) { constexpr float kPi = 3.14159265358979323846f; ImVec2 p0 = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("##tach_gauge", ImVec2(diameter, diameter)); ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 center(p0.x + diameter * 0.5f, p0.y + diameter * 0.5f); float radius = diameter * 0.5f - 4.0f; // Angle convention here: 0 = 3 o'clock, increases clockwise. // 135 -> 405 sweeps the long way around through the top (270), // leaving a gap at the bottom like a real tach face. const float a_min = 135.0f * kPi / 180.0f; const float a_max = 405.0f * kPi / 180.0f; const ImU32 face_color = IM_COL32(25, 25, 28, 255); const ImU32 rim_color = IM_COL32(200, 200, 200, 255); const ImU32 tick_color = IM_COL32(220, 220, 220, 255); const ImU32 redline_col = IM_COL32(220, 40, 40, 255); const ImU32 needle_color = (rpm >= redline_rpm) ? IM_COL32(255, 60, 60, 255) : IM_COL32(255, 200, 60, 255); // Face draw_list->AddCircleFilled(center, radius, face_color, 64); draw_list->AddCircle(center, radius, rim_color, 64, 2.0f); // Tick marks + labels, one per 1000 rpm const int major_step = 1000; const int num_majors = static_cast(max_rpm) / major_step; for (int i = 0; i <= num_majors; ++i) { float t = (i * major_step) / max_rpm; float angle = a_min + t * (a_max - a_min); ImVec2 dir(std::cos(angle), std::sin(angle)); bool in_redline = (i * major_step) >= redline_rpm; ImU32 color = in_redline ? redline_col : tick_color; ImVec2 p_inner(center.x + dir.x * radius * 0.80f, center.y + dir.y * radius * 0.80f); ImVec2 p_outer(center.x + dir.x * radius * 0.95f, center.y + dir.y * radius * 0.95f); draw_list->AddLine(p_inner, p_outer, color, 2.0f); char buf[8]; std::snprintf(buf, sizeof(buf), "%d", i); ImVec2 text_size = ImGui::CalcTextSize(buf); ImVec2 p_label(center.x + dir.x * radius * 0.63f - text_size.x * 0.5f, center.y + dir.y * radius * 0.63f - text_size.y * 0.5f); draw_list->AddText(p_label, color, buf); } // Redline arc along the rim if (redline_rpm < max_rpm) { float t_redline = static_cast(redline_rpm / max_rpm); float redline_angle = a_min + t_redline * (a_max - a_min); draw_list->PathArcTo(center, radius * 0.95f, redline_angle, a_max); draw_list->PathStroke(redline_col, 0, 3.0f); } // Needle float rpm_clamped = rpm < 0.0f ? 0.0f : (rpm > max_rpm ? max_rpm : rpm); float t = rpm_clamped / max_rpm; float needle_angle = a_min + t * (a_max - a_min); ImVec2 needle_dir(std::cos(needle_angle), std::sin(needle_angle)); ImVec2 needle_tip(center.x + needle_dir.x * radius * 0.85f, center.y + needle_dir.y * radius * 0.85f); draw_list->AddLine(center, needle_tip, needle_color, 3.0f); draw_list->AddCircleFilled(center, radius * 0.06f, IM_COL32(230, 230, 230, 255), 24); // Digital readout under the needle hub char rpm_buf[32]; std::snprintf(rpm_buf, sizeof(rpm_buf), "%.0f RPM", rpm); ImVec2 rpm_text_size = ImGui::CalcTextSize(rpm_buf); draw_list->AddText(ImVec2(center.x - rpm_text_size.x * 0.5f, center.y + radius * 0.35f), IM_COL32(255, 255, 255, 255), rpm_buf); } bool Gui::init() { glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) return false; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); m_window = glfwCreateWindow(900, 600, "Engine Sim", nullptr, nullptr); if (!m_window) { glfwTerminate(); return false; } glfwMakeContextCurrent(m_window); glfwSwapInterval(1); // vsync IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui::StyleColorsDark(); ImGui_ImplGlfw_InitForOpenGL(m_window, true); ImGui_ImplOpenGL3_Init(); // nullptr glsl version -> backend picks one to match the hints above return true; } bool Gui::begin_frame() { if (glfwWindowShouldClose(m_window)) return false; glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); return true; } void Gui::render(const EngineStats& stats) { const ImGuiIO& io = ImGui::GetIO(); ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(io.DisplaySize); ImGui::Begin("Engine Sim", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); ImGui::Columns(2, "main_columns"); // --- Left column: primary live readouts --- DrawTachGauge(static_cast(stats.rpm), static_cast(stats.max_rpm), static_cast(stats.redline_rpm), 220.0f); ImGui::Text("Fuel Consumption: %.2f L/h", stats.fuel_consumption_lph); ImGui::Text("Crank Power: %.2f kW", stats.crank_power_kw); ImGui::Text("Output Heat: %.1f J", stats.output_heat_j); ImGui::NextColumn(); // --- Right column: extensible stats list (oil temp, coolant temp, ...) --- ImGui::Text("Stats"); ImGui::Separator(); for (const StatEntry& s : stats.extra_stats) { ImGui::Text("%s: %.2f %s", s.label.c_str(), s.value, s.unit.c_str()); } ImGui::Columns(1); ImGui::End(); } void Gui::end_frame() { ImGui::Render(); int display_w, display_h; glfwGetFramebufferSize(m_window, &display_w, &display_h); glViewport(0, 0, display_w, display_h); glClearColor(0.1f, 0.1f, 0.12f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(m_window); } void Gui::shutdown() { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(m_window); glfwTerminate(); }