Adding changing notifier and quickened graticule change

This commit is contained in:
2026-04-07 08:02:33 -07:00
parent 337d423639
commit 7db6259b06
4 changed files with 291 additions and 47 deletions

View File

@@ -276,6 +276,36 @@ The Raspberry Pi code will live in a separate git repository with its own CLAUDE
and its own CMakeLists.txt, since it targets a different architecture (ARM) and has
a different toolchain and dependencies. Do not mix it into this repository hierarchy
NEW suggestions =======
1. The a scope bearing must not follow the rotating sweep on the ppi scope. The A scope
during the time they were popular before the ppi scope used a manual control to set the
bearing. The bearing setting uses a servo motor to rotate the dish antenna to the bearing
desired by the operator. We need to simulate that by not a scope not to follow the bearing
shown on the ppi scope. This bearing must be controled by control 7.
2. Add two more controls; Range for ppi scope cursor and Bearing for the PPI scope cursor
3. Pleae add a small window under the A scope to show the setting of the A scope bearnig.
4. Temporary: Have the bearing for a scope set to the location of one of the current targets.
5. Have the cursor on the ppi scope follow one of the targets and not move around.
New update 1
1. I notice that the height of the pulse on the A scope seems to be going up and down in sync
with the ppi scope rotating.
2. Add a range figure in the small box under the A scope
3. Have the a scope graticules change a little faster
4. it appears that the cursor range ring sometimes appears outside the boundary of the scope
New update 2
1. Add small text window above that a scope that says "Operator Manually Changing Range Graticule"
This appears only when the graticule is changing
NOTE on my plan for coding
1. I want to test and debug the code feature by feature.
@@ -305,10 +335,5 @@ Order of testing features.
========================================================
Generate code for testiong feature 1 and 2 and 3 only;
1. General initialization and set up basic boundaries of the two scopes
on the screen. No features on each scope yet.
2. Edge graticule on ppi scope (Bearing ticks and numbers)
3. replaceable graticules for A scope
Do not generate any other code
Generate code the run this and hold for 10 seconds and exit
Generate code for testiong feature 1,2,3,4,5,6 nly;

Binary file not shown.

View File

@@ -23,6 +23,7 @@
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
// ─── Constants ───────────────────────────────────────────────────────────────
@@ -49,15 +50,15 @@ static constexpr float P7P_R = 0.35f;
static constexpr float P7P_G = 0.88f;
static constexpr float P7P_B = 0.18f;
// Digits rendered into the font atlas: '0''9'
static constexpr int GLYPH_FIRST = '0';
static constexpr int GLYPH_COUNT = 10;
// Full printable ASCII rendered into the font atlas: ' '(32) through '~'(126)
static constexpr int GLYPH_FIRST = ' ';
static constexpr int GLYPH_COUNT = 95;
// Feature 3 timing
static constexpr float HOLD_SEC = 20.0f; // seconds per range setting
static constexpr float SLIDE_OUT_SEC = 5.0f; // slide current graticule out (up)
static constexpr float WAIT_SEC = 2.0f; // blank pause between graticules
static constexpr float SLIDE_IN_SEC = 5.0f; // slide new graticule in (from top)
static constexpr float HOLD_SEC = 5.0f; // seconds per range setting
static constexpr float SLIDE_OUT_SEC = 2.0f; // slide current graticule out (up)
static constexpr float WAIT_SEC = 0.5f; // blank pause between graticules
static constexpr float SLIDE_IN_SEC = 2.0f; // slide new graticule in (from top)
// Feature 4 — sweep
static constexpr float SWEEP_DEG_PS = 20.0f * 6.0f; // 20 RPM → 120 °/s
@@ -278,6 +279,8 @@ static void appendTextQuads(std::vector<float>& verts, const FontAtlas& fa,
struct Layout {
float ppiCX, ppiCY, ppiR;
float asLeft, asTop, asRight, asBot;
float brgLeft, brgTop, brgRight, brgBot; // A scope bearing display window
float notifyLeft, notifyRight, notifyTop, notifyBot; // "changing graticule" notice
};
static Layout computeLayout(float W, float H, float marginPx)
@@ -286,7 +289,10 @@ static Layout computeLayout(float W, float H, float marginPx)
float availW = (W - marginPx) - W * 0.5f;
float availH = (H - marginPx) - marginPx;
L.ppiR = std::min(availW, availH) * 0.5f;
// The bearing graticule outer ring sits at ppiR * 1.18, so shrink ppiR so
// that outer ring — not the inner circle — lands at the margin boundary.
static constexpr float GRAT_SCALE = 1.18f;
L.ppiR = std::min(availW, availH) * 0.5f / GRAT_SCALE;
L.ppiCX = W * 0.5f + availW * 0.5f;
L.ppiCY = marginPx + availH * 0.5f;
@@ -297,6 +303,22 @@ static Layout computeLayout(float W, float H, float marginPx)
L.asTop = (H - asH) * 0.5f;
L.asBot = L.asTop + asH;
// Bearing display window — small box directly below the A scope
const float brgGap = H * 0.008f; // gap between A scope bottom and window top
const float brgH = H * 0.060f; // window height (two text lines)
L.brgLeft = L.asLeft;
L.brgRight = L.asRight;
L.brgTop = L.asBot + brgGap;
L.brgBot = L.brgTop + brgH;
// Notification window — small box directly above the A scope
const float notifyGap = H * 0.008f;
const float notifyH = H * 0.040f;
L.notifyLeft = L.asLeft;
L.notifyRight = L.asRight;
L.notifyBot = L.asTop - notifyGap;
L.notifyTop = L.notifyBot - notifyH;
return L;
}
@@ -802,10 +824,13 @@ static AScopeTrace buildAScopeTrace()
}
// Renders target return spikes on the A scope clipped to the A scope box.
// The A scope antenna is pointed at a fixed bearing (ascopeBearingDeg),
// independent of the PPI sweep. Every radar pulse illuminates that bearing,
// so returns are continuous — brightness is steady, no sweep dependency.
static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
FakeTarget* tgts, int nTgts,
float maxRangeMi, float ascopeBearingDeg,
float sweepAngle, float curTime, float W, float H)
float W, float H)
{
static const float THRESH = 3.5f;
static const float PAD = 4.0f;
@@ -818,8 +843,6 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
const float sigW = gx1 - gx0;
const float sigH = (L.asTop + PAD) - gy1; // negative: upward from baseline
const bool sweepHere = angDiff(sweepAngle, ascopeBearingDeg) < THRESH;
glEnable(GL_SCISSOR_TEST);
glScissor((GLint)L.asLeft, (GLint)(H - L.asBot),
(GLint)(L.asRight - L.asLeft), (GLint)(L.asBot - L.asTop));
@@ -828,27 +851,22 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
const GLint locCol = glGetUniformLocation(at.prog, "uColor");
for (int i = 0; i < nTgts; ++i) {
FakeTarget& t = tgts[i];
const FakeTarget& t = tgts[i];
if (sweepHere && angDiff(ascopeBearingDeg, t.bearingDeg) < THRESH)
at.lastActT[i] = curTime;
// Only show targets whose bearing falls within the A scope beam
if (angDiff(ascopeBearingDeg, t.bearingDeg) >= THRESH) continue;
float rangeFrac = t.rangeMiles / maxRangeMi;
if (rangeFrac > 1.0f) continue;
float dt = curTime - at.lastActT[i];
float fade = (at.lastActT[i] < 0.0f) ? 0.0f
: std::max(0.0f, 1.0f - dt / TARG_PERSIST);
if (fade < 0.02f) continue;
float coreRad, bloomRad, baseBright;
sizeFromApparent(apparentFt(t), L.ppiR, coreRad, bloomRad, baseBright);
float bright = baseBright * fade;
float height = sigH * bright; // upward (sigH is negative → spike goes up)
float height = sigH * baseBright; // upward (sigH is negative)
float sx = gx0 + rangeFrac * sigW;
float sy0 = gy1; // baseline
float sy1 = gy1 + height; // peak (upward because sigH < 0)
float sy0 = gy1;
float sy1 = gy1 + height;
float verts[6] = {
ndcX(sx - HALF_W, W), ndcY(sy0, H),
@@ -857,7 +875,7 @@ static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
};
glBindBuffer(GL_ARRAY_BUFFER, at.vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW);
glUniform3f(locCol, P1_R * bright, P1_G * bright, P1_B * bright);
glUniform3f(locCol, P1_R * baseBright, P1_G * baseBright, P1_B * baseBright);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
@@ -1025,6 +1043,180 @@ static void renderCursor(CursorLayer& cl, const Layout& L,
glBindVertexArray(0);
}
// ─── A scope bearing display window ──────────────────────────────────────────
//
// Small box below the A scope showing the current A scope bearing setting
// as "BRG XXX" in incandescent colour.
// Reuses scope_bounds shader (uColor, no offset) for the box outline
// and text/text shaders for the label.
struct BearingDisplay {
GLuint boxVAO = 0, boxVBO = 0;
int boxCount = 0;
GLuint textVAO = 0, textVBO = 0;
};
static BearingDisplay buildBearingDisplay(const Layout& L, float W, float H)
{
BearingDisplay bd{};
// Static box outline (4 line segments)
std::vector<float> v;
auto ln = [&](float x1, float y1, float x2, float y2) {
v.push_back(ndcX(x1,W)); v.push_back(ndcY(y1,H));
v.push_back(ndcX(x2,W)); v.push_back(ndcY(y2,H));
};
ln(L.brgLeft, L.brgTop, L.brgRight, L.brgTop);
ln(L.brgRight, L.brgTop, L.brgRight, L.brgBot);
ln(L.brgRight, L.brgBot, L.brgLeft, L.brgBot);
ln(L.brgLeft, L.brgBot, L.brgLeft, L.brgTop);
bd.boxCount = (int)v.size() / 2;
makeLineVAO(bd.boxVAO, bd.boxVBO, v);
// Dynamic text VAO (rebuilt each frame with current bearing value)
glGenVertexArrays(1, &bd.textVAO); glGenBuffers(1, &bd.textVBO);
glBindVertexArray(bd.textVAO);
glBindBuffer(GL_ARRAY_BUFFER, bd.textVBO);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), nullptr);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float),
reinterpret_cast<void*>(2*sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
return bd;
}
static void renderBearingDisplay(BearingDisplay& bd, const Layout& L,
GLuint boxProg, GLuint textProg,
const FontAtlas& fa, float bearingDeg,
float maxRangeMi, float W, float H)
{
// Box outline
glUseProgram(boxProg);
glUniform3f(glGetUniformLocation(boxProg, "uColor"),
INCAN_R, INCAN_G, INCAN_B);
glBindVertexArray(bd.boxVAO);
glDrawArrays(GL_LINES, 0, bd.boxCount);
glBindVertexArray(0);
// Two lines: "BRG 000" and "RNG 15"
int iBear = static_cast<int>(bearingDeg) % 360;
if (iBear < 0) iBear += 360;
int iRng = static_cast<int>(maxRangeMi);
char brgBuf[12], rngBuf[12];
std::snprintf(brgBuf, sizeof(brgBuf), "BRG %03d", iBear);
std::snprintf(rngBuf, sizeof(rngBuf), "RNG %2d", iRng);
const float cx = (L.brgLeft + L.brgRight) * 0.5f;
const float brgH = L.brgBot - L.brgTop;
const float cy1 = L.brgTop + brgH * 0.30f; // upper line
const float cy2 = L.brgTop + brgH * 0.70f; // lower line
std::vector<float> tv;
appendTextQuads(tv, fa, std::string(brgBuf), cx, cy1, W, H);
appendTextQuads(tv, fa, std::string(rngBuf), cx, cy2, W, H);
if (tv.empty()) return;
glBindBuffer(GL_ARRAY_BUFFER, bd.textVBO);
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(tv.size()*sizeof(float)),
tv.data(), GL_DYNAMIC_DRAW);
glUseProgram(textProg);
glUniform3f(glGetUniformLocation(textProg, "uColor"),
INCAN_R, INCAN_G, INCAN_B);
glUniform1i(glGetUniformLocation(textProg, "uTexture"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, fa.texture);
glBindVertexArray(bd.textVAO);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(tv.size() / 4));
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(0);
}
// ─── New Update 2: "Operator Manually Changing Range Graticule" window ────────
//
// Appears above the A scope only while the graticule is animating (any phase
// other than HOLD).
struct GratNotifyWindow {
GLuint boxVAO = 0, boxVBO = 0;
int boxCount = 0;
GLuint textVAO = 0, textVBO = 0;
};
static GratNotifyWindow buildGratNotifyWindow(const Layout& L, float W, float H)
{
GratNotifyWindow gn{};
std::vector<float> v;
auto ln = [&](float x1, float y1, float x2, float y2) {
v.push_back(ndcX(x1,W)); v.push_back(ndcY(y1,H));
v.push_back(ndcX(x2,W)); v.push_back(ndcY(y2,H));
};
ln(L.notifyLeft, L.notifyTop, L.notifyRight, L.notifyTop);
ln(L.notifyRight, L.notifyTop, L.notifyRight, L.notifyBot);
ln(L.notifyRight, L.notifyBot, L.notifyLeft, L.notifyBot);
ln(L.notifyLeft, L.notifyBot, L.notifyLeft, L.notifyTop);
gn.boxCount = (int)v.size() / 2;
makeLineVAO(gn.boxVAO, gn.boxVBO, v);
glGenVertexArrays(1, &gn.textVAO); glGenBuffers(1, &gn.textVBO);
glBindVertexArray(gn.textVAO);
glBindBuffer(GL_ARRAY_BUFFER, gn.textVBO);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), nullptr);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float),
reinterpret_cast<void*>(2*sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
return gn;
}
static void renderGratNotifyWindow(GratNotifyWindow& gn, const Layout& L,
GLuint boxProg, GLuint textProg,
const FontAtlas& fa, float W, float H)
{
// Box outline
glUseProgram(boxProg);
glUniform3f(glGetUniformLocation(boxProg, "uColor"),
INCAN_R, INCAN_G, INCAN_B);
glBindVertexArray(gn.boxVAO);
glDrawArrays(GL_LINES, 0, gn.boxCount);
glBindVertexArray(0);
// Text centred in box, clipped to box bounds
const float cx = (L.notifyLeft + L.notifyRight) * 0.5f;
const float cy = (L.notifyTop + L.notifyBot) * 0.5f;
std::vector<float> tv;
appendTextQuads(tv, fa, "Operator Manually Changing Range Graticule",
cx, cy, W, H);
if (tv.empty()) return;
glEnable(GL_SCISSOR_TEST);
glScissor((GLint) L.notifyLeft,
(GLint)(H - L.notifyBot),
(GLint)(L.notifyRight - L.notifyLeft),
(GLint)(L.notifyBot - L.notifyTop));
glBindBuffer(GL_ARRAY_BUFFER, gn.textVBO);
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(tv.size()*sizeof(float)),
tv.data(), GL_DYNAMIC_DRAW);
glUseProgram(textProg);
glUniform3f(glGetUniformLocation(textProg, "uColor"),
INCAN_R, INCAN_G, INCAN_B);
glUniform1i(glGetUniformLocation(textProg, "uTexture"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, fa.texture);
glBindVertexArray(gn.textVAO);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(tv.size() / 4));
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(0);
glDisable(GL_SCISSOR_TEST);
}
// ─── Key callback ─────────────────────────────────────────────────────────────
static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/)
@@ -1097,15 +1289,15 @@ int main()
RingLayer rl = buildRingLayer(fa);
// Feature 5 — Four fake targets per CLAUDE.md spec:
// 1. 10 mi north, 100 ft long / 20 ft beam, heading south (head-on) → apparent ~20 ft
// 2. 5 mi south, 20 ft long / 5 ft beam, heading south (stern-on) → apparent ~5 ft
// 3. 4 mi east, 30 ft long / 10 ft beam, heading north (full side) → apparent ~30 ft
// 4. 1 mi west, 100 ft long / 25 ft beam, heading south (full side) → apparent ~100 ft
// 1. 5 mi N, 100 ft / 20 ft, heading S at 1 kt (head-on) → apparent ~20 ft
// 2. 5 mi S, 20 ft / 5 ft, heading S at 20 kt (stern-on) → apparent ~5 ft
// 3. 6 mi E, 30 ft / 10 ft, heading N at 30 kt (full side) → apparent ~30 ft
// 4. 6 mi W, 100 ft / 25 ft, heading S at 5 kt (full side) → apparent ~100 ft
FakeTarget targets[4] = {
{ 0.0f, 10.0f, 100.0f, 20.0f, 180.0f, -999.0f }, // N head-on, ~20 ft apparent
{ 0.0f, 5.0f, 100.0f, 20.0f, 180.0f, -999.0f }, // N head-on, ~20 ft apparent
{ 180.0f, 5.0f, 20.0f, 5.0f, 180.0f, -999.0f }, // S stern-on, ~5 ft apparent
{ 90.0f, 4.0f, 30.0f, 10.0f, 0.0f, -999.0f }, // E full side, ~30 ft apparent
{ 270.0f, 1.0f, 100.0f, 25.0f, 180.0f, -999.0f }, // W full side, ~100 ft apparent
{ 90.0f, 6.0f, 30.0f, 10.0f, 0.0f, -999.0f }, // E full side, ~30 ft apparent
{ 270.0f, 6.0f, 100.0f, 25.0f, 180.0f, -999.0f }, // W full side, ~100 ft apparent
};
TargetLayer tl = buildTargetLayer();
@@ -1115,6 +1307,13 @@ int main()
// Feature 6 — PPI cursor
CursorLayer cl = buildCursorLayer();
// Bearing display window (new suggestion #3)
// Reuse scope_bounds program for the box, text program for label
BearingDisplay bd = buildBearingDisplay(layout, W, H);
// New Update 2 — graticule change notification window
GratNotifyWindow gn = buildGratNotifyWindow(layout, W, H);
const float ppiR = layout.ppiR;
// NDC height of A scope box — used for slide animation
@@ -1127,8 +1326,10 @@ int main()
GratPhase gratPhase = GratPhase::HOLD;
float phaseTimer = 0.0f;
// A scope bearing (° true, CW from N)
const float ascopeBearingDeg = 90.0f;
// A scope bearing (° true, CW from N).
// New suggestion #4: temporarily locked to targets[0] bearing (north, 0°).
// Will be driven by Control 7 once hardware is available.
float ascopeBearingDeg = targets[0].bearingDeg; // 0.0° — north
float sweepAngle = 0.0f;
float prevTime = static_cast<float>(glfwGetTime());
@@ -1236,10 +1437,14 @@ int main()
}
}
// ── New Update 2: notification window above A scope ───────────────────
if (gratPhase != GratPhase::HOLD)
renderGratNotifyWindow(gn, layout, sb.prog, bg.textProg, fa, W, H);
// ── A scope signal trace ──────────────────────────────────────────────
renderAScopeTrace(at, layout, targets, 4,
RANGE_CONFIGS[curRange].maxMiles, ascopeBearingDeg,
sweepAngle, now, W, H);
W, H);
// ── Feature 4: PPI range rings ────────────────────────────────────────
renderRingLayer(rl, layout, curRange, sweepAngle, now, W, H, fa);
@@ -1249,12 +1454,22 @@ int main()
RANGE_CONFIGS[curRange].maxMiles, ppiR,
sweepAngle, now, W, H);
// ── Feature 6: PPI cursor (slow auto-animation for test) ──────────────
// Range oscillates between ~15 % and ~85 % over 12 s period.
// Bearing rotates slowly at 18 °/s (one full revolution per 20 s).
float cursorRangeFrac = 0.15f + 0.70f * (std::sin(now * (2.0f*PI/12.0f)) * 0.5f + 0.5f);
float cursorBearingDeg = std::fmod(now * 18.0f, 360.0f);
renderCursor(cl, layout, cursorRangeFrac, cursorBearingDeg, W, H);
// ── Feature 6: PPI cursor locked to targets[2] (east, 6 mi) ──────────
// New suggestion #5: cursor follows one target; not free-roaming.
// Only draw when the target is within the current range setting.
{
const float cursorRangeFrac = targets[2].rangeMiles
/ RANGE_CONFIGS[curRange].maxMiles;
const float cursorBearingDeg = targets[2].bearingDeg;
if (cursorRangeFrac <= 1.0f)
renderCursor(cl, layout, cursorRangeFrac, cursorBearingDeg, W, H);
}
// ── Bearing display window (new suggestion #3) ────────────────────────
renderBearingDisplay(bd, layout,
sb.prog, bg.textProg,
fa, ascopeBearingDeg,
RANGE_CONFIGS[curRange].maxMiles, W, H);
glfwSwapBuffers(win);
glfwPollEvents();
@@ -1287,6 +1502,10 @@ int main()
glDeleteProgram(at.prog);
glDeleteVertexArrays(1, &cl.vao); glDeleteBuffers(1, &cl.vbo);
glDeleteProgram(cl.prog);
glDeleteVertexArrays(1, &bd.boxVAO); glDeleteBuffers(1, &bd.boxVBO);
glDeleteVertexArrays(1, &bd.textVAO); glDeleteBuffers(1, &bd.textVBO);
glDeleteVertexArrays(1, &gn.boxVAO); glDeleteBuffers(1, &gn.boxVBO);
glDeleteVertexArrays(1, &gn.textVAO); glDeleteBuffers(1, &gn.textVBO);
glfwDestroyWindow(win);
glfwTerminate();