/* * MIT License * Author: Mark Allyn * * graticule.cpp — incandescent bearing graticule and cursor overlay. */ #include "graticule.h" #include "left_panel.h" // for TextRenderer definition #include #include #include #include #include static const float PI = 3.14159265f; // ---- Shader helpers (same pattern as phosphor.cpp) ---- static std::string gReadFile(const std::string& p) { std::ifstream f(p); if (!f.is_open()) { fprintf(stderr,"graticule: can't open %s\n",p.c_str()); return ""; } std::ostringstream ss; ss << f.rdbuf(); return ss.str(); } static GLuint gCompile(GLenum type, const std::string& src) { const char* s = src.c_str(); GLuint sh = glCreateShader(type); glShaderSource(sh,1,&s,nullptr); glCompileShader(sh); GLint ok=0; glGetShaderiv(sh,GL_COMPILE_STATUS,&ok); if(!ok){char l[1024]; glGetShaderInfoLog(sh,1024,nullptr,l); fprintf(stderr,"grat shader: %s\n",l);} return sh; } static GLuint gLink(const std::string& vp, const std::string& fp) { std::string vs=gReadFile(vp), fs=gReadFile(fp); if(vs.empty()||fs.empty()) return 0; GLuint v=gCompile(GL_VERTEX_SHADER,vs), f=gCompile(GL_FRAGMENT_SHADER,fs); GLuint p=glCreateProgram(); glAttachShader(p,v); glAttachShader(p,f); glLinkProgram(p); GLint ok=0; glGetProgramiv(p,GL_LINK_STATUS,&ok); if(!ok){char l[1024]; glGetProgramInfoLog(p,1024,nullptr,l); fprintf(stderr,"grat prog: %s\n",l);} glDeleteShader(v); glDeleteShader(f); return p; } // ---------------------------------------------------------------- static float brgToRadians(float deg, float offset) { // Convert bearing (CW from north) to math angle (CCW from east), // subtracting any display offset. Result in radians. float a = 90.0f - (deg - offset); return a * PI / 180.0f; } // ---------------------------------------------------------------- bool Graticule::init(const std::string& shaderDir, TextRenderer& tr, float cx, float cy, float radius) { cx_ = cx; cy_ = cy; r_ = radius; tr_ = &tr; prog_ = gLink(shaderDir + "graticule.vert", shaderDir + "graticule.frag"); if (!prog_) return false; buildRingGeometry(); buildTickGeometry(); // Pre-compute label positions at GRAT_LABEL_INTERVAL_DEG steps labels_.clear(); float labelR = r_ * GRAT_LABEL_RING_FRAC; for (int deg = 0; deg < 360; deg += GRAT_LABEL_INTERVAL_DEG) { float rad = brgToRadians(static_cast(deg), 0.0f); LabelPos lp; lp.x = cx_ + labelR * std::cos(rad); lp.y = cy_ - labelR * std::sin(rad); // screen y is inverted snprintf(lp.text, sizeof(lp.text), "%d", deg); labels_.push_back(lp); } // Cursor geometry is dynamic; create empty VAO/VBO now glGenVertexArrays(1, &cursVAO_); glGenBuffers(1, &cursVBO_); glBindVertexArray(cursVAO_); glBindBuffer(GL_ARRAY_BUFFER, cursVBO_); glBufferData(GL_ARRAY_BUFFER, 200 * sizeof(float) * 2, nullptr, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), nullptr); glBindVertexArray(0); return true; } void Graticule::buildRingGeometry() { const int N = GRAT_SEGMENTS; std::vector verts; verts.reserve((N + 1) * 2 * 2); // inner + outer ring for (int ring = 0; ring < 2; ++ring) { float rad = r_ * (ring == 0 ? GRAT_INNER_RING_FRAC : GRAT_OUTER_RING_FRAC); for (int i = 0; i <= N; ++i) { float a = 2.0f * PI * i / N; verts.push_back(cx_ + rad * std::cos(a)); verts.push_back(cy_ - rad * std::sin(a)); // flip y for screen } } ringVertCount_ = static_cast(verts.size()) / 2; glGenVertexArrays(1, &ringVAO_); glGenBuffers(1, &ringVBO_); glBindVertexArray(ringVAO_); glBindBuffer(GL_ARRAY_BUFFER, ringVBO_); glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), nullptr); glBindVertexArray(0); } void Graticule::buildTickGeometry() { std::vector verts; verts.reserve(360 * 4); // 2 verts × 2 floats per tick float innerR = r_ * GRAT_INNER_RING_FRAC; for (int deg = 0; deg < 360; ++deg) { float rad = brgToRadians(static_cast(deg), 0.0f); float cR = std::cos(rad); float sR = std::sin(rad); // Major ticks every 10 degrees, minor otherwise float len = (deg % 10 == 0) ? r_ * GRAT_TICK_MAJOR_FRAC : r_ * GRAT_TICK_MINOR_FRAC; float x0 = cx_ + innerR * cR; float y0 = cy_ - innerR * sR; float x1 = cx_ + (innerR - len) * cR; float y1 = cy_ - (innerR - len) * sR; verts.push_back(x0); verts.push_back(y0); verts.push_back(x1); verts.push_back(y1); } tickVertCount_ = static_cast(verts.size()) / 2; glGenVertexArrays(1, &tickVAO_); glGenBuffers(1, &tickVBO_); glBindVertexArray(tickVAO_); glBindBuffer(GL_ARRAY_BUFFER, tickVBO_); glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), nullptr); glBindVertexArray(0); } // ---------------------------------------------------------------- void Graticule::render(float viewportW, float viewportH, float intensity, float /*bearingOffset*/) const { if (!prog_) return; glUseProgram(prog_); glUniform2f(glGetUniformLocation(prog_, "u_viewportSize"), viewportW, viewportH); float r = GRAT_R * intensity; float g = GRAT_G * intensity; float b = GRAT_B * intensity; glUniform4f(glGetUniformLocation(prog_, "u_color"), r, g, b, 1.0f); // Inner and outer rings (LINE_STRIP segments) glBindVertexArray(ringVAO_); int half = ringVertCount_ / 2; glDrawArrays(GL_LINE_STRIP, 0, half); // inner glDrawArrays(GL_LINE_STRIP, half, half); // outer glBindVertexArray(0); // Tick marks glBindVertexArray(tickVAO_); glDrawArrays(GL_LINES, 0, tickVertCount_); glBindVertexArray(0); // Text labels if (tr_) { for (const auto& lp : labels_) { tr_->renderText(lp.text, lp.x, lp.y, static_cast(GRATICULE_LABEL_FONT_SIZE) / 16.0f, r, g, b, viewportW, viewportH, true /* centred */); } } } void Graticule::renderCursor(float viewportW, float viewportH, float brgDeg, float rngNorm) const { if (!prog_ || !cursVAO_) return; // Build cursor geometry: 10° arc + bearing crossline std::vector verts; const float arcSpan = CURSOR_ARC_SPAN_DEG; const float arcSteps = 20.0f; const float cursorR = r_ * rngNorm; // Arc for (int i = 0; i <= (int)arcSteps; ++i) { float a = brgDeg - arcSpan * 0.5f + arcSpan * i / arcSteps; float rad = brgToRadians(a, 0.0f); verts.push_back(cx_ + cursorR * std::cos(rad)); verts.push_back(cy_ - cursorR * std::sin(rad)); } // Bearing crossline: scope centre → just beyond arc float lineRad = brgToRadians(brgDeg, 0.0f); float lineLen = cursorR + r_ * 0.04f; // slightly beyond arc verts.push_back(cx_); verts.push_back(cy_); verts.push_back(cx_ + lineLen * std::cos(lineRad)); verts.push_back(cy_ - lineLen * std::sin(lineRad)); int nVerts = static_cast(verts.size()) / 2; int arcEnd = static_cast(arcSteps) + 1; glBindBuffer(GL_ARRAY_BUFFER, cursVBO_); glBufferSubData(GL_ARRAY_BUFFER, 0, verts.size() * sizeof(float), verts.data()); glUseProgram(prog_); glUniform2f(glGetUniformLocation(prog_, "u_viewportSize"), viewportW, viewportH); glUniform4f(glGetUniformLocation(prog_, "u_color"), CURSOR_R, CURSOR_G, CURSOR_B, 1.0f); glBindVertexArray(cursVAO_); glDrawArrays(GL_LINE_STRIP, 0, arcEnd); // arc glDrawArrays(GL_LINES, arcEnd, 2); // crossline glBindVertexArray(0); } Graticule::~Graticule() { if (prog_) glDeleteProgram(prog_); if (ringVAO_) glDeleteVertexArrays(1, &ringVAO_); if (ringVBO_) glDeleteBuffers(1, &ringVBO_); if (tickVAO_) glDeleteVertexArrays(1, &tickVAO_); if (tickVBO_) glDeleteBuffers(1, &tickVBO_); if (cursVAO_) glDeleteVertexArrays(1, &cursVAO_); if (cursVBO_) glDeleteBuffers(1, &cursVBO_); }