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 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 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 NOTE on my plan for coding
1. I want to test and debug the code feature by feature. 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; Generate code for testiong feature 1,2,3,4,5,6 nly;
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

Binary file not shown.

View File

@@ -23,6 +23,7 @@
#include <iostream> #include <iostream>
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <cstdio>
// ─── Constants ─────────────────────────────────────────────────────────────── // ─── Constants ───────────────────────────────────────────────────────────────
@@ -49,15 +50,15 @@ static constexpr float P7P_R = 0.35f;
static constexpr float P7P_G = 0.88f; static constexpr float P7P_G = 0.88f;
static constexpr float P7P_B = 0.18f; static constexpr float P7P_B = 0.18f;
// Digits rendered into the font atlas: '0''9' // Full printable ASCII rendered into the font atlas: ' '(32) through '~'(126)
static constexpr int GLYPH_FIRST = '0'; static constexpr int GLYPH_FIRST = ' ';
static constexpr int GLYPH_COUNT = 10; static constexpr int GLYPH_COUNT = 95;
// Feature 3 timing // Feature 3 timing
static constexpr float HOLD_SEC = 20.0f; // seconds per range setting static constexpr float HOLD_SEC = 5.0f; // seconds per range setting
static constexpr float SLIDE_OUT_SEC = 5.0f; // slide current graticule out (up) static constexpr float SLIDE_OUT_SEC = 2.0f; // slide current graticule out (up)
static constexpr float WAIT_SEC = 2.0f; // blank pause between graticules static constexpr float WAIT_SEC = 0.5f; // blank pause between graticules
static constexpr float SLIDE_IN_SEC = 5.0f; // slide new graticule in (from top) static constexpr float SLIDE_IN_SEC = 2.0f; // slide new graticule in (from top)
// Feature 4 — sweep // Feature 4 — sweep
static constexpr float SWEEP_DEG_PS = 20.0f * 6.0f; // 20 RPM → 120 °/s 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 { struct Layout {
float ppiCX, ppiCY, ppiR; float ppiCX, ppiCY, ppiR;
float asLeft, asTop, asRight, asBot; 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) 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 availW = (W - marginPx) - W * 0.5f;
float availH = (H - marginPx) - marginPx; 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.ppiCX = W * 0.5f + availW * 0.5f;
L.ppiCY = marginPx + availH * 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.asTop = (H - asH) * 0.5f;
L.asBot = L.asTop + asH; 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; return L;
} }
@@ -802,10 +824,13 @@ static AScopeTrace buildAScopeTrace()
} }
// Renders target return spikes on the A scope clipped to the A scope box. // 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, static void renderAScopeTrace(AScopeTrace& at, const Layout& L,
FakeTarget* tgts, int nTgts, FakeTarget* tgts, int nTgts,
float maxRangeMi, float ascopeBearingDeg, 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 THRESH = 3.5f;
static const float PAD = 4.0f; 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 sigW = gx1 - gx0;
const float sigH = (L.asTop + PAD) - gy1; // negative: upward from baseline const float sigH = (L.asTop + PAD) - gy1; // negative: upward from baseline
const bool sweepHere = angDiff(sweepAngle, ascopeBearingDeg) < THRESH;
glEnable(GL_SCISSOR_TEST); glEnable(GL_SCISSOR_TEST);
glScissor((GLint)L.asLeft, (GLint)(H - L.asBot), glScissor((GLint)L.asLeft, (GLint)(H - L.asBot),
(GLint)(L.asRight - L.asLeft), (GLint)(L.asBot - L.asTop)); (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"); const GLint locCol = glGetUniformLocation(at.prog, "uColor");
for (int i = 0; i < nTgts; ++i) { for (int i = 0; i < nTgts; ++i) {
FakeTarget& t = tgts[i]; const FakeTarget& t = tgts[i];
if (sweepHere && angDiff(ascopeBearingDeg, t.bearingDeg) < THRESH) // Only show targets whose bearing falls within the A scope beam
at.lastActT[i] = curTime; if (angDiff(ascopeBearingDeg, t.bearingDeg) >= THRESH) continue;
float rangeFrac = t.rangeMiles / maxRangeMi; float rangeFrac = t.rangeMiles / maxRangeMi;
if (rangeFrac > 1.0f) continue; 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; float coreRad, bloomRad, baseBright;
sizeFromApparent(apparentFt(t), L.ppiR, 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 sx = gx0 + rangeFrac * sigW;
float sy0 = gy1; // baseline float sy0 = gy1;
float sy1 = gy1 + height; // peak (upward because sigH < 0) float sy1 = gy1 + height;
float verts[6] = { float verts[6] = {
ndcX(sx - HALF_W, W), ndcY(sy0, H), 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); glBindBuffer(GL_ARRAY_BUFFER, at.vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_DYNAMIC_DRAW); 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); glDrawArrays(GL_TRIANGLES, 0, 3);
} }
@@ -1025,6 +1043,180 @@ static void renderCursor(CursorLayer& cl, const Layout& L,
glBindVertexArray(0); 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 ───────────────────────────────────────────────────────────── // ─── Key callback ─────────────────────────────────────────────────────────────
static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/) static void onKey(GLFWwindow* win, int key, int /*scan*/, int action, int /*mods*/)
@@ -1097,15 +1289,15 @@ int main()
RingLayer rl = buildRingLayer(fa); RingLayer rl = buildRingLayer(fa);
// Feature 5 — Four fake targets per CLAUDE.md spec: // 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 // 1. 5 mi N, 100 ft / 20 ft, heading S at 1 kt (head-on) → apparent ~20 ft
// 2. 5 mi south, 20 ft long / 5 ft beam, heading south (stern-on) → apparent ~5 ft // 2. 5 mi S, 20 ft / 5 ft, heading S at 20 kt (stern-on) → apparent ~5 ft
// 3. 4 mi east, 30 ft long / 10 ft beam, heading north (full side) → apparent ~30 ft // 3. 6 mi E, 30 ft / 10 ft, heading N at 30 kt (full side) → apparent ~30 ft
// 4. 1 mi west, 100 ft long / 25 ft beam, heading south (full side) → apparent ~100 ft // 4. 6 mi W, 100 ft / 25 ft, heading S at 5 kt (full side) → apparent ~100 ft
FakeTarget targets[4] = { 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 { 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 { 90.0f, 6.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 { 270.0f, 6.0f, 100.0f, 25.0f, 180.0f, -999.0f }, // W full side, ~100 ft apparent
}; };
TargetLayer tl = buildTargetLayer(); TargetLayer tl = buildTargetLayer();
@@ -1115,6 +1307,13 @@ int main()
// Feature 6 — PPI cursor // Feature 6 — PPI cursor
CursorLayer cl = buildCursorLayer(); 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; const float ppiR = layout.ppiR;
// NDC height of A scope box — used for slide animation // NDC height of A scope box — used for slide animation
@@ -1127,8 +1326,10 @@ int main()
GratPhase gratPhase = GratPhase::HOLD; GratPhase gratPhase = GratPhase::HOLD;
float phaseTimer = 0.0f; float phaseTimer = 0.0f;
// A scope bearing (° true, CW from N) // A scope bearing (° true, CW from N).
const float ascopeBearingDeg = 90.0f; // 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 sweepAngle = 0.0f;
float prevTime = static_cast<float>(glfwGetTime()); 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 ────────────────────────────────────────────── // ── A scope signal trace ──────────────────────────────────────────────
renderAScopeTrace(at, layout, targets, 4, renderAScopeTrace(at, layout, targets, 4,
RANGE_CONFIGS[curRange].maxMiles, ascopeBearingDeg, RANGE_CONFIGS[curRange].maxMiles, ascopeBearingDeg,
sweepAngle, now, W, H); W, H);
// ── Feature 4: PPI range rings ──────────────────────────────────────── // ── Feature 4: PPI range rings ────────────────────────────────────────
renderRingLayer(rl, layout, curRange, sweepAngle, now, W, H, fa); renderRingLayer(rl, layout, curRange, sweepAngle, now, W, H, fa);
@@ -1249,12 +1454,22 @@ int main()
RANGE_CONFIGS[curRange].maxMiles, ppiR, RANGE_CONFIGS[curRange].maxMiles, ppiR,
sweepAngle, now, W, H); sweepAngle, now, W, H);
// ── Feature 6: PPI cursor (slow auto-animation for test) ────────────── // ── Feature 6: PPI cursor locked to targets[2] (east, 6 mi) ──────────
// Range oscillates between ~15 % and ~85 % over 12 s period. // New suggestion #5: cursor follows one target; not free-roaming.
// Bearing rotates slowly at 18 °/s (one full revolution per 20 s). // Only draw when the target is within the current range setting.
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); const float cursorRangeFrac = targets[2].rangeMiles
renderCursor(cl, layout, cursorRangeFrac, cursorBearingDeg, W, H); / 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); glfwSwapBuffers(win);
glfwPollEvents(); glfwPollEvents();
@@ -1287,6 +1502,10 @@ int main()
glDeleteProgram(at.prog); glDeleteProgram(at.prog);
glDeleteVertexArrays(1, &cl.vao); glDeleteBuffers(1, &cl.vbo); glDeleteVertexArrays(1, &cl.vao); glDeleteBuffers(1, &cl.vbo);
glDeleteProgram(cl.prog); 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); glfwDestroyWindow(win);
glfwTerminate(); glfwTerminate();