From 5355a9ededa98ca2bd5bf5c9028b3c28fd677440 Mon Sep 17 00:00:00 2001 From: Mark Allyn Date: Wed, 17 Jun 2026 10:35:08 -0700 Subject: [PATCH] new code --- CMakeLists.txt | 38 ++ include/01_classes.h | 92 ++++ include/02_text_description.h | 96 ++++ include/settings.h | 76 +++ shaders/02_text_description_frag.glsl | 46 ++ shaders/02_text_description_vert.glsl | 46 ++ src/01_classes.cpp | 453 ++++++++++++++++ src/02_text_description.cpp | 278 ++++++++++ src/main.cpp | 711 ++++++++++++++++++++++++++ 9 files changed, 1836 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 include/01_classes.h create mode 100644 include/02_text_description.h create mode 100644 shaders/02_text_description_frag.glsl create mode 100644 shaders/02_text_description_vert.glsl create mode 100644 src/01_classes.cpp create mode 100644 src/02_text_description.cpp create mode 100644 src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..010a611 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) +project(radar_simulator) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(OpenGL REQUIRED) +find_package(glfw3 REQUIRED) +find_package(Freetype REQUIRED) +find_package(Threads REQUIRED) + +# GLAD is bundled in the project tree +add_library(glad STATIC + ${CMAKE_SOURCE_DIR}/glad/src/glad.c +) +target_include_directories(glad PUBLIC + ${CMAKE_SOURCE_DIR}/include +) + +add_executable(radar_simulator + ${CMAKE_SOURCE_DIR}/global_data.cpp + ${CMAKE_SOURCE_DIR}/src/main.cpp + ${CMAKE_SOURCE_DIR}/src/01_classes.cpp + ${CMAKE_SOURCE_DIR}/src/02_text_description.cpp +) + +target_include_directories(radar_simulator PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${FREETYPE_INCLUDE_DIRS} +) + +target_link_libraries(radar_simulator PRIVATE + glad + OpenGL::GL + glfw + ${FREETYPE_LIBRARIES} + Threads::Threads +) diff --git a/include/01_classes.h b/include/01_classes.h new file mode 100644 index 0000000..97fa66b --- /dev/null +++ b/include/01_classes.h @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * 01_classes.h + * Author: Mark Allyn + * Component: 01_classes + * + * Abstract RadarBase class and four stub subclasses. + * Each stub renders its name and description in the three screen panels. + * No real radar simulation is performed here; this is for testing + * the initialization and radar-management/selection flow. + */ + +#pragma once + +#include "data_structure_and_constants.h" +#include "settings.h" + +/* Abstract base — one instance per radar type. */ +class RadarBase { +public: + /* Render radar description and control list into the left panel viewport. */ + virtual void render_left_panel() = 0; + + /* Render scope content (stub: radar name only) into the scope viewport. */ + virtual void render_scope() = 0; + + /* Render current control values into the status bar viewport. */ + virtual void render_status_bar() = 0; + + virtual ~RadarBase() = default; +}; + +/* Chain Home radar (1940s A-scope) — Ventnor, Isle of Wight. */ +class ChainHomeRadar : public RadarBase { +public: + void render_left_panel() override; + void render_scope() override; + void render_status_bar() override; +}; + +/* Marine A-Scope radar — Community Boating Center, Bellingham Bay. */ +class MarineAScopeRadar : public RadarBase { +public: + void render_left_panel() override; + void render_scope() override; + void render_status_bar() override; +}; + +/* Marine Traffic Control PPI radar — center of Bellingham Bay. */ +class MarineTrafficRadar : public RadarBase { +public: + void render_left_panel() override; + void render_scope() override; + void render_status_bar() override; +}; + +/* Police Boat PPI radar — Taylor Dock / Boulevard Park, Bellingham Bay. */ +class PoliceboatRadar : public RadarBase { +public: + void render_left_panel() override; + void render_scope() override; + void render_status_bar() override; +}; + +/* One global instance per radar — indexed by RADAR_* constants in main.cpp. */ +extern ChainHomeRadar g_chain_home; +extern MarineAScopeRadar g_marine_ascope; +extern MarineTrafficRadar g_marine_traffic; +extern PoliceboatRadar g_police_boat; diff --git a/include/02_text_description.h b/include/02_text_description.h new file mode 100644 index 0000000..f750694 --- /dev/null +++ b/include/02_text_description.h @@ -0,0 +1,96 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * 02_text_description.h + * Author: Mark Allyn + * Component: 02_text_description + * + * FreeType-based text renderer used by all three screen panels + * (left description panel, scope panel, status bar). + * + * Usage: + * 1. Call g_text_renderer.initialize_shaders() after GLAD is loaded. + * 2. Call g_text_renderer.initialize_font(FONT_PATH, FONT_SIZE_NORMAL). + * 3. Before rendering into a viewport, call set_projection(w, h). + * 4. Call render_text() with viewport-local pixel coordinates. + */ + +#pragma once + +#include +#include +#include +#include "data_structure_and_constants.h" +#include "settings.h" + +/* Cached metrics for one loaded glyph. */ +struct glyph_character { + GLuint texture_id; // GL_RED single-channel texture + int size_x; // bitmap width in pixels + int size_y; // bitmap height in pixels + int bearing_x; // pixel offset from cursor to left of glyph + int bearing_y; // pixel offset from cursor baseline to top of glyph + unsigned int advance; // horizontal advance in 1/64-pixel units +}; + +class TextRenderer { +public: + TextRenderer() : vao_(0), vbo_(0), shader_program_(0) {} + + /* Compile and link 02_text_description_{vert,frag}.glsl. + * Must be called once, after GLAD is initialised. */ + bool initialize_shaders(); + + /* Load glyphs for printable ASCII (32–126) from the given font file. + * font_size is the desired pixel height. */ + bool initialize_font(const std::string& font_path, unsigned int font_size); + + /* Render text at (x, y) in viewport-local pixel coordinates. + * y=0 is the bottom of the current viewport. + * scale multiplies the loaded glyph size. + * color_r/g/b are 0.0–1.0. */ + void render_text(const std::string& text, + float x, float y, float scale, + float color_r, float color_g, float color_b); + + /* Return the rendered pixel width of text at the given scale. */ + float get_text_width(const std::string& text, float scale) const; + + /* Set the orthographic projection for a viewport of w × h pixels. + * Call this each time glViewport changes before rendering text. */ + void set_projection(int w, int h); + + /* Release all GL resources and glyph textures. */ + void cleanup(); + +private: + std::map characters_; + GLuint vao_; + GLuint vbo_; + GLuint shader_program_; +}; + +/* Global instance — shared by all panels and radar stubs. */ +extern TextRenderer g_text_renderer; diff --git a/include/settings.h b/include/settings.h index c73e84d..4fc0f43 100644 --- a/include/settings.h +++ b/include/settings.h @@ -126,3 +126,79 @@ #define PPI_BOAT_RANGE_STEP_3 6.0f #define PPI_BOAT_RANGE_STEP_4 12.0f #define PPI_BOAT_RANGE_STEP_5 24.0f + +/* ============================================================ + * WINDOW AND PANEL LAYOUT + * Component: Thread 1 (main.cpp), 01_classes, 02_text_description + * ============================================================ */ + +#define WINDOW_WIDTH 1920 +#define WINDOW_HEIGHT 1080 +#define WINDOW_TITLE "RAF / Marine Radar Exhibit" + +// Left text panel occupies left side; scope and status bar share the right side +#define LEFT_PANEL_WIDTH 500 +#define STATUS_BAR_HEIGHT 250 + +// Derived dimensions — scope area fills remaining right-side space +#define SCOPE_WIDTH (WINDOW_WIDTH - LEFT_PANEL_WIDTH) +#define SCOPE_HEIGHT (WINDOW_HEIGHT - STATUS_BAR_HEIGHT) + +/* ============================================================ + * TEXT RENDERING + * Component: 02_text_description + * ============================================================ */ + +#define FONT_PATH "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" +#define FONT_SIZE_NORMAL 18u +#define FONT_SIZE_TITLE 24u + +// Pixels between text baselines at scale 1.0 with FONT_SIZE_NORMAL +#define LINE_HEIGHT 24.0f + +// Pixel margin from panel edge for text placement +#define TEXT_MARGIN 10.0f + +/* ============================================================ + * SHADER DIRECTORY + * Component: all components that load shaders + * ============================================================ */ + +// Path is relative to the build/ directory where the executable lives +#define SHADER_DIR "../shaders/" + +/* ============================================================ + * KEYBOARD CONTROL STEP SIZES + * Component: Thread 1 keyboard handler (main.cpp) + * ============================================================ */ + +#define INTENSITY_STEP 0.05f +#define SENSITIVITY_STEP 0.05f +#define STC_SENSITIVITY_STEP 0.05f +#define STC_RANGE_STEP 0.05f +#define NOISE_FILTER_STEP 0.05f +#define RADIOGON_STEP 1.0f +#define BEARING_CURSOR_STEP 1.0f +#define RANGE_CURSOR_STEP 0.02f +#define MARINE_A_BEARING_STEP 1.0f +#define MAX_RANGE_STEP 0.1f + +/* ============================================================ + * KEYBOARD CONTROL LIMITS + * Component: Thread 1 keyboard handler (main.cpp) + * ============================================================ */ + +#define INTENSITY_MIN 0.0f +#define INTENSITY_MAX 2.0f +#define SENSITIVITY_MIN 0.0f +#define SENSITIVITY_MAX 2.0f +#define STC_SENSITIVITY_MIN 0.0f +#define STC_SENSITIVITY_MAX 1.0f +#define STC_RANGE_MIN 0.0f +#define STC_RANGE_MAX 1.0f +#define NOISE_FILTER_MIN 0.0f +#define NOISE_FILTER_MAX 1.0f +#define RANGE_CURSOR_MIN 0.0f +#define RANGE_CURSOR_MAX 1.0f +#define MAX_RANGE_MIN 0.1f +#define MAX_RANGE_MAX 1.0f diff --git a/shaders/02_text_description_frag.glsl b/shaders/02_text_description_frag.glsl new file mode 100644 index 0000000..ea3780d --- /dev/null +++ b/shaders/02_text_description_frag.glsl @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * 02_text_description_frag.glsl + * Author: Mark Allyn + * Component: 02_text_description + * + * Fragment shader for FreeType glyph rendering. + * Glyphs are uploaded as GL_RED single-channel textures; the red channel + * drives the alpha so the background shows through unfilled pixels. + */ + +#version 430 core + +in vec2 tex_coords; +out vec4 frag_color; + +uniform sampler2D text_sampler; // bound to texture unit 0 +uniform vec3 text_color; // RGB, 0.0–1.0 + +void main() { + float alpha = texture(text_sampler, tex_coords).r; + frag_color = vec4(text_color, alpha); +} diff --git a/shaders/02_text_description_vert.glsl b/shaders/02_text_description_vert.glsl new file mode 100644 index 0000000..cfb9f19 --- /dev/null +++ b/shaders/02_text_description_vert.glsl @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * 02_text_description_vert.glsl + * Author: Mark Allyn + * Component: 02_text_description + * + * Vertex shader for FreeType glyph quad rendering. + * Each vertex carries both its screen position and its texture coordinate + * packed into a single vec4 to minimise attribute slots. + */ + +#version 430 core + +layout (location = 0) in vec4 vertex; // xy = screen position (pixels), zw = texture coords + +out vec2 tex_coords; + +uniform mat4 projection; // orthographic: maps pixel space to NDC for current viewport + +void main() { + gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); + tex_coords = vertex.zw; +} diff --git a/src/01_classes.cpp b/src/01_classes.cpp new file mode 100644 index 0000000..55a24cd --- /dev/null +++ b/src/01_classes.cpp @@ -0,0 +1,453 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * 01_classes.cpp + * Author: Mark Allyn + * Component: 01_classes + * + * Stub implementations for all four radar subclasses. + * Each renders description text in the left panel, the radar name in the + * scope panel, and current control values in the status bar. + * No real radar simulation occurs here; this exists to test the + * initialization and radar-management/selection flow. + */ + +#include "01_classes.h" +#include "02_text_description.h" +#include "data_structure_and_constants.h" +#include "settings.h" + +#include +#include + +/* ------------------------------------------------------------------ */ +/* Global instances */ +/* ------------------------------------------------------------------ */ + +ChainHomeRadar g_chain_home; +MarineAScopeRadar g_marine_ascope; +MarineTrafficRadar g_marine_traffic; +PoliceboatRadar g_police_boat; + +/* ------------------------------------------------------------------ */ +/* Colour constants (RGB, 0.0–1.0) */ +/* ------------------------------------------------------------------ */ + +static const float COL_WHITE[3] = { 1.00f, 1.00f, 1.00f }; +static const float COL_RED[3] = { 1.00f, 0.25f, 0.25f }; +static const float COL_PINK[3] = { 1.00f, 0.60f, 0.80f }; +static const float COL_YELLOW[3] = { 1.00f, 1.00f, 0.00f }; +static const float COL_GREEN_DIM[3] = { 0.00f, 0.85f, 0.30f }; +static const float COL_GREEN_LO[3] = { 0.00f, 0.55f, 0.18f }; + +/* ------------------------------------------------------------------ */ +/* Layout helpers */ +/* ------------------------------------------------------------------ */ + +static const float SCALE_TITLE = 1.25f; +static const float SCALE_NORMAL = 1.00f; +static const float SCALE_SMALL = 0.85f; +static const float MARGIN = static_cast(TEXT_MARGIN); +static const float LH = LINE_HEIGHT; /* line height at scale 1.0 */ + +/* Render one line of text and advance y downward by one line. */ +static void text_line(const std::string& text, float x, float& y, + float scale, + float cr, float cg, float cb) { + g_text_renderer.render_text(text, x, y, scale, cr, cg, cb); + y -= LH * scale; +} + +/* Render a blank spacer line. */ +static void blank_line(float& y, float scale) { + y -= LH * scale * 0.6f; +} + +/* Render one control entry: label in red, keys in pink, on the same line. + * Format: " label_text key_lo / key_hi" + * Returns the new y after advancing one line. */ +static void control_line(const std::string& label, + const std::string& key_lo, const std::string& key_hi, + float x, float& y, float scale) { + float xp = x + 6.0f; + + /* Label in red */ + g_text_renderer.render_text(label, xp, y, scale, + COL_RED[0], COL_RED[1], COL_RED[2]); + xp += g_text_renderer.get_text_width(label, scale); + + /* Lower key in pink */ + g_text_renderer.render_text(key_lo, xp, y, scale, + COL_PINK[0], COL_PINK[1], COL_PINK[2]); + xp += g_text_renderer.get_text_width(key_lo, scale); + + /* Separator in red */ + g_text_renderer.render_text(" / ", xp, y, scale, + COL_RED[0], COL_RED[1], COL_RED[2]); + xp += g_text_renderer.get_text_width(" / ", scale); + + /* Upper key in pink */ + g_text_renderer.render_text(key_hi, xp, y, scale, + COL_PINK[0], COL_PINK[1], COL_PINK[2]); + + y -= LH * scale; +} + +/* ------------------------------------------------------------------ */ +/* Scope helper: render centred name + stub subtitle */ +/* ------------------------------------------------------------------ */ + +static void render_scope_name(const std::string& radar_name) { + const float scope_w = static_cast(SCOPE_WIDTH); + const float scope_h = static_cast(SCOPE_HEIGHT); + + float name_w = g_text_renderer.get_text_width(radar_name, 2.0f); + float nx = (scope_w - name_w) / 2.0f; + float ny = scope_h / 2.0f + 20.0f; + g_text_renderer.render_text(radar_name, nx, ny, 2.0f, + COL_GREEN_DIM[0], COL_GREEN_DIM[1], COL_GREEN_DIM[2]); + + std::string sub = "[ stub - initialization test only ]"; + float sub_w = g_text_renderer.get_text_width(sub, SCALE_NORMAL); + float sx = (scope_w - sub_w) / 2.0f; + float sy = ny - LH * 2.0f; + g_text_renderer.render_text(sub, sx, sy, SCALE_NORMAL, + COL_GREEN_LO[0], COL_GREEN_LO[1], COL_GREEN_LO[2]); +} + +/* ------------------------------------------------------------------ */ +/* Status bar helper: common controls (intensity, sensitivity, STC) */ +/* ------------------------------------------------------------------ */ + +static void render_common_status(float x, float& y, float scale) { + char buf[320]; + snprintf(buf, sizeof(buf), + "Intensity: %.2f | Sensitivity: %.2f | STC Sens: %.2f | STC Range: %.2f", + uniform_intensity, uniform_sensitivity, + uniform_stc_sensitivity, uniform_stc_range); + g_text_renderer.render_text(buf, x, y, scale, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); + y -= LH * scale; +} + +/* ================================================================== */ +/* ChainHomeRadar */ +/* ================================================================== */ + +void ChainHomeRadar::render_left_panel() { + float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE; + float x = MARGIN; + + text_line("CHAIN HOME RADAR", x, y, SCALE_TITLE, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Location: Ventnor, Isle of Wight", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("The UK developed Chain Home in the 1940s", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("to detect German aircraft attacking across", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("the English Channel. It was one of the", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("first operational radar systems ever built.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("The scope shows range on the horizontal", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("axis and echo amplitude on the vertical.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Bearing is found with the radiogoniometer:", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("rotate the search coil until the echo", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("amplitude reaches a minimum (null point).", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + /* Controls header */ + text_line("--- CONTROLS ---", x, y, SCALE_NORMAL, + COL_RED[0], COL_RED[1], COL_RED[2]); + + control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL); + control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL); + control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL); + control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL); + control_line("Radiogoniometer: ", "U", "I", x, y, SCALE_NORMAL); + blank_line(y, SCALE_NORMAL); + + text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +void ChainHomeRadar::render_scope() { + render_scope_name("CHAIN HOME RADAR"); +} + +void ChainHomeRadar::render_status_bar() { + float x = MARGIN; + float y = static_cast(STATUS_BAR_HEIGHT) - MARGIN - LH; + + text_line("CHAIN HOME RADAR (Ventnor, Isle of Wight) | Fixed range: 200 km", + x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); + + render_common_status(x, y, SCALE_NORMAL); + + char buf[200]; + snprintf(buf, sizeof(buf), "Radiogoniometer: %.1f deg", uniform_radiogoniometer); + g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +/* ================================================================== */ +/* MarineAScopeRadar */ +/* ================================================================== */ + +void MarineAScopeRadar::render_left_panel() { + float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE; + float x = MARGIN; + + text_line("MARINE A-SCOPE RADAR", x, y, SCALE_TITLE, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Location: Community Boating Center,", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("Bellingham Bay, Washington", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Microwave radar was developed after", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("Chain Home. Its shorter wavelengths", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("could detect small targets such as a", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("submarine periscope or conning tower.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("A steerable dish is connected to an", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("oscilloscope. Bearing is determined by", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("rotating the dish with the servo control.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("--- CONTROLS ---", x, y, SCALE_NORMAL, + COL_RED[0], COL_RED[1], COL_RED[2]); + + control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL); + control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL); + control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL); + control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL); + control_line("Max Range: ", "O", "P", x, y, SCALE_NORMAL); + control_line("Antenna Rotation: ", "G", "H", x, y, SCALE_NORMAL); + blank_line(y, SCALE_NORMAL); + + text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +void MarineAScopeRadar::render_scope() { + render_scope_name("MARINE A-SCOPE RADAR"); +} + +void MarineAScopeRadar::render_status_bar() { + float x = MARGIN; + float y = static_cast(STATUS_BAR_HEIGHT) - MARGIN - LH; + + text_line("MARINE A-SCOPE RADAR (Community Boating Center, Bellingham Bay)", + x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); + + render_common_status(x, y, SCALE_NORMAL); + + char buf[200]; + snprintf(buf, sizeof(buf), + "Max Range (normalized): %.2f | Antenna Bearing: %.1f deg", + uniform_max_range, uniform_marine_a_bearing); + g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +/* ================================================================== */ +/* MarineTrafficRadar */ +/* ================================================================== */ + +void MarineTrafficRadar::render_left_panel() { + float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE; + float x = MARGIN; + + text_line("MARINE TRAFFIC CONTROL RADAR", x, y, SCALE_TITLE, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Location: Center of Bellingham Bay,", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("Washington", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("The Plan Position Indicator (PPI) radar", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("sweeps a beam clockwise from north.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("Range is the distance of a blip from", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("centre; bearing is the beam direction.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("All modern marine radars use this type", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("of display. Coastal guard stations use", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("them to manage harbour traffic.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("--- CONTROLS ---", x, y, SCALE_NORMAL, + COL_RED[0], COL_RED[1], COL_RED[2]); + + control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL); + control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL); + control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL); + control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL); + control_line("Rain Noise Filter: ", "T", "Y", x, y, SCALE_NORMAL); + control_line("Max Range: ", "O", "P", x, y, SCALE_NORMAL); + control_line("Range Cursor: ", "A", "S", x, y, SCALE_NORMAL); + control_line("Bearing Cursor: ", "D", "F", x, y, SCALE_NORMAL); + blank_line(y, SCALE_NORMAL); + + text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +void MarineTrafficRadar::render_scope() { + render_scope_name("MARINE TRAFFIC CONTROL RADAR"); +} + +void MarineTrafficRadar::render_status_bar() { + float x = MARGIN; + float y = static_cast(STATUS_BAR_HEIGHT) - MARGIN - LH; + + text_line("MARINE TRAFFIC CONTROL RADAR (Bellingham Bay)", + x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); + + render_common_status(x, y, SCALE_NORMAL); + + char buf[280]; + snprintf(buf, sizeof(buf), + "Noise Filter: %.2f | Max Range (norm): %.2f | " + "Range Cursor: %.2f | Bearing Cursor: %.1f deg", + uniform_noise_filter, uniform_max_range, + uniform_range_cursor, uniform_bearing_cursor); + g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +/* ================================================================== */ +/* PoliceboatRadar */ +/* ================================================================== */ + +void PoliceboatRadar::render_left_panel() { + float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH * SCALE_TITLE; + float x = MARGIN; + + text_line("POLICE BOAT RADAR", x, y, SCALE_TITLE, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Starting location: Taylor Dock /", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("Boulevard Park, Bellingham Bay", x, y, SCALE_SMALL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("This PPI radar is mounted on a moving", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("police boat patrolling Bellingham Bay.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("The sweep rotates clockwise relative to", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("the boat's heading, not true north. A", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("small indicator shows where north is.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("Watch how the display changes as the", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + text_line("boat moves and turns.", x, y, SCALE_NORMAL, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + blank_line(y, SCALE_NORMAL); + + text_line("--- CONTROLS ---", x, y, SCALE_NORMAL, + COL_RED[0], COL_RED[1], COL_RED[2]); + + control_line("Intensity: ", "3", "4", x, y, SCALE_NORMAL); + control_line("Sensitivity: ", "5", "6", x, y, SCALE_NORMAL); + control_line("STC Sensitivity: ", "Q", "W", x, y, SCALE_NORMAL); + control_line("STC Range: ", "E", "R", x, y, SCALE_NORMAL); + control_line("Rain Noise Filter: ", "T", "Y", x, y, SCALE_NORMAL); + control_line("Max Range: ", "O", "P", x, y, SCALE_NORMAL); + control_line("Range Cursor: ", "A", "S", x, y, SCALE_NORMAL); + control_line("Bearing Cursor: ", "D", "F", x, y, SCALE_NORMAL); + blank_line(y, SCALE_NORMAL); + + text_line("1 - Return to Radar Selection", x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} + +void PoliceboatRadar::render_scope() { + render_scope_name("POLICE BOAT RADAR"); +} + +void PoliceboatRadar::render_status_bar() { + float x = MARGIN; + float y = static_cast(STATUS_BAR_HEIGHT) - MARGIN - LH; + + text_line("POLICE BOAT RADAR (Bellingham Bay - moving)", + x, y, SCALE_NORMAL, COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); + + render_common_status(x, y, SCALE_NORMAL); + + char buf[280]; + snprintf(buf, sizeof(buf), + "Noise Filter: %.2f | Max Range (norm): %.2f | " + "Range Cursor: %.2f | Bearing Cursor: %.1f deg", + uniform_noise_filter, uniform_max_range, + uniform_range_cursor, uniform_bearing_cursor); + g_text_renderer.render_text(buf, x, y, SCALE_NORMAL, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); +} diff --git a/src/02_text_description.cpp b/src/02_text_description.cpp new file mode 100644 index 0000000..b9bbe0d --- /dev/null +++ b/src/02_text_description.cpp @@ -0,0 +1,278 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * 02_text_description.cpp + * Author: Mark Allyn + * Component: 02_text_description + * + * FreeType-based text renderer. Loads ASCII glyphs into GL_RED textures + * and renders them as textured quads into whichever viewport is active. + */ + +#include "02_text_description.h" + +#include +#include FT_FREETYPE_H + +#include +#include +#include + +/* Global singleton used by all components. */ +TextRenderer g_text_renderer; + +/* ------------------------------------------------------------------ */ +/* Internal helpers */ +/* ------------------------------------------------------------------ */ + +static bool read_file(const std::string& path, std::string& out) { + std::ifstream f(path); + if (!f.is_open()) { + fprintf(stderr, "[02_text] cannot open shader: %s\n", path.c_str()); + return false; + } + std::ostringstream ss; + ss << f.rdbuf(); + out = ss.str(); + return true; +} + +static GLuint compile_shader_src(const char* source, GLenum type) { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + GLint ok = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + if (!ok) { + char log[512]; + glGetShaderInfoLog(shader, 512, nullptr, log); + fprintf(stderr, "[02_text] shader compile error:\n%s\n", log); + } + return shader; +} + +/* ------------------------------------------------------------------ */ +/* TextRenderer public methods */ +/* ------------------------------------------------------------------ */ + +bool TextRenderer::initialize_shaders() { + std::string vert_src, frag_src; + if (!read_file(SHADER_DIR "02_text_description_vert.glsl", vert_src)) return false; + if (!read_file(SHADER_DIR "02_text_description_frag.glsl", frag_src)) return false; + + GLuint vert = compile_shader_src(vert_src.c_str(), GL_VERTEX_SHADER); + GLuint frag = compile_shader_src(frag_src.c_str(), GL_FRAGMENT_SHADER); + + shader_program_ = glCreateProgram(); + glAttachShader(shader_program_, vert); + glAttachShader(shader_program_, frag); + glLinkProgram(shader_program_); + + GLint ok = 0; + glGetProgramiv(shader_program_, GL_LINK_STATUS, &ok); + if (!ok) { + char log[512]; + glGetProgramInfoLog(shader_program_, 512, nullptr, log); + fprintf(stderr, "[02_text] shader link error:\n%s\n", log); + glDeleteShader(vert); + glDeleteShader(frag); + return false; + } + + glDeleteShader(vert); + glDeleteShader(frag); + + /* VAO/VBO for one glyph quad: 6 vertices × 4 floats (xy + uv). */ + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); + + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, nullptr, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast(0)); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + /* Bind text_sampler uniform to texture unit 0. */ + glUseProgram(shader_program_); + glUniform1i(glGetUniformLocation(shader_program_, "text_sampler"), 0); + glUseProgram(0); + +#if DEBUG_SHADER_COMPILE + fprintf(stdout, "[02_text] shaders compiled and linked OK\n"); +#endif + + return true; +} + +bool TextRenderer::initialize_font(const std::string& font_path, unsigned int font_size) { + FT_Library ft_lib; + if (FT_Init_FreeType(&ft_lib)) { + fprintf(stderr, "[02_text] FreeType init failed\n"); + return false; + } + + FT_Face ft_face; + if (FT_New_Face(ft_lib, font_path.c_str(), 0, &ft_face)) { + fprintf(stderr, "[02_text] cannot load font: %s\n", font_path.c_str()); + FT_Done_FreeType(ft_lib); + return false; + } + + FT_Set_Pixel_Sizes(ft_face, 0, font_size); + + /* Upload each printable ASCII glyph as a GL_RED texture. */ + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + for (unsigned char c = 32; c < 127; c++) { + if (FT_Load_Char(ft_face, c, FT_LOAD_RENDER)) { + fprintf(stderr, "[02_text] failed to load glyph 0x%02x\n", c); + continue; + } + + GLuint tex = 0; + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexImage2D( + GL_TEXTURE_2D, 0, GL_RED, + static_cast(ft_face->glyph->bitmap.width), + static_cast(ft_face->glyph->bitmap.rows), + 0, GL_RED, GL_UNSIGNED_BYTE, + ft_face->glyph->bitmap.buffer + ); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glyph_character ch; + ch.texture_id = tex; + ch.size_x = static_cast(ft_face->glyph->bitmap.width); + ch.size_y = static_cast(ft_face->glyph->bitmap.rows); + ch.bearing_x = ft_face->glyph->bitmap_left; + ch.bearing_y = ft_face->glyph->bitmap_top; + ch.advance = static_cast(ft_face->glyph->advance.x); + characters_[static_cast(c)] = ch; + } + + glBindTexture(GL_TEXTURE_2D, 0); + FT_Done_Face(ft_face); + FT_Done_FreeType(ft_lib); + + fprintf(stdout, "[02_text] font loaded: %s at %u px\n", font_path.c_str(), font_size); + return true; +} + +void TextRenderer::render_text(const std::string& text, + float x, float y, float scale, + float color_r, float color_g, float color_b) { + glUseProgram(shader_program_); + glUniform3f(glGetUniformLocation(shader_program_, "text_color"), + color_r, color_g, color_b); + glActiveTexture(GL_TEXTURE0); + glBindVertexArray(vao_); + + for (std::string::const_iterator it = text.cbegin(); it != text.cend(); ++it) { + char c = *it; + if (characters_.count(c) == 0) { + /* Advance by a fixed amount for unknown characters. */ + x += 10.0f * scale; + continue; + } + + const glyph_character& ch = characters_.at(c); + + float xpos = x + static_cast(ch.bearing_x) * scale; + float ypos = y - static_cast(ch.size_y - ch.bearing_y) * scale; + float w = static_cast(ch.size_x) * scale; + float h = static_cast(ch.size_y) * scale; + + if (w > 0.0f && h > 0.0f) { + float vertices[6][4] = { + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos, ypos, 0.0f, 1.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + { xpos + w, ypos + h, 1.0f, 0.0f } + }; + glBindTexture(GL_TEXTURE_2D, ch.texture_id); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + x += static_cast(ch.advance >> 6) * scale; + } + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); +} + +float TextRenderer::get_text_width(const std::string& text, float scale) const { + float width = 0.0f; + for (std::string::const_iterator it = text.cbegin(); it != text.cend(); ++it) { + char c = *it; + if (characters_.count(c) == 0) { + width += 10.0f * scale; + continue; + } + width += static_cast(characters_.at(c).advance >> 6) * scale; + } + return width; +} + +void TextRenderer::set_projection(int w, int h) { + /* Column-major orthographic matrix mapping pixel space [0,w]x[0,h] + * to NDC. Near=-1, far=1 (depth unused for 2D text). */ + float fw = static_cast(w); + float fh = static_cast(h); + float proj[16] = { + 2.0f / fw, 0.0f, 0.0f, 0.0f, + 0.0f, 2.0f / fh, 0.0f, 0.0f, + 0.0f, 0.0f, -1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, 1.0f + }; + glUseProgram(shader_program_); + glUniformMatrix4fv( + glGetUniformLocation(shader_program_, "projection"), + 1, GL_FALSE, proj + ); + glUseProgram(0); +} + +void TextRenderer::cleanup() { + for (std::map::iterator it = characters_.begin(); + it != characters_.end(); ++it) { + glDeleteTextures(1, &it->second.texture_id); + } + characters_.clear(); + + if (vao_) { glDeleteVertexArrays(1, &vao_); vao_ = 0; } + if (vbo_) { glDeleteBuffers(1, &vbo_); vbo_ = 0; } + if (shader_program_) { glDeleteProgram(shader_program_); shader_program_ = 0; } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..cc8b1af --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,711 @@ +/* + * MIT License + * + * Copyright (c) 2026 Mark Allyn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * main.cpp + * Author: Mark Allyn + * Component: 03_setup_and_radar_selection (spec: 03_setup_and_radar_selection.md) + * + * Entry point. Handles: + * - Command-line parsing to set radar availability. + * - OpenGL / GLFW / GLAD initialisation (Thread 1). + * - GL debug output callback. + * - Shader and font loading for all components. + * - Radar selection loop (intro screen). + * - Main render loop at ~30 Hz. + * - Keyboard callbacks for radar selection and scope controls. + * + * Thread: Thread 1 (all GL and keyboard work lives here). + */ + +/* GLAD must be included before any other OpenGL header. */ +#include +#include + +#include "data_structure_and_constants.h" +#include "settings.h" +#include "01_classes.h" +#include "02_text_description.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Application state */ +/* ------------------------------------------------------------------ */ + +enum class AppState { + SELECTION, /* intro / radar-selection screen */ + OPERATING /* a radar is active */ +}; + +static AppState g_app_state = AppState::SELECTION; + +/* Entry in the selectable-radar list (built from radar_management[]). */ +struct selectable_radar { + int management_index; /* RADAR_* constant */ + std::string display_name; + RadarBase* instance; +}; + +static std::vector g_selectable; +static int g_selection_cursor = 0; + +/* Pointer to the currently operating radar object (null in SELECTION state). */ +static RadarBase* g_active_radar = nullptr; + +/* ------------------------------------------------------------------ */ +/* Colour constants for selection / intro rendering */ +/* ------------------------------------------------------------------ */ + +static const float COL_WHITE[3] = { 1.00f, 1.00f, 1.00f }; +static const float COL_YELLOW[3] = { 1.00f, 1.00f, 0.00f }; +static const float COL_GREEN_HI[3] = { 0.20f, 1.00f, 0.40f }; /* highlighted item */ +static const float COL_GREEN_DIM[3] = { 0.00f, 0.65f, 0.22f }; /* non-selected item */ +static const float COL_PINK[3] = { 1.00f, 0.60f, 0.80f }; +static const float COL_RED[3] = { 1.00f, 0.25f, 0.25f }; + +static const float LH_MAIN = LINE_HEIGHT; +static const float MARGIN = static_cast(TEXT_MARGIN); + +/* ------------------------------------------------------------------ */ +/* GL debug callback (GPU robustness protocol) */ +/* ------------------------------------------------------------------ */ + +static void GLAPIENTRY gl_debug_callback(GLenum /*source*/, + GLenum type, + GLuint /*id*/, + GLenum severity, + GLsizei /*length*/, + const GLchar* message, + const void* /*user_param*/) { + /* Skip verbose notifications to keep output readable. */ + if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return; + + const char* type_str = "UNKNOWN"; + switch (type) { + case GL_DEBUG_TYPE_ERROR: type_str = "ERROR"; break; + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: type_str = "DEPRECATED"; break; + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: type_str = "UNDEFINED_BEHAVIOUR"; break; + case GL_DEBUG_TYPE_PERFORMANCE: type_str = "PERFORMANCE"; break; + case GL_DEBUG_TYPE_PORTABILITY: type_str = "PORTABILITY"; break; + default: break; + } + fprintf(stderr, "[GL %s] %s\n", type_str, message); +} + +/* ------------------------------------------------------------------ */ +/* Keyboard callback (Thread 1) */ +/* ------------------------------------------------------------------ */ + +static void key_callback(GLFWwindow* window, int key, int /*scancode*/, + int action, int /*mods*/) { + if (action != GLFW_PRESS && action != GLFW_REPEAT) return; + + if (key == GLFW_KEY_ESCAPE) { + glfwSetWindowShouldClose(window, GLFW_TRUE); + return; + } + + /* ---- SELECTION state ---- */ + if (g_app_state == AppState::SELECTION) { + if (g_selectable.empty()) return; + + if (key == GLFW_KEY_1) { + g_selection_cursor = + (g_selection_cursor + 1) % static_cast(g_selectable.size()); + } else if (key == GLFW_KEY_2) { + g_selection_cursor = + (g_selection_cursor - 1 + static_cast(g_selectable.size())) + % static_cast(g_selectable.size()); + } else if (key == GLFW_KEY_ENTER || key == GLFW_KEY_KP_ENTER) { + /* Activate selected radar. */ + int mi = g_selectable[g_selection_cursor].management_index; + for (int i = 0; i < RADAR_COUNT; i++) radar_management[i].selected = 0; + radar_management[mi].selected = 1; + current_radar = mi; + g_active_radar = g_selectable[g_selection_cursor].instance; + g_app_state = AppState::OPERATING; + } + return; + } + + /* ---- OPERATING state ---- */ + + /* '1' returns to selection from any operating radar. */ + if (key == GLFW_KEY_1) { + radar_management[current_radar].selected = 0; + current_radar = RADAR_NONE; + g_active_radar = nullptr; + g_app_state = AppState::SELECTION; + return; + } + + /* Scope controls — applied to whichever radar is active. */ + switch (key) { + /* Intensity (all scopes) */ + case GLFW_KEY_3: + uniform_intensity = std::max(INTENSITY_MIN, + uniform_intensity - INTENSITY_STEP); + break; + case GLFW_KEY_4: + uniform_intensity = std::min(INTENSITY_MAX, + uniform_intensity + INTENSITY_STEP); + break; + + /* Receiver sensitivity (all scopes) */ + case GLFW_KEY_5: + uniform_sensitivity = std::max(SENSITIVITY_MIN, + uniform_sensitivity - SENSITIVITY_STEP); + break; + case GLFW_KEY_6: + uniform_sensitivity = std::min(SENSITIVITY_MAX, + uniform_sensitivity + SENSITIVITY_STEP); + break; + + /* STC sensitivity (all scopes) */ + case GLFW_KEY_Q: + uniform_stc_sensitivity = std::max(STC_SENSITIVITY_MIN, + uniform_stc_sensitivity - STC_SENSITIVITY_STEP); + break; + case GLFW_KEY_W: + uniform_stc_sensitivity = std::min(STC_SENSITIVITY_MAX, + uniform_stc_sensitivity + STC_SENSITIVITY_STEP); + break; + + /* STC range (all scopes) */ + case GLFW_KEY_E: + uniform_stc_range = std::max(STC_RANGE_MIN, + uniform_stc_range - STC_RANGE_STEP); + break; + case GLFW_KEY_R: + uniform_stc_range = std::min(STC_RANGE_MAX, + uniform_stc_range + STC_RANGE_STEP); + break; + + /* Rain noise filter (PPI scopes only) */ + case GLFW_KEY_T: + if (current_radar == RADAR_MARINE_TRAFFIC || + current_radar == RADAR_POLICE_BOAT) { + uniform_noise_filter = std::max(NOISE_FILTER_MIN, + uniform_noise_filter - NOISE_FILTER_STEP); + } + break; + case GLFW_KEY_Y: + if (current_radar == RADAR_MARINE_TRAFFIC || + current_radar == RADAR_POLICE_BOAT) { + uniform_noise_filter = std::min(NOISE_FILTER_MAX, + uniform_noise_filter + NOISE_FILTER_STEP); + } + break; + + /* Radiogoniometer (Chain Home only) */ + case GLFW_KEY_U: + if (current_radar == RADAR_CHAIN_HOME) { + uniform_radiogoniometer -= RADIOGON_STEP; + if (uniform_radiogoniometer < 0.0f) + uniform_radiogoniometer += 360.0f; + } + break; + case GLFW_KEY_I: + if (current_radar == RADAR_CHAIN_HOME) { + uniform_radiogoniometer += RADIOGON_STEP; + if (uniform_radiogoniometer >= 360.0f) + uniform_radiogoniometer -= 360.0f; + } + break; + + /* Max range (all scopes except Chain Home) */ + case GLFW_KEY_O: + if (current_radar != RADAR_CHAIN_HOME) { + uniform_max_range = std::max(MAX_RANGE_MIN, + uniform_max_range - MAX_RANGE_STEP); + } + break; + case GLFW_KEY_P: + if (current_radar != RADAR_CHAIN_HOME) { + uniform_max_range = std::min(MAX_RANGE_MAX, + uniform_max_range + MAX_RANGE_STEP); + } + break; + + /* Range cursor (PPI scopes only) */ + case GLFW_KEY_A: + if (current_radar == RADAR_MARINE_TRAFFIC || + current_radar == RADAR_POLICE_BOAT) { + uniform_range_cursor = std::max(RANGE_CURSOR_MIN, + uniform_range_cursor - RANGE_CURSOR_STEP); + } + break; + case GLFW_KEY_S: + if (current_radar == RADAR_MARINE_TRAFFIC || + current_radar == RADAR_POLICE_BOAT) { + uniform_range_cursor = std::min(RANGE_CURSOR_MAX, + uniform_range_cursor + RANGE_CURSOR_STEP); + } + break; + + /* Bearing cursor (PPI scopes only) */ + case GLFW_KEY_D: + if (current_radar == RADAR_MARINE_TRAFFIC || + current_radar == RADAR_POLICE_BOAT) { + uniform_bearing_cursor -= BEARING_CURSOR_STEP; + if (uniform_bearing_cursor < 0.0f) + uniform_bearing_cursor += 360.0f; + } + break; + case GLFW_KEY_F: + if (current_radar == RADAR_MARINE_TRAFFIC || + current_radar == RADAR_POLICE_BOAT) { + uniform_bearing_cursor += BEARING_CURSOR_STEP; + if (uniform_bearing_cursor >= 360.0f) + uniform_bearing_cursor -= 360.0f; + } + break; + + /* Marine A-scope antenna rotation */ + case GLFW_KEY_G: + if (current_radar == RADAR_MARINE_ASCOPE) { + uniform_marine_a_bearing -= MARINE_A_BEARING_STEP; + if (uniform_marine_a_bearing < 0.0f) + uniform_marine_a_bearing += 360.0f; + } + break; + case GLFW_KEY_H: + if (current_radar == RADAR_MARINE_ASCOPE) { + uniform_marine_a_bearing += MARINE_A_BEARING_STEP; + if (uniform_marine_a_bearing >= 360.0f) + uniform_marine_a_bearing -= 360.0f; + } + break; + + default: + break; + } +} + +/* ------------------------------------------------------------------ */ +/* parse_args: set radar availability from command line */ +/* ------------------------------------------------------------------ */ + +static void parse_args(int argc, char* argv[]) { + /* Stub build: mark all four radars as operatable (fully built). */ + for (int i = 1; i < RADAR_COUNT; i++) { + radar_management[i].operatable = 1; + } + + if (argc <= 1) { + /* No arguments — all operatable radars are available. */ + for (int i = 1; i < RADAR_COUNT; i++) { + if (radar_management[i].operatable) + radar_management[i].available = 1; + } + } else { + for (int a = 1; a < argc; a++) { + std::string arg = argv[a]; + if (arg == "chain_home") radar_management[RADAR_CHAIN_HOME].available = 1; + else if (arg == "marine_ascope") radar_management[RADAR_MARINE_ASCOPE].available = 1; + else if (arg == "marine_traffic")radar_management[RADAR_MARINE_TRAFFIC].available = 1; + else if (arg == "police_boat") radar_management[RADAR_POLICE_BOAT].available = 1; + else fprintf(stderr, "[main] unknown radar name ignored: %s\n", arg.c_str()); + } + } + + /* Build the selectable list in the canonical order. */ + g_selectable.clear(); + + struct radar_meta { int idx; const char* name; RadarBase* obj; }; + radar_meta entries[4] = { + { RADAR_CHAIN_HOME, "Chain Home Radar", &g_chain_home }, + { RADAR_MARINE_ASCOPE, "Marine A-Scope Radar", &g_marine_ascope}, + { RADAR_MARINE_TRAFFIC, "Marine Traffic Control Radar", &g_marine_traffic}, + { RADAR_POLICE_BOAT, "Police Boat Radar", &g_police_boat }, + }; + + for (int e = 0; e < 4; e++) { + if (radar_management[entries[e].idx].available) { + selectable_radar sr; + sr.management_index = entries[e].idx; + sr.display_name = entries[e].name; + sr.instance = entries[e].obj; + g_selectable.push_back(sr); + } + } + + if (g_selectable.empty()) { + fprintf(stderr, "[main] WARNING: no radars are available. " + "Run with no args to enable all.\n"); + } + + fprintf(stdout, "[main] %zu radar(s) available.\n", g_selectable.size()); +} + +/* ------------------------------------------------------------------ */ +/* Intro / selection screen rendering */ +/* ------------------------------------------------------------------ */ + +static void render_intro_left_panel() { + float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH_MAIN * 1.25f; + float x = MARGIN; + + g_text_renderer.render_text("RAF / MARINE RADAR EXHIBIT", x, y, 1.25f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + y -= LH_MAIN * 1.25f; + + y -= LH_MAIN * 0.5f; + + const char* intro[] = { + "Welcome to the radar exhibit.", + "", + "This exhibit lets you experience", + "radar technology from the 1940s", + "and 1950s — from the Chain Home", + "system that helped defend Britain", + "in World War 2, to the marine PPI", + "radars used by the Coast Guard", + "and police today.", + "", + "Use the controls on the right to", + "select a radar and explore it.", + nullptr + }; + + for (int i = 0; intro[i] != nullptr; i++) { + if (intro[i][0] == '\0') { + y -= LH_MAIN * 0.5f; + } else { + g_text_renderer.render_text(intro[i], x, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + y -= LH_MAIN; + } + } +} + +static void render_radar_list() { + float scope_w = static_cast(SCOPE_WIDTH); + float scope_h = static_cast(SCOPE_HEIGHT); + + /* Title */ + std::string title = "SELECT A RADAR"; + float tw = g_text_renderer.get_text_width(title, 1.5f); + g_text_renderer.render_text(title, + (scope_w - tw) / 2.0f, + scope_h - 60.0f, + 1.5f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + + if (g_selectable.empty()) { + std::string msg = "No radars available. Restart with radar names as arguments."; + float mw = g_text_renderer.get_text_width(msg, 1.0f); + g_text_renderer.render_text(msg, + (scope_w - mw) / 2.0f, + scope_h / 2.0f, + 1.0f, + COL_YELLOW[0], COL_YELLOW[1], COL_YELLOW[2]); + return; + } + + /* List each available radar; highlight the current selection. */ + int count = static_cast(g_selectable.size()); + float list_y_start = scope_h / 2.0f + (count * LH_MAIN * 1.4f) / 2.0f; + + for (int i = 0; i < count; i++) { + std::string entry = g_selectable[i].display_name; + bool highlighted = (i == g_selection_cursor); + + float scale = highlighted ? 1.3f : 1.0f; + const float* col = highlighted ? COL_GREEN_HI : COL_GREEN_DIM; + + std::string prefix = highlighted ? "> " : " "; + std::string line = prefix + entry; + + float lw = g_text_renderer.get_text_width(line, scale); + float lx = (scope_w - lw) / 2.0f; + float ly = list_y_start - i * LH_MAIN * 1.4f; + + g_text_renderer.render_text(line, lx, ly, scale, + col[0], col[1], col[2]); + } +} + +static void render_selection_status_bar() { + float x = MARGIN; + float y = static_cast(STATUS_BAR_HEIGHT) - MARGIN - LH_MAIN; + + g_text_renderer.render_text("Use ", x, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + float xp = x + g_text_renderer.get_text_width("Use ", 1.0f); + + g_text_renderer.render_text("1", xp, y, 1.0f, + COL_PINK[0], COL_PINK[1], COL_PINK[2]); + xp += g_text_renderer.get_text_width("1", 1.0f); + + g_text_renderer.render_text(" / ", xp, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + xp += g_text_renderer.get_text_width(" / ", 1.0f); + + g_text_renderer.render_text("2", xp, y, 1.0f, + COL_PINK[0], COL_PINK[1], COL_PINK[2]); + xp += g_text_renderer.get_text_width("2", 1.0f); + + g_text_renderer.render_text(" to move the selection. Press ", xp, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + xp += g_text_renderer.get_text_width(" to move the selection. Press ", 1.0f); + + g_text_renderer.render_text("ENTER", xp, y, 1.0f, + COL_PINK[0], COL_PINK[1], COL_PINK[2]); + xp += g_text_renderer.get_text_width("ENTER", 1.0f); + + g_text_renderer.render_text(" to activate.", xp, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + + y -= LH_MAIN; + g_text_renderer.render_text("Press ", x, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); + xp = x + g_text_renderer.get_text_width("Press ", 1.0f); + g_text_renderer.render_text("ESC", xp, y, 1.0f, + COL_PINK[0], COL_PINK[1], COL_PINK[2]); + xp += g_text_renderer.get_text_width("ESC", 1.0f); + g_text_renderer.render_text(" to exit the exhibit.", xp, y, 1.0f, + COL_WHITE[0], COL_WHITE[1], COL_WHITE[2]); +} + +/* ------------------------------------------------------------------ */ +/* Viewport rendering orchestrators */ +/* ------------------------------------------------------------------ */ + +static void render_selection_screen() { + int scope_x = LEFT_PANEL_WIDTH; + int scope_y = STATUS_BAR_HEIGHT; + int scope_w = SCOPE_WIDTH; + int scope_h = SCOPE_HEIGHT; + + /* Left panel */ + glViewport(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + glScissor(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + glEnable(GL_SCISSOR_TEST); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + render_intro_left_panel(); + + /* Scope area — radar list */ + glViewport(scope_x, scope_y, scope_w, scope_h); + glScissor(scope_x, scope_y, scope_w, scope_h); + glClearColor(0.0f, 0.05f, 0.01f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(scope_w, scope_h); + render_radar_list(); + + /* Status bar */ + glViewport(scope_x, 0, scope_w, STATUS_BAR_HEIGHT); + glScissor(scope_x, 0, scope_w, STATUS_BAR_HEIGHT); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(scope_w, STATUS_BAR_HEIGHT); + render_selection_status_bar(); + + glDisable(GL_SCISSOR_TEST); +} + +static void render_operating_screen() { + int scope_x = LEFT_PANEL_WIDTH; + int scope_y = STATUS_BAR_HEIGHT; + int scope_w = SCOPE_WIDTH; + int scope_h = SCOPE_HEIGHT; + + /* Left panel — radar description and controls */ + glViewport(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + glScissor(0, 0, LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + glEnable(GL_SCISSOR_TEST); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + g_active_radar->render_left_panel(); + + /* Scope — radar name (stub) */ + glViewport(scope_x, scope_y, scope_w, scope_h); + glScissor(scope_x, scope_y, scope_w, scope_h); + glClearColor(0.0f, 0.05f, 0.01f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(scope_w, scope_h); + g_active_radar->render_scope(); + + /* Status bar — current control values */ + glViewport(scope_x, 0, scope_w, STATUS_BAR_HEIGHT); + glScissor(scope_x, 0, scope_w, STATUS_BAR_HEIGHT); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(scope_w, STATUS_BAR_HEIGHT); + g_active_radar->render_status_bar(); + + glDisable(GL_SCISSOR_TEST); +} + +/* ------------------------------------------------------------------ */ +/* main */ +/* ------------------------------------------------------------------ */ + +int main(int argc, char* argv[]) { + + /* 1. Parse command-line arguments to set radar availability. */ + parse_args(argc, argv); + + /* 2. Initialise GLFW. */ + if (!glfwInit()) { + fprintf(stderr, "[main] GLFW init failed\n"); + return 1; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#if ENABLE_GL_DEBUG_OUTPUT + glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); +#endif + + GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, + WINDOW_TITLE, nullptr, nullptr); + if (!window) { + fprintf(stderr, "[main] GLFW window creation failed\n"); + glfwTerminate(); + return 1; + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); /* vsync */ + + /* 3. Initialise GLAD. */ + if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) { + fprintf(stderr, "[main] GLAD init failed\n"); + glfwTerminate(); + return 1; + } + + fprintf(stdout, "[main] OpenGL %s — %s\n", + glGetString(GL_VERSION), glGetString(GL_RENDERER)); + + /* 4. Enable GL debug output. */ +#if ENABLE_GL_DEBUG_OUTPUT + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + glDebugMessageCallback(gl_debug_callback, nullptr); + glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, + GL_DEBUG_SEVERITY_NOTIFICATION, + 0, nullptr, GL_FALSE); + fprintf(stdout, "[main] GL debug output enabled\n"); +#endif + + /* 5. Set global GL state. */ + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + /* 6. Load text renderer shaders and font. */ + if (!g_text_renderer.initialize_shaders()) { + fprintf(stderr, "[main] text renderer shader init failed\n"); + glfwTerminate(); + return 1; + } + if (!g_text_renderer.initialize_font(FONT_PATH, FONT_SIZE_NORMAL)) { + fprintf(stderr, "[main] font load failed: " FONT_PATH "\n"); + glfwTerminate(); + return 1; + } + + /* 7. Register keyboard callback. */ + glfwSetKeyCallback(window, key_callback); + + /* 8. If exactly one radar is available, auto-activate after 5 seconds. */ + if (g_selectable.size() == 1) { + fprintf(stdout, "[main] single radar available; activating in 5 s...\n"); + + using clock = std::chrono::steady_clock; + clock::time_point deadline = clock::now() + std::chrono::seconds(5); + + while (!glfwWindowShouldClose(window) && clock::now() < deadline) { + glfwPollEvents(); + + glDisable(GL_SCISSOR_TEST); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + render_selection_screen(); + glfwSwapBuffers(window); + + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + + /* Break early if the user pressed Enter. */ + if (g_app_state == AppState::OPERATING) break; + } + + /* Auto-activate if the user has not already pressed Enter. */ + if (g_app_state == AppState::SELECTION && !glfwWindowShouldClose(window)) { + radar_management[g_selectable[0].management_index].selected = 1; + current_radar = g_selectable[0].management_index; + g_active_radar = g_selectable[0].instance; + g_app_state = AppState::OPERATING; + } + } + + /* 9. Main render loop at ~30 Hz. */ + using clock = std::chrono::steady_clock; + const std::chrono::duration frame_time(1.0 / 30.0); + + while (!glfwWindowShouldClose(window)) { + clock::time_point frame_start = clock::now(); + + glfwPollEvents(); + + /* Clear full window to a near-black background. */ + glDisable(GL_SCISSOR_TEST); + glClearColor(0.02f, 0.02f, 0.02f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + if (g_app_state == AppState::SELECTION) { + render_selection_screen(); + } else { + render_operating_screen(); + } + + glfwSwapBuffers(window); + + /* Sleep for the remainder of the frame period. */ + std::chrono::duration elapsed = clock::now() - frame_start; + if (elapsed < frame_time) { + std::this_thread::sleep_for(frame_time - elapsed); + } + } + + /* 10. Cleanup. */ + g_text_renderer.cleanup(); + glfwDestroyWindow(window); + glfwTerminate(); + + return 0; +}