Files
Avery Haas 1175b498b0 add to gui
2026-08-31 23:57:40 -04:00

205 lines
7.5 KiB
C++

#include "gui.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <GLFW/glfw3.h> // drags in the system GL headers we need below
#include <cmath>
#include <cstdio>
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<int>(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<float>(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");
// --- (0,0): primary live readouts — tach + throttle lever side by side ---
DrawTachGauge(static_cast<float>(stats.rpm), static_cast<float>(stats.max_rpm),
static_cast<float>(stats.redline_rpm), 220.0f);
ImGui::SameLine();
ImGui::VSliderFloat("##throttle_lever", ImVec2(40, 160), &m_throttle_lever, 0.0f, 1.0f, "");
// VSliderFloat is only "active" while the mouse is down on it, so
// this is the spring-back: anything other than an active drag
// resets the lever to closed, including on release.
if (!ImGui::IsItemActive()) {
m_throttle_lever = 0.0f;
}
ImGui::SameLine();
ImGui::Text("Throttle\n%.0f%%", m_throttle_lever * 100.0f);
// Row 1 starts here in both columns, so column 1's content below
// lines up with column 0's regardless of which column is taller.
float row1_y = ImGui::GetCursorPosY();
// --- (1,0): fuel/power readouts + accessory toggles ---
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::Spacing();
ImGui::Separator();
ImGui::Text("Accessories");
ImGui::Checkbox("Alternator", &m_accessories.alternator);
ImGui::Checkbox("A/C", &m_accessories.ac);
ImGui::Checkbox("Power Steering", &m_accessories.power_steering);
ImGui::NextColumn();
// --- (1,1): extensible stats list (oil temp, coolant temp, ...) ---
ImGui::SetCursorPosY(row1_y);
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();
}