diff --git a/CLAUDE.md b/CLAUDE.md index 9a78153..79f856f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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; + diff --git a/build/CMakeFiles/radar_simulation.dir/src/main.cpp.o b/build/CMakeFiles/radar_simulation.dir/src/main.cpp.o index aa63105..30bdd9e 100644 Binary files a/build/CMakeFiles/radar_simulation.dir/src/main.cpp.o and b/build/CMakeFiles/radar_simulation.dir/src/main.cpp.o differ diff --git a/build/radar_simulation b/build/radar_simulation index 124da7d..2be59ce 100755 Binary files a/build/radar_simulation and b/build/radar_simulation differ diff --git a/src/main.cpp b/src/main.cpp index a8c5f9b..b131a20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include #include #include +#include // ─── 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& 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 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(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(bearingDeg) % 360; + if (iBear < 0) iBear += 360; + int iRng = static_cast(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 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(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 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(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 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(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(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();