new code
This commit is contained in:
38
CMakeLists.txt
Normal file
38
CMakeLists.txt
Normal file
@@ -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
|
||||||
|
)
|
||||||
92
include/01_classes.h
Normal file
92
include/01_classes.h
Normal file
@@ -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;
|
||||||
96
include/02_text_description.h
Normal file
96
include/02_text_description.h
Normal file
@@ -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 <string>
|
||||||
|
#include <map>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#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<char, glyph_character> characters_;
|
||||||
|
GLuint vao_;
|
||||||
|
GLuint vbo_;
|
||||||
|
GLuint shader_program_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Global instance — shared by all panels and radar stubs. */
|
||||||
|
extern TextRenderer g_text_renderer;
|
||||||
@@ -126,3 +126,79 @@
|
|||||||
#define PPI_BOAT_RANGE_STEP_3 6.0f
|
#define PPI_BOAT_RANGE_STEP_3 6.0f
|
||||||
#define PPI_BOAT_RANGE_STEP_4 12.0f
|
#define PPI_BOAT_RANGE_STEP_4 12.0f
|
||||||
#define PPI_BOAT_RANGE_STEP_5 24.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
|
||||||
|
|||||||
46
shaders/02_text_description_frag.glsl
Normal file
46
shaders/02_text_description_frag.glsl
Normal file
@@ -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);
|
||||||
|
}
|
||||||
46
shaders/02_text_description_vert.glsl
Normal file
46
shaders/02_text_description_vert.glsl
Normal file
@@ -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;
|
||||||
|
}
|
||||||
453
src/01_classes.cpp
Normal file
453
src/01_classes.cpp
Normal file
@@ -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 <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 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<float>(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<float>(SCOPE_WIDTH);
|
||||||
|
const float scope_h = static_cast<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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]);
|
||||||
|
}
|
||||||
278
src/02_text_description.cpp
Normal file
278
src/02_text_description.cpp
Normal file
@@ -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 <ft2build.h>
|
||||||
|
#include FT_FREETYPE_H
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
/* 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<void*>(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<GLsizei>(ft_face->glyph->bitmap.width),
|
||||||
|
static_cast<GLsizei>(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<int>(ft_face->glyph->bitmap.width);
|
||||||
|
ch.size_y = static_cast<int>(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<unsigned int>(ft_face->glyph->advance.x);
|
||||||
|
characters_[static_cast<char>(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<float>(ch.bearing_x) * scale;
|
||||||
|
float ypos = y - static_cast<float>(ch.size_y - ch.bearing_y) * scale;
|
||||||
|
float w = static_cast<float>(ch.size_x) * scale;
|
||||||
|
float h = static_cast<float>(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<float>(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<float>(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<float>(w);
|
||||||
|
float fh = static_cast<float>(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<char, glyph_character>::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; }
|
||||||
|
}
|
||||||
711
src/main.cpp
Normal file
711
src/main.cpp
Normal file
@@ -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 <glad/glad.h>
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#include "data_structure_and_constants.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "01_classes.h"
|
||||||
|
#include "02_text_description.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 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<selectable_radar> 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<float>(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<int>(g_selectable.size());
|
||||||
|
} else if (key == GLFW_KEY_2) {
|
||||||
|
g_selection_cursor =
|
||||||
|
(g_selection_cursor - 1 + static_cast<int>(g_selectable.size()))
|
||||||
|
% static_cast<int>(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<float>(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<float>(SCOPE_WIDTH);
|
||||||
|
float scope_h = static_cast<float>(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<int>(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<float>(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<GLADloadproc>(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<double> 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<double> 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user