#include "ui/Cursors.h" #include #include #include namespace baudmine { static double binToFreqHelper(int bin, double sampleRate, bool isIQ, int fftSize) { if (isIQ) { return -sampleRate / 2.0 + (static_cast(bin) / fftSize) * sampleRate; } else { return (static_cast(bin) / fftSize) * sampleRate; } } void Cursors::pushAvg(AvgState& st, float dB, int bin) const { // Reset if cursor moved to a different bin or averaging was reduced. if (bin != st.lastBin) { st.samples.clear(); st.sum = 0.0; st.lastBin = bin; } st.samples.push_back(dB); st.sum += dB; int maxN = std::max(1, avgCount); while (static_cast(st.samples.size()) > maxN) { st.sum -= st.samples.front(); st.samples.pop_front(); } } float Cursors::avgDBA() const { return avgA_.samples.empty() ? cursorA.dB : static_cast(avgA_.sum / avgA_.samples.size()); } float Cursors::avgDBB() const { return avgB_.samples.empty() ? cursorB.dB : static_cast(avgB_.sum / avgB_.samples.size()); } void Cursors::update(const std::vector& spectrumDB, double sampleRate, bool isIQ, int fftSize) { // Update dB values at cursor bin positions if (cursorA.active && cursorA.bin >= 0 && cursorA.bin < static_cast(spectrumDB.size())) { cursorA.dB = spectrumDB[cursorA.bin]; pushAvg(avgA_, cursorA.dB, cursorA.bin); } if (cursorB.active && cursorB.bin >= 0 && cursorB.bin < static_cast(spectrumDB.size())) { cursorB.dB = spectrumDB[cursorB.bin]; pushAvg(avgB_, cursorB.dB, cursorB.bin); } } void Cursors::draw(const SpectrumDisplay& specDisplay, float posX, float posY, float sizeX, float sizeY, double sampleRate, bool isIQ, FreqScale freqScale, float minDB, float maxDB, float viewLo, float viewHi) const { ImDrawList* dl = ImGui::GetWindowDrawList(); // Draw cursor lines and crosshairs (no labels here). auto drawCursorMarker = [&](const CursorInfo& c, float dispDB, ImU32 color) { if (!c.active) return; float x = specDisplay.freqToScreenX(c.freq, posX, sizeX, sampleRate, isIQ, freqScale, viewLo, viewHi); float dbNorm = (dispDB - minDB) / (maxDB - minDB); dbNorm = std::clamp(dbNorm, 0.0f, 1.0f); float y = posY + sizeY * (1.0f - dbNorm); dl->AddLine({x, posY}, {x, posY + sizeY}, color, 1.0f); dl->AddLine({posX, y}, {posX + sizeX, y}, color & 0x80FFFFFF, 1.0f); dl->AddCircle({x, y}, 5.0f, color, 12, 2.0f); }; // Format a cursor label string. auto formatLabel = [](char* buf, size_t sz, const char* label, double freq, float dB) { fmtFreqDB(buf, sz, label, freq, dB); }; float aDB = avgDBA(), bDB = avgDBB(); drawCursorMarker(cursorA, aDB, IM_COL32(255, 255, 0, 220)); drawCursorMarker(cursorB, bDB, IM_COL32(100, 220, 255, 220)); // Draw labels at the top, touching the cursor's vertical line. // If the label would overflow the right edge, flip it to the left side. auto drawCursorLabel = [&](const CursorInfo& c, float dispDB, ImU32 color, const char* label, int row) { if (!c.active) return; float x = specDisplay.freqToScreenX(c.freq, posX, sizeX, sampleRate, isIQ, freqScale, viewLo, viewHi); char buf[128]; formatLabel(buf, sizeof(buf), label, c.freq, dispDB); ImVec2 sz = ImGui::CalcTextSize(buf); float lineH = ImGui::GetTextLineHeight(); // draw starting from the second line -- on the first line, we have cursor data float ty = posY + 4 + (row + 1) * (lineH + 4); // Place right of cursor line; flip left if it would overflow. float tx; if (x + 6 + sz.x + 2 <= posX + sizeX) tx = x + 6; else tx = x - 6 - sz.x; dl->AddRectFilled({tx - 2, ty - 1}, {tx + sz.x + 2, ty + sz.y + 1}, IM_COL32(0, 0, 0, 180)); dl->AddText({tx, ty}, color, buf); }; drawCursorLabel(cursorA, aDB, IM_COL32(255, 255, 0, 220), "A", 0); drawCursorLabel(cursorB, bDB, IM_COL32(100, 220, 255, 220), "B", cursorA.active ? 1 : 0); // Delta display (two lines, column-aligned on '=') if (showDelta && cursorA.active && cursorB.active) { double dFreq = cursorB.freq - cursorA.freq; float dDB = bDB - aDB; char deltaBuf[128]; fmtFreqDB(deltaBuf, sizeof(deltaBuf), "D", dFreq, dDB); ImVec2 dSz = ImGui::CalcTextSize(deltaBuf); // Reserve space for hover label to the right. float reserveW = ImGui::CalcTextSize(" 00.000 kHz 000.0 dB").x; float tx = posX + sizeX - dSz.x - reserveW; float lineH = ImGui::GetTextLineHeight(); float ty = posY + 4; ImU32 col = IM_COL32(255, 200, 100, 255); dl->AddText({tx, ty}, col, deltaBuf); } // (Hover cursor line is drawn cross-panel by Application.) } void Cursors::drawPanel() { auto showCursor = [](const char* label, const CursorInfo& c, float dispDB) { if (!c.active) { ImGui::TextDisabled("%s: --", label); return; } char buf[128]; fmtFreqDB(buf, sizeof(buf), label, c.freq, dispDB); ImGui::Text("%s", buf); }; float aDB = avgDBA(), bDB = avgDBB(); showCursor("A", cursorA, aDB); showCursor("B", cursorB, bDB); if (cursorA.active && cursorB.active) { double dF = cursorB.freq - cursorA.freq; float dA = bDB - aDB; char dbuf[128]; fmtFreqDB(dbuf, sizeof(dbuf), "D", dF, dA); ImGui::Text("%s", dbuf); } ImGui::Checkbox("Snap to peaks", &snapToPeaks); // Averaging slider (logarithmic scale) ImGui::SetNextItemWidth(-1); ImGui::SliderInt("##avgcount", &avgCount, 1, 20000, avgCount == 1 ? "No avg" : "Avg: %d", ImGuiSliderFlags_Logarithmic); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Cursor averaging (samples)"); } void Cursors::setCursorA(double freq, float dB, int bin) { cursorA = {true, freq, dB, bin}; } void Cursors::setCursorB(double freq, float dB, int bin) { cursorB = {true, freq, dB, bin}; } void Cursors::snapToPeak(const std::vector& spectrumDB, double sampleRate, bool isIQ, int fftSize) { if (spectrumDB.empty()) return; auto it = std::max_element(spectrumDB.begin(), spectrumDB.end()); int bin = static_cast(std::distance(spectrumDB.begin(), it)); double freq = binToFreqHelper(bin, sampleRate, isIQ, fftSize); setCursorA(freq, *it, bin); } int Cursors::findLocalPeak(const std::vector& spectrumDB, int centerBin, int window) const { int bins = static_cast(spectrumDB.size()); int lo = std::max(0, centerBin - window); int hi = std::min(bins - 1, centerBin + window); int best = lo; for (int i = lo + 1; i <= hi; ++i) { if (spectrumDB[i] > spectrumDB[best]) best = i; } return best; } } // namespace baudmine