fix: dynamic frequency labels and fix frequency clamping crash

- Frequency tick/label intervals now adapt to zoom level instead of
  always showing 0–Nyquist (5Hz → 10kHz depending on zoom)
- Labels use coarser spacing than ticks to avoid clutter
- Fix segfault when zooming out past signal bounds by properly clamping
  freqViewStart/freqViewEnd to [0,1] with correct width preservation
  in zoom, pan, and scrollbar drag
- Fix firstTick calculation that was off-by-one
This commit is contained in:
2026-05-24 22:14:17 -07:00
parent 1cbba956b8
commit 78cd21c957
+62 -20
View File
@@ -1123,27 +1123,50 @@ static void DrawLabels(Rectangle bounds)
DrawTextScaled(label, x, bounds.y + bounds.height + 5, baseFontSize, textColor); DrawTextScaled(label, x, bounds.y + bounds.height + 5, baseFontSize, textColor);
} }
// Frequency labels at 1kHz intervals with 200Hz ticks // Frequency labels adapted to current zoom level
float maxFreq = (float)app.signal.sampleRate / 2.0f; float maxFreq = (float)app.signal.sampleRate / 2.0f;
int maxKhz = (int)(maxFreq / 1000.0f); float freqMin = app.freqViewStart * maxFreq;
float freqMax = app.freqViewEnd * maxFreq;
for (int hz = 0; hz <= maxFreq; hz += 200) { // Choose tick spacing based on zoom range
float t = hz / maxFreq; float freqRange = freqMax - freqMin;
int tickSpacing;
if (freqRange < 20) tickSpacing = 5;
else if (freqRange < 50) tickSpacing = 10;
else if (freqRange < 200) tickSpacing = 50;
else if (freqRange < 1000) tickSpacing = 100;
else if (freqRange < 5000) tickSpacing = 200;
else if (freqRange < 20000) tickSpacing = 1000;
else if (freqRange < 50000) tickSpacing = 5000;
else tickSpacing = 10000;
// Labels use next coarser spacing so they stay readable
int labelSpacing = tickSpacing;
if (labelSpacing <= 10) labelSpacing = 10;
else if (labelSpacing <= 50) labelSpacing = 50;
else if (labelSpacing <= 200) labelSpacing = 200;
else if (labelSpacing <= 1000) labelSpacing = 1000;
else if (labelSpacing <= 5000) labelSpacing = 5000;
else labelSpacing = 10000;
// Round freqMin up to nearest tick spacing (smallest multiple >= freqMin)
int firstTick = ((int)(freqMin / tickSpacing)) * tickSpacing;
if (firstTick < freqMin) firstTick += tickSpacing;
for (int hz = firstTick; hz <= freqMax; hz += tickSpacing) {
float t = (hz - freqMin) / freqRange;
float y = bounds.y + bounds.height - t * bounds.height; float y = bounds.y + bounds.height - t * bounds.height;
Color tickColor = (hz % 1000 == 0) ? GRAY : Fade(GRAY, 0.4f); Color tickColor = (hz % 1000 == 0) ? GRAY : Fade(GRAY, 0.4f);
DrawLineV((Vector2){ bounds.x - 5, y }, (Vector2){ bounds.x, y }, tickColor); DrawLineV((Vector2){ bounds.x - 5, y }, (Vector2){ bounds.x, y }, tickColor);
} }
for (int khz = 0; khz <= maxKhz; khz++) { // Draw labels at the coarser spacing
float freq = khz * 1000.0f; for (int hz = firstTick; hz <= freqMax; hz += labelSpacing) {
if (freq > maxFreq) break; float t = (hz - freqMin) / freqRange;
float t = freq / maxFreq;
float y = bounds.y + bounds.height - t * bounds.height; float y = bounds.y + bounds.height - t * bounds.height;
char label[32]; char label[32];
if (khz == 0) sprintf(label, "0"); if (hz < 10000) sprintf(label, "%.0fHz", (float)hz);
else if (khz < 10) sprintf(label, "%dk", khz); else sprintf(label, "%.0fkHz", (float)hz / 1000.0f);
else sprintf(label, "%d", khz); DrawTextScaled(label, bounds.x - 70, y - 5, baseFontSize, textColor);
DrawTextScaled(label, bounds.x - 15, y - 5, baseFontSize, textColor);
} }
} }
@@ -1693,14 +1716,14 @@ int main(int argc, char* argv[])
mouseF = app.freqViewStart + mouseF * (app.freqViewEnd - app.freqViewStart); mouseF = app.freqViewStart + mouseF * (app.freqViewEnd - app.freqViewStart);
float freqWidth = app.freqViewEnd - app.freqViewStart; float freqWidth = app.freqViewEnd - app.freqViewStart;
float newFreqWidth = freqWidth * zoomFactor; float newFreqWidth = freqWidth * zoomFactor;
if (newFreqWidth < 0.02f) newFreqWidth = 0.02f; if (newFreqWidth < 0.001f) newFreqWidth = 0.001f;
if (newFreqWidth > 1.0f) newFreqWidth = 1.0f;
float belowMouse = mouseF - app.freqViewStart; float belowMouse = mouseF - app.freqViewStart;
float aboveMouse = app.freqViewEnd - mouseF; float aboveMouse = app.freqViewEnd - mouseF;
app.freqViewStart = mouseF - belowMouse * (newFreqWidth / freqWidth); app.freqViewStart = mouseF - belowMouse * (newFreqWidth / freqWidth);
app.freqViewEnd = mouseF + aboveMouse * (newFreqWidth / freqWidth); app.freqViewEnd = mouseF + aboveMouse * (newFreqWidth / freqWidth);
if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = newFreqWidth; } // Clamp to physical frequency limits [0, 1] — can't see beyond Nyquist or below 0 Hz
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = 1 - newFreqWidth; } if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = fminf(app.freqViewEnd, 1.0f); }
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = fmaxf(app.freqViewStart, 0.0f); }
// Invalidate texture cache // Invalidate texture cache
app.visibleTextureValid = false; app.visibleTextureValid = false;
@@ -1728,8 +1751,19 @@ int main(int argc, char* argv[])
if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - viewWidth; } if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - viewWidth; }
app.freqViewStart = app.panStartFreqViewStart + dy * freqWidth; app.freqViewStart = app.panStartFreqViewStart + dy * freqWidth;
app.freqViewEnd = app.panStartFreqViewEnd + dy * freqWidth; app.freqViewEnd = app.panStartFreqViewEnd + dy * freqWidth;
if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = freqWidth; } // Clamp to physical limits [0, 1]
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = 1 - freqWidth; } if (app.freqViewStart < 0) {
float actualWidth = app.freqViewEnd - app.freqViewStart;
app.freqViewStart = 0;
app.freqViewEnd = fminf(actualWidth, 1.0f);
}
if (app.freqViewEnd > 1) {
float actualWidth = app.freqViewEnd - app.freqViewStart;
app.freqViewEnd = 1;
app.freqViewStart = fmaxf(1.0f - actualWidth, 0.0f);
}
if (app.freqViewStart < 0) app.freqViewStart = 0;
if (app.freqViewEnd > 1) app.freqViewEnd = 1;
app.visibleTextureValid = false; app.visibleTextureValid = false;
} }
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.isPanning = false; if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.isPanning = false;
@@ -2182,8 +2216,16 @@ int main(int argc, char* argv[])
float freqWidth = app.freqViewEnd - app.freqViewStart; float freqWidth = app.freqViewEnd - app.freqViewStart;
app.freqViewStart = dragStartFreqViewStart - dy; app.freqViewStart = dragStartFreqViewStart - dy;
app.freqViewEnd = app.freqViewStart + freqWidth; app.freqViewEnd = app.freqViewStart + freqWidth;
if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = freqWidth; } if (app.freqViewStart < 0) {
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = 1 - freqWidth; } app.freqViewStart = 0;
app.freqViewEnd = fminf(freqWidth, 1.0f);
}
if (app.freqViewEnd > 1) {
app.freqViewEnd = 1;
app.freqViewStart = fmaxf(1.0f - freqWidth, 0.0f);
}
if (app.freqViewStart < 0) app.freqViewStart = 0;
if (app.freqViewEnd > 1) app.freqViewEnd = 1;
app.visibleTextureValid = false; app.visibleTextureValid = false;
} }
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { draggingH = false; draggingV = false; } if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { draggingH = false; draggingV = false; }