From 2f8a3706c5d5772573a5868271bad58c81c0408f Mon Sep 17 00:00:00 2001 From: Mark Allyn Date: Thu, 18 Jun 2026 10:23:58 -0700 Subject: [PATCH] Change font size and use outlines --- 01_classes.md | 24 +++-- CLAUDE.md | 32 +++--- include/settings.h | 23 ++++- src/01_classes.cpp | 12 +-- src/main.cpp | 248 +++++++++++++++++++++++++++++++++++---------- 5 files changed, 254 insertions(+), 85 deletions(-) diff --git a/01_classes.md b/01_classes.md index 15a9a98..6068329 100644 --- a/01_classes.md +++ b/01_classes.md @@ -27,19 +27,21 @@ Examples of data and functions for this class and it subordinates: of the left hand panel. Please note that in the future, this left hand panel may be moved to a second physical monitor. -3. Text status panel to be located below the radar scope panel. This will display the current - control settings for the controls and the range and bearing and description of - targets that are on or near the bearing and range cursor. The text status panel shall manage - the hit-test. The hit test shall determine which (if any) targets are either directly the same - rand and the same bearing as the cursor or close to it. The closeness shall be determined - by a configuration variable. Since the cursor information as well as the target information - is already in thread 1, this operatoin shall be performed by the CPU as the CPU will have - all information on the target, including the name and description. +3. Text status panel to be located below the left-hand text panel (bottom of the left + side of the screen, NOT below the scope). The scope takes the full right-side height + with no panel beneath it. The status panel will display the current control settings + and the range and bearing and description of targets that are on or near the bearing + and range cursor. The text status panel shall manage the hit-test. The hit test shall + determine which (if any) targets are either directly the same range and the same + bearing as the cursor or close to it. The closeness shall be determined by a + configuration variable. Since the cursor information as well as the target information + is already in thread 1, this operation shall be performed by the CPU as the CPU will + have all information on the target, including the name and description. - Please note that any text (such as the vessel name and registration will not be part + Please note that any text (such as the vessel name and registration) will not be part of the SSBO target information. It will need to be formed in text blocks that can - be submitted as textures to be inserted into the status text panel below the scope - panel. + be submitted as textures to be inserted into the status text panel at the bottom of + the left side of the screen. 4. Target data. This will handle the target data that is presented by both the simulator and the raspberry pi data. All that data will be in the format of the diff --git a/CLAUDE.md b/CLAUDE.md index eb12bf3..a68e831 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,23 +148,29 @@ of {PROJECT_DIR}/build/radar_simulator. In the CMakeLists.txt, plese use the name radar_simulator for the add_executable call. -There will be three main areas of the screen. On the right hand side will be the radar -scope. +There will be three screen areas (viewports): -On the left hand side of the screen will be a text description of the scope as well -as the controls of the scope and keyboard keys for each control. This text will be -white while the control labels will be red and the keystrokes will be in pink. -At some point, pending a decision with the museum, we may purchase components to mount the controls on a panel. Until that is done, the controls will be on the keyboard. +1. Left text panel (upper-left): displays the description of the current scope and the + keyboard controls for that scope. Text is white; control labels are red; keystrokes + are pink. This panel occupies the upper portion of the left side of the window. + At some point, pending a decision with the museum, we may purchase components to + mount the controls on a panel. Until that is done, the controls will be on the keyboard. -On the right hand side of the screen, below the scope, would be a text status window, showing current -maximum range, range, and bearing as appropriate to which scope we are using as well as the identification -of the scope that we are using. +2. Status bar (lower-left): sits directly below the left text panel. It shows the current + control values — maximum range, range cursor, bearing cursor, and scope identification — + as appropriate to whichever scope is active. This text will be yellow. -Suggest that we use glViewport to define the left-hand text area, the main scope, -and the yellow status bar. This allows you to render the scope with its own coordinate -system while keeping the text fixed. Along with glViewport, use glScissor and glEnable(GL_SCISSOR_TEST) +3. Scope (right side, full height): the radar scope takes the entire right side of the + window from top to bottom. There is no status bar below the scope. -Below the scope will be a text status window. This text will be yellow +Use glViewport, glScissor, and glEnable(GL_SCISSOR_TEST) to define each of the three +areas so that each can render with its own coordinate system. + +The left side dimensions are defined in settings.h: + LEFT_PANEL_WIDTH — horizontal width of the left side + STATUS_BAR_HEIGHT — height of the status bar (bottom of left side) + LEFT_TEXT_HEIGHT — height of the text panel (top of left side; = WINDOW_HEIGHT - STATUS_BAR_HEIGHT) +The scope fills the remaining right side at full window height (SCOPE_WIDTH x WINDOW_HEIGHT). Scopes in the right panel diff --git a/include/settings.h b/include/settings.h index 452e6ae..d61cec5 100644 --- a/include/settings.h +++ b/include/settings.h @@ -136,13 +136,28 @@ #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 +/* Left side is split vertically: text panel on top, status bar on bottom. + * Right side is the scope viewport — it takes the full window height. */ #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) +// Derived dimensions +#define SCOPE_WIDTH (WINDOW_WIDTH - LEFT_PANEL_WIDTH) +#define SCOPE_HEIGHT WINDOW_HEIGHT // scope takes full right-side height +#define LEFT_TEXT_HEIGHT (WINDOW_HEIGHT - STATUS_BAR_HEIGHT) // text portion of left panel + +// Pixel margin on every side of the scope area (keeps edges visible on the monitor) +#define SCOPE_MARGIN 10 +// Available scope area after removing the margin +#define SCOPE_VW (SCOPE_WIDTH - 2 * SCOPE_MARGIN) +#define SCOPE_VH (SCOPE_HEIGHT - 2 * SCOPE_MARGIN) +// Square scope: side length is the smaller of SCOPE_VW and SCOPE_VH (height wins here) +#define SCOPE_SQUARE SCOPE_VH +// Horizontal offset to centre the square inside the SCOPE_VW-wide available area +#define SCOPE_SQUARE_XOFF ((SCOPE_VW - SCOPE_SQUARE) / 2) + +// 1 = draw a white debug border box around each screen panel (temporary layout aid) +#define DEBUG_DRAW_WINDOW_BORDERS 1 /* ============================================================ * TEXT RENDERING diff --git a/src/01_classes.cpp b/src/01_classes.cpp index 5edd035..ac43062 100644 --- a/src/01_classes.cpp +++ b/src/01_classes.cpp @@ -127,8 +127,8 @@ static void control_line(const std::string& label, /* ------------------------------------------------------------------ */ 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); + const float scope_w = static_cast(SCOPE_SQUARE); + const float scope_h = static_cast(SCOPE_SQUARE); float name_w = g_text_renderer_large.get_text_width(radar_name, 1.0f); float nx = (scope_w - name_w) / 2.0f; @@ -164,7 +164,7 @@ static void render_common_status(float x, float& y, float scale) { /* ================================================================== */ void ChainHomeRadar::render_left_panel() { - float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH_LARGE; + float y = static_cast(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE; float x = MARGIN; text_line_large("CHAIN HOME RADAR", x, y, @@ -238,7 +238,7 @@ void ChainHomeRadar::render_status_bar() { /* ================================================================== */ void MarineAScopeRadar::render_left_panel() { - float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH_LARGE; + float y = static_cast(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE; float x = MARGIN; text_line_large("MARINE A-SCOPE RADAR", x, y, @@ -310,7 +310,7 @@ void MarineAScopeRadar::render_status_bar() { /* ================================================================== */ void MarineTrafficRadar::render_left_panel() { - float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH_LARGE; + float y = static_cast(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE; float x = MARGIN; text_line_large("MARINE TRAFFIC CONTROL RADAR", x, y, @@ -386,7 +386,7 @@ void MarineTrafficRadar::render_status_bar() { /* ================================================================== */ void PoliceboatRadar::render_left_panel() { - float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH_LARGE; + float y = static_cast(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE; float x = MARGIN; text_line_large("POLICE BOAT RADAR", x, y, diff --git a/src/main.cpp b/src/main.cpp index c5a724c..65c69ba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,6 +57,126 @@ #include #include +/* ------------------------------------------------------------------ */ +/* Debug border-box line renderer (DEBUG_DRAW_WINDOW_BORDERS) */ +/* ------------------------------------------------------------------ */ + +#if DEBUG_DRAW_WINDOW_BORDERS + +static const char* LINE_VERT_SRC = + "#version 430 core\n" + "layout(location = 0) in vec2 pos;\n" + "uniform mat4 projection;\n" + "void main() {\n" + " gl_Position = projection * vec4(pos, 0.0, 1.0);\n" + "}\n"; + +static const char* LINE_FRAG_SRC = + "#version 430 core\n" + "uniform vec3 line_color;\n" + "out vec4 frag_color;\n" + "void main() {\n" + " frag_color = vec4(line_color, 1.0);\n" + "}\n"; + +static GLuint g_line_vao = 0; +static GLuint g_line_vbo = 0; +static GLuint g_line_prog = 0; + +static bool init_line_renderer() { + GLint ok = 0; + char log_buf[512]; + + GLuint vert = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vert, 1, &LINE_VERT_SRC, nullptr); + glCompileShader(vert); + glGetShaderiv(vert, GL_COMPILE_STATUS, &ok); + if (!ok) { + glGetShaderInfoLog(vert, 512, nullptr, log_buf); + fprintf(stderr, "[main] border vert shader error:\n%s\n", log_buf); + glDeleteShader(vert); + return false; + } + + GLuint frag = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(frag, 1, &LINE_FRAG_SRC, nullptr); + glCompileShader(frag); + glGetShaderiv(frag, GL_COMPILE_STATUS, &ok); + if (!ok) { + glGetShaderInfoLog(frag, 512, nullptr, log_buf); + fprintf(stderr, "[main] border frag shader error:\n%s\n", log_buf); + glDeleteShader(vert); + glDeleteShader(frag); + return false; + } + + g_line_prog = glCreateProgram(); + glAttachShader(g_line_prog, vert); + glAttachShader(g_line_prog, frag); + glLinkProgram(g_line_prog); + glGetProgramiv(g_line_prog, GL_LINK_STATUS, &ok); + if (!ok) { + glGetProgramInfoLog(g_line_prog, 512, nullptr, log_buf); + fprintf(stderr, "[main] border shader link error:\n%s\n", log_buf); + glDeleteShader(vert); + glDeleteShader(frag); + return false; + } + glDeleteShader(vert); + glDeleteShader(frag); + + glGenVertexArrays(1, &g_line_vao); + glGenBuffers(1, &g_line_vbo); + glBindVertexArray(g_line_vao); + glBindBuffer(GL_ARRAY_BUFFER, g_line_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, nullptr, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + fprintf(stdout, "[main] debug border renderer initialised\n"); + return true; +} + +/* Draw a white line-loop rectangle inset 3 px from the edges of the + * current viewport (w x h). Call after setting glViewport/glScissor. */ +static void draw_border_box(int w, int h) { + const float fw = static_cast(w); + const float fh = static_cast(h); + const float pad = 3.0f; + + float verts[8] = { + pad, pad, + fw - pad, pad, + fw - pad, fh - pad, + pad, fh - pad + }; + + /* Same orthographic pixel-space projection as TextRenderer::set_projection. */ + 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(g_line_prog); + glUniformMatrix4fv(glGetUniformLocation(g_line_prog, "projection"), + 1, GL_FALSE, proj); + glUniform3f(glGetUniformLocation(g_line_prog, "line_color"), + 1.0f, 1.0f, 1.0f); + + glBindVertexArray(g_line_vao); + glBindBuffer(GL_ARRAY_BUFFER, g_line_vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts); + glDrawArrays(GL_LINE_LOOP, 0, 4); + glBindVertexArray(0); + glUseProgram(0); +} + +#endif /* DEBUG_DRAW_WINDOW_BORDERS */ + /* ------------------------------------------------------------------ */ /* Application state */ /* ------------------------------------------------------------------ */ @@ -373,7 +493,7 @@ static void parse_args(int argc, char* argv[]) { /* ------------------------------------------------------------------ */ static void render_intro_left_panel() { - float y = static_cast(WINDOW_HEIGHT) - MARGIN - LH_LARGE; + float y = static_cast(LEFT_TEXT_HEIGHT) - MARGIN - LH_LARGE; float x = MARGIN; g_text_renderer_large.render_text("RAF / MARINE RADAR EXHIBIT", x, y, 1.0f, @@ -410,8 +530,8 @@ static void render_intro_left_panel() { } static void render_radar_list() { - float scope_w = static_cast(SCOPE_WIDTH); - float scope_h = static_cast(SCOPE_HEIGHT); + float scope_w = static_cast(SCOPE_SQUARE); + float scope_h = static_cast(SCOPE_SQUARE); /* Title */ std::string title = "SELECT A RADAR"; @@ -502,75 +622,89 @@ static void render_selection_status_bar() { /* ------------------------------------------------------------------ */ 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); + + /* Left text panel — upper portion of the left side */ + glViewport(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); + glScissor(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); 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_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + g_text_renderer.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); + g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); render_intro_left_panel(); +#if DEBUG_DRAW_WINDOW_BORDERS + draw_border_box(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); +#endif - /* Scope area — radar list */ - glViewport(scope_x, scope_y, scope_w, scope_h); - glScissor(scope_x, scope_y, scope_w, scope_h); + /* Left status bar — lower portion of the left side */ + glViewport(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + glScissor(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + render_selection_status_bar(); +#if DEBUG_DRAW_WINDOW_BORDERS + draw_border_box(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); +#endif + + /* Scope — square, centred in the right-side area */ + glViewport(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN, + SCOPE_SQUARE, SCOPE_SQUARE); + glScissor(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN, + SCOPE_SQUARE, SCOPE_SQUARE); glClearColor(0.0f, 0.05f, 0.01f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - g_text_renderer.set_projection(scope_w, scope_h); - g_text_renderer_large.set_projection(scope_w, scope_h); + g_text_renderer.set_projection(SCOPE_SQUARE, SCOPE_SQUARE); + g_text_renderer_large.set_projection(SCOPE_SQUARE, SCOPE_SQUARE); 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); - g_text_renderer_large.set_projection(scope_w, STATUS_BAR_HEIGHT); - render_selection_status_bar(); +#if DEBUG_DRAW_WINDOW_BORDERS + draw_border_box(SCOPE_SQUARE, SCOPE_SQUARE); +#endif 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); + + /* Left text panel — upper portion of the left side */ + glViewport(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); + glScissor(0, STATUS_BAR_HEIGHT, LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); 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_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, WINDOW_HEIGHT); + g_text_renderer.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); + g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); g_active_radar->render_left_panel(); +#if DEBUG_DRAW_WINDOW_BORDERS + draw_border_box(LEFT_PANEL_WIDTH, LEFT_TEXT_HEIGHT); +#endif - /* Scope — radar name (stub) */ - glViewport(scope_x, scope_y, scope_w, scope_h); - glScissor(scope_x, scope_y, scope_w, scope_h); + /* Left status bar — lower portion of the left side */ + glViewport(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + glScissor(0, 0, LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + glClearColor(0.04f, 0.04f, 0.04f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + g_text_renderer.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + g_text_renderer_large.set_projection(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); + g_active_radar->render_status_bar(); +#if DEBUG_DRAW_WINDOW_BORDERS + draw_border_box(LEFT_PANEL_WIDTH, STATUS_BAR_HEIGHT); +#endif + + /* Scope — square, centred in the right-side area */ + glViewport(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN, + SCOPE_SQUARE, SCOPE_SQUARE); + glScissor(LEFT_PANEL_WIDTH + SCOPE_MARGIN + SCOPE_SQUARE_XOFF, SCOPE_MARGIN, + SCOPE_SQUARE, SCOPE_SQUARE); glClearColor(0.0f, 0.05f, 0.01f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - g_text_renderer.set_projection(scope_w, scope_h); - g_text_renderer_large.set_projection(scope_w, scope_h); + g_text_renderer.set_projection(SCOPE_SQUARE, SCOPE_SQUARE); + g_text_renderer_large.set_projection(SCOPE_SQUARE, SCOPE_SQUARE); 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_text_renderer_large.set_projection(scope_w, STATUS_BAR_HEIGHT); - g_active_radar->render_status_bar(); +#if DEBUG_DRAW_WINDOW_BORDERS + draw_border_box(SCOPE_SQUARE, SCOPE_SQUARE); +#endif glDisable(GL_SCISSOR_TEST); } @@ -633,6 +767,13 @@ int main(int argc, char* argv[]) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + /* 6a. Initialise debug border renderer (guarded by DEBUG_DRAW_WINDOW_BORDERS). */ +#if DEBUG_DRAW_WINDOW_BORDERS + if (!init_line_renderer()) { + fprintf(stderr, "[main] debug border renderer init failed (non-fatal)\n"); + } +#endif + /* 6. Load text renderer shaders and fonts. */ if (!g_text_renderer.initialize_shaders()) { fprintf(stderr, "[main] text renderer shader init failed\n"); @@ -720,6 +861,11 @@ int main(int argc, char* argv[]) { /* 10. Cleanup. */ g_text_renderer.cleanup(); +#if DEBUG_DRAW_WINDOW_BORDERS + if (g_line_vao) { glDeleteVertexArrays(1, &g_line_vao); g_line_vao = 0; } + if (g_line_vbo) { glDeleteBuffers(1, &g_line_vbo); g_line_vbo = 0; } + if (g_line_prog) { glDeleteProgram(g_line_prog); g_line_prog = 0; } +#endif glfwDestroyWindow(window); glfwTerminate();