refactor: group selection state into a Selection sub-struct + ClearSelection()

The box-selection state was 11 fields spread across three comment blocks
(time sel, freq sel, drag state). Consolidate them into one Selection
sub-struct (app.sel.*) and extract the 4-line 'clear to full range' block —
duplicated in 5 places — into a ClearSelection() helper.

Pure mechanical rename (anchored to app./spa-> so the ScopeView fields of the
same name are untouched) + behavior-identical dedup. Fully-settled render
verified pixel-identical (AE=0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 09:15:50 -07:00
parent ab7a6bc71c
commit 8e4250ae63
5 changed files with 103 additions and 107 deletions
+4 -4
View File
@@ -182,8 +182,8 @@ void PlaySelectedRegion(void)
{ {
if (!app.loaded || !app.stftComputed) return; if (!app.loaded || !app.stftComputed) return;
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples); int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples); int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
int numSamples = endSample - startSample; int numSamples = endSample - startSample;
if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return; if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return;
@@ -191,8 +191,8 @@ void PlaySelectedRegion(void)
memcpy(regionSamples, app.signal.samples + startSample, numSamples * sizeof(float)); memcpy(regionSamples, app.signal.samples + startSample, numSamples * sizeof(float));
float maxFreq = (float)app.signal.sampleRate / 2.0f; float maxFreq = (float)app.signal.sampleRate / 2.0f;
float freqLow = app.freqSelectionStart * maxFreq; float freqLow = app.sel.freqStart * maxFreq;
float freqHigh = app.freqSelectionEnd * maxFreq; float freqHigh = app.sel.freqEnd * maxFreq;
if (freqLow > 10.0f || freqHigh < maxFreq - 10.0f) { if (freqLow > 10.0f || freqHigh < maxFreq - 10.0f) {
TraceLog(LOG_INFO, "Applying bandpass filter: %.0f - %.0f Hz", freqLow, freqHigh); TraceLog(LOG_INFO, "Applying bandpass filter: %.0f - %.0f Hz", freqLow, freqHigh);
ApplyBandpassFilter(regionSamples, numSamples, app.signal.sampleRate, freqLow, freqHigh); ApplyBandpassFilter(regionSamples, numSamples, app.signal.sampleRate, freqLow, freqHigh);
+18 -18
View File
@@ -341,9 +341,9 @@ void DrawLabels(Rectangle bounds)
void DrawSelection(Rectangle bounds) void DrawSelection(Rectangle bounds)
{ {
// Only draw if selection is not full range AND not currently dragging // Only draw if selection is not full range AND not currently dragging
bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f || bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f ||
app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f); app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
if (!hasSelection || app.isTimeSelecting) return; // Don't draw overlay while dragging if (!hasSelection || app.sel.isTimeSelecting) return; // Don't draw overlay while dragging
Color overlayColor = Fade(BLACK, 0.25f); // Lighter overlay Color overlayColor = Fade(BLACK, 0.25f); // Lighter overlay
@@ -351,10 +351,10 @@ void DrawSelection(Rectangle bounds)
float viewWidth = app.viewEnd - app.viewStart; float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart; float freqWidth = app.freqViewEnd - app.freqViewStart;
float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width; float selStartX = bounds.x + ((app.sel.timeStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width; float selEndX = bounds.x + ((app.sel.timeEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height; float selStartY = bounds.y + bounds.height - ((app.sel.freqEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height; float selEndY = bounds.y + bounds.height - ((app.sel.freqStart - app.freqViewStart) / freqWidth) * bounds.height;
// Clamp to viewport bounds // Clamp to viewport bounds
selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX)); selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX));
@@ -373,8 +373,8 @@ void DrawSelection(Rectangle bounds)
// Display selection stats inside viewport, clamped to fit // Display selection stats inside viewport, clamped to fit
{ {
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples); int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples); int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample); SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample);
if (stats.durationSec > 0.0f && app.signal.samples != NULL) { if (stats.durationSec > 0.0f && app.signal.samples != NULL) {
@@ -439,17 +439,17 @@ void DrawSelection(Rectangle bounds)
void DrawSelectionDrag(Rectangle bounds) void DrawSelectionDrag(Rectangle bounds)
{ {
// Draw bounding box while dragging (no overlay) // Draw bounding box while dragging (no overlay)
if ((!app.isTimeSelecting && !app.isFreqSelecting && !app.isDraggingSelection) || if ((!app.sel.isTimeSelecting && !app.sel.isFreqSelecting && !app.sel.isDragging) ||
(app.isDraggingSelection && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return; (app.sel.isDragging && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return;
// Convert signal coordinates to viewport coordinates // Convert signal coordinates to viewport coordinates
float viewWidth = app.viewEnd - app.viewStart; float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart; float freqWidth = app.freqViewEnd - app.freqViewStart;
float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width; float selStartX = bounds.x + ((app.sel.timeStart - app.viewStart) / viewWidth) * bounds.width;
float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width; float selEndX = bounds.x + ((app.sel.timeEnd - app.viewStart) / viewWidth) * bounds.width;
float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height; float selStartY = bounds.y + bounds.height - ((app.sel.freqEnd - app.freqViewStart) / freqWidth) * bounds.height;
float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height; float selEndY = bounds.y + bounds.height - ((app.sel.freqStart - app.freqViewStart) / freqWidth) * bounds.height;
// Clamp to viewport bounds // Clamp to viewport bounds
selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX)); selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX));
@@ -467,8 +467,8 @@ void DrawSelectionDrag(Rectangle bounds)
// Display live stats while dragging (inside viewport, clamped to fit) // Display live stats while dragging (inside viewport, clamped to fit)
{ {
int startSample = (int)(app.timeSelectionStart * app.signal.numSamples); int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples); int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample); SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample);
if (stats.durationSec > 0.0f && app.signal.samples != NULL) { if (stats.durationSec > 0.0f && app.signal.samples != NULL) {
@@ -538,7 +538,7 @@ void DrawPlayhead(Rectangle bounds)
{ {
if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return; if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return;
float timePos = app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart); float timePos = app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart);
float viewWidth = app.viewEnd - app.viewStart; float viewWidth = app.viewEnd - app.viewStart;
float t = (timePos - app.viewStart) / viewWidth; float t = (timePos - app.viewStart) / viewWidth;
float x = bounds.x + t * bounds.width; float x = bounds.x + t * bounds.width;
+52 -62
View File
@@ -120,8 +120,7 @@ void ResetForNewSignal(void)
// Reset view + selection to full range. // Reset view + selection to full range.
app.viewStart = 0.0f; app.viewEnd = 1.0f; app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f; ClearSelection();
app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f;
// Invalidate the cached visible texture. // Invalidate the cached visible texture.
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
@@ -223,8 +222,8 @@ int main(int argc, char* argv[])
TraceLog(LOG_WARNING, "Failed to load TTF font, using default bitmap font"); TraceLog(LOG_WARNING, "Failed to load TTF font, using default bitmap font");
} }
app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f; app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f; app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
app.viewStart = 0.0f; app.viewEnd = 1.0f; app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f; app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.showGrid = true; app.showGrid = true;
@@ -326,7 +325,7 @@ int main(int argc, char* argv[])
} }
// Track playhead position manually // Track playhead position manually
app.playheadElapsed += GetFrameTime(); app.playheadElapsed += GetFrameTime();
float selectionDuration = (app.timeSelectionEnd - app.timeSelectionStart) * app.signal.duration; float selectionDuration = (app.sel.timeEnd - app.sel.timeStart) * app.signal.duration;
if (selectionDuration > 0) { if (selectionDuration > 0) {
app.playheadT = app.playheadElapsed / selectionDuration; app.playheadT = app.playheadElapsed / selectionDuration;
} }
@@ -528,10 +527,7 @@ int main(int argc, char* argv[])
app.showFileBrowser = false; app.showFileBrowser = false;
} else { } else {
// Clear selections instead of exiting // Clear selections instead of exiting
app.timeSelectionStart = 0.0f; ClearSelection();
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
} }
} }
@@ -556,15 +552,12 @@ int main(int argc, char* argv[])
// Right-click clears selection // Right-click clears selection
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) { if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) {
app.timeSelectionStart = 0.0f; ClearSelection();
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
} }
// Check if click is inside existing selection (for dragging) // Check if click is inside existing selection (for dragging)
bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f || bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f ||
app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f); app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
bool clickInsideSelection = false; bool clickInsideSelection = false;
bool hoverInsideSelection = false; bool hoverInsideSelection = false;
if (hasSelection && CheckCollisionPointRec(mousePos, selBounds)) { if (hasSelection && CheckCollisionPointRec(mousePos, selBounds)) {
@@ -574,8 +567,8 @@ int main(int argc, char* argv[])
float mouseTime = app.viewStart + ((mousePos.x - selBounds.x) / selBounds.width) * viewWidth; float mouseTime = app.viewStart + ((mousePos.x - selBounds.x) / selBounds.width) * viewWidth;
float mouseFreq = app.freqViewStart + (1.0f - (mousePos.y - selBounds.y) / selBounds.height) * freqWidth; float mouseFreq = app.freqViewStart + (1.0f - (mousePos.y - selBounds.y) / selBounds.height) * freqWidth;
if (mouseTime >= app.timeSelectionStart && mouseTime <= app.timeSelectionEnd && if (mouseTime >= app.sel.timeStart && mouseTime <= app.sel.timeEnd &&
mouseFreq >= app.freqSelectionStart && mouseFreq <= app.freqSelectionEnd) { mouseFreq >= app.sel.freqStart && mouseFreq <= app.sel.freqEnd) {
hoverInsideSelection = true; hoverInsideSelection = true;
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
clickInsideSelection = true; clickInsideSelection = true;
@@ -584,7 +577,7 @@ int main(int argc, char* argv[])
} }
// Set cursor based on context // Set cursor based on context
if (app.isDraggingSelection) { if (app.sel.isDragging) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
} else if (hoverInsideSelection) { } else if (hoverInsideSelection) {
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
@@ -597,7 +590,7 @@ int main(int argc, char* argv[])
// Set cursor to resize all when near divider // Set cursor to resize all when near divider
if (mouseNearDivider && !app.isDividing) { if (mouseNearDivider && !app.isDividing) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
} else if (app.isDraggingSelection) { } else if (app.sel.isDragging) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
} else if (hoverInsideSelection) { } else if (hoverInsideSelection) {
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
@@ -607,85 +600,82 @@ int main(int argc, char* argv[])
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
if (clickInsideSelection) { if (clickInsideSelection) {
// Start dragging existing selection // Start dragging existing selection
app.isDraggingSelection = true; app.sel.isDragging = true;
app.dragSelectionStartPos = mousePos; app.sel.dragStartPos = mousePos;
app.dragSelectionTimeStart = app.timeSelectionStart; app.sel.dragTimeStart = app.sel.timeStart;
app.dragSelectionFreqStart = app.freqSelectionStart; app.sel.dragFreqStart = app.sel.freqStart;
} else { } else {
// Start new box selection // Start new box selection
app.isTimeSelecting = true; app.sel.isTimeSelecting = true;
app.isFreqSelecting = true; app.sel.isFreqSelecting = true;
app.selectStartPos = mousePos; app.sel.selectStartPos = mousePos;
// Convert screen position to signal coordinates (accounting for zoom) // Convert screen position to signal coordinates (accounting for zoom)
float viewportT = (mousePos.x - selBounds.x) / selBounds.width; float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height; float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.timeSelectionStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f); app.sel.timeStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.timeSelectionEnd = app.timeSelectionStart; app.sel.timeEnd = app.sel.timeStart;
app.freqSelectionStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f); app.sel.freqStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
app.freqSelectionEnd = app.freqSelectionStart; app.sel.freqEnd = app.sel.freqStart;
} }
} }
// Dragging existing selection // Dragging existing selection
if (app.isDraggingSelection && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { if (app.sel.isDragging && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float viewWidth = app.viewEnd - app.viewStart; float viewWidth = app.viewEnd - app.viewStart;
float freqWidth = app.freqViewEnd - app.freqViewStart; float freqWidth = app.freqViewEnd - app.freqViewStart;
float dx = (mousePos.x - app.dragSelectionStartPos.x) / selBounds.width; float dx = (mousePos.x - app.sel.dragStartPos.x) / selBounds.width;
float dy = (mousePos.y - app.dragSelectionStartPos.y) / selBounds.height; float dy = (mousePos.y - app.sel.dragStartPos.y) / selBounds.height;
float timeShift = dx * viewWidth; float timeShift = dx * viewWidth;
float freqShift = -dy * freqWidth; // Y is inverted float freqShift = -dy * freqWidth; // Y is inverted
float timeWidth = app.timeSelectionEnd - app.timeSelectionStart; float timeWidth = app.sel.timeEnd - app.sel.timeStart;
float freqHeight = app.freqSelectionEnd - app.freqSelectionStart; float freqHeight = app.sel.freqEnd - app.sel.freqStart;
app.timeSelectionStart = Clamp(app.dragSelectionTimeStart + timeShift, 0.0f, 1.0f - timeWidth); app.sel.timeStart = Clamp(app.sel.dragTimeStart + timeShift, 0.0f, 1.0f - timeWidth);
app.timeSelectionEnd = app.timeSelectionStart + timeWidth; app.sel.timeEnd = app.sel.timeStart + timeWidth;
app.freqSelectionStart = Clamp(app.dragSelectionFreqStart + freqShift, 0.0f, 1.0f - freqHeight); app.sel.freqStart = Clamp(app.sel.dragFreqStart + freqShift, 0.0f, 1.0f - freqHeight);
app.freqSelectionEnd = app.freqSelectionStart + freqHeight; app.sel.freqEnd = app.sel.freqStart + freqHeight;
} }
// Creating new box selection // Creating new box selection
if ((app.isTimeSelecting || app.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { if ((app.sel.isTimeSelecting || app.sel.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float viewportT = (mousePos.x - selBounds.x) / selBounds.width; float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height; float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height;
app.timeSelectionEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f); app.sel.timeEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f);
app.freqSelectionEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f); app.sel.freqEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f);
} }
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
if (app.isDraggingSelection) { if (app.sel.isDragging) {
app.isDraggingSelection = false; app.sel.isDragging = false;
} else if (app.isTimeSelecting || app.isFreqSelecting) { } else if (app.sel.isTimeSelecting || app.sel.isFreqSelecting) {
// Check if drag was large enough (minimum 5 pixels) // Check if drag was large enough (minimum 5 pixels)
float dx = mousePos.x - app.selectStartPos.x; float dx = mousePos.x - app.sel.selectStartPos.x;
float dy = mousePos.y - app.selectStartPos.y; float dy = mousePos.y - app.sel.selectStartPos.y;
float dragDist = sqrtf(dx * dx + dy * dy); float dragDist = sqrtf(dx * dx + dy * dy);
if (dragDist > 5.0f) { if (dragDist > 5.0f) {
// Normalize so start < end // Normalize so start < end
if (app.timeSelectionEnd < app.timeSelectionStart) { if (app.sel.timeEnd < app.sel.timeStart) {
float tmp = app.timeSelectionStart; float tmp = app.sel.timeStart;
app.timeSelectionStart = app.timeSelectionEnd; app.sel.timeStart = app.sel.timeEnd;
app.timeSelectionEnd = tmp; app.sel.timeEnd = tmp;
} }
if (app.freqSelectionEnd < app.freqSelectionStart) { if (app.sel.freqEnd < app.sel.freqStart) {
float tmp = app.freqSelectionStart; float tmp = app.sel.freqStart;
app.freqSelectionStart = app.freqSelectionEnd; app.sel.freqStart = app.sel.freqEnd;
app.freqSelectionEnd = tmp; app.sel.freqEnd = tmp;
} }
} else { } else {
// Drag too small - revert to full range // Drag too small - revert to full range
app.timeSelectionStart = 0.0f; ClearSelection();
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
} }
app.isTimeSelecting = false; app.sel.isTimeSelecting = false;
app.isFreqSelecting = false; app.sel.isFreqSelecting = false;
} }
} }
} }
@@ -982,7 +972,7 @@ int main(int argc, char* argv[])
app.scopeView.data.sampleRate = app.signal.sampleRate; app.scopeView.data.sampleRate = app.signal.sampleRate;
// Show playhead if playing // Show playhead if playing
if (app.isPlaying) { if (app.isPlaying) {
DrawScopeView(&app.scopeView, app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart)); DrawScopeView(&app.scopeView, app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart));
} else { } else {
DrawScopeView(&app.scopeView, -1.0f); DrawScopeView(&app.scopeView, -1.0f);
} }
+24 -15
View File
@@ -91,6 +91,21 @@ typedef struct {
int nextOrder; int nextOrder;
} FFTSizeCache; } FFTSizeCache;
// The time+frequency box selection and its drag/move interaction state.
// All coordinates are 0-1 normalized. A "box-select" drags out a new box;
// a "move" drags an existing box around.
typedef struct {
float timeStart, timeEnd; // selected time span
float freqStart, freqEnd; // selected frequency span
bool isTimeSelecting; // dragging out a new time span
bool isFreqSelecting; // dragging out a new frequency span
Vector2 selectStartPos; // mouse pos when a box-select began (min-drag check)
bool isDragging; // moving an existing selection box
Vector2 dragStartPos; // mouse pos when the move began
float dragTimeStart; // selection start time when the move began
float dragFreqStart; // selection freq start when the move began
} Selection;
typedef struct { typedef struct {
AudioSignal signal; AudioSignal signal;
StftResult stft; StftResult stft;
@@ -103,27 +118,14 @@ typedef struct {
float playheadT; // 0-1 normalized position in selection float playheadT; // 0-1 normalized position in selection
float playheadElapsed; // Elapsed seconds since play started float playheadElapsed; // Elapsed seconds since play started
// Time selection (0-1 normalized) // Time + frequency box selection and its drag/move interaction state.
float timeSelectionStart; Selection sel;
float timeSelectionEnd;
bool isTimeSelecting;
// Frequency selection (0-1 normalized)
float freqSelectionStart;
float freqSelectionEnd;
bool isFreqSelecting;
// Export settings // Export settings
float exportScale; float exportScale;
char exportDir[4096]; char exportDir[4096];
char exportMessage[256]; char exportMessage[256];
Vector2 selectStartPos; // For minimum drag distance check
bool isDraggingSelection; // Dragging existing selection box
Vector2 dragSelectionStartPos; // Mouse position when started dragging selection
float dragSelectionTimeStart; // Selection start time when dragging
float dragSelectionFreqStart; // Selection freq start when dragging
// Viewport/zoom controls // Viewport/zoom controls
float viewStart; // 0-1, start of visible time region float viewStart; // 0-1, start of visible time region
float viewEnd; // 0-1, end of visible time region float viewEnd; // 0-1, end of visible time region
@@ -233,6 +235,13 @@ static inline bool UiModalOpen(void)
return app.showFileBrowser || app.showAbout; return app.showFileBrowser || app.showAbout;
} }
// Reset the box selection to the full signal (the "no selection" state).
static inline void ClearSelection(void)
{
app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
}
// ============================================================================ // ============================================================================
// Keymap — single source of truth for global key bindings. // Keymap — single source of truth for global key bindings.
// The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose // The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose
+5 -8
View File
@@ -266,10 +266,10 @@ void ExportPNG(const SpectrogramApp* spa, const char* dirPath)
int imgH = spa->spectrogramImage.height; int imgH = spa->spectrogramImage.height;
// Selection region in image-pixel coordinates // Selection region in image-pixel coordinates
int selX0 = (int)(spa->timeSelectionStart * imgW); int selX0 = (int)(spa->sel.timeStart * imgW);
int selX1 = (int)(spa->timeSelectionEnd * imgW); int selX1 = (int)(spa->sel.timeEnd * imgW);
int selY0 = (int)((1.0f - spa->freqSelectionEnd) * imgH); int selY0 = (int)((1.0f - spa->sel.freqEnd) * imgH);
int selY1 = (int)((1.0f - spa->freqSelectionStart) * imgH); int selY1 = (int)((1.0f - spa->sel.freqStart) * imgH);
// Clamp to image bounds // Clamp to image bounds
selX0 = Clamp(selX0, 0, imgW); selX0 = Clamp(selX0, 0, imgW);
@@ -468,10 +468,7 @@ void DrawSidebar(void)
// Clear selection (identical to the Esc key) // Clear selection (identical to the Esc key)
Rectangle clearButton = { x, y, sidebarWidth - 10 * scale, 25 * scale }; Rectangle clearButton = { x, y, sidebarWidth - 10 * scale, 25 * scale };
if (Clicked(clearButton)) { if (Clicked(clearButton)) {
app.timeSelectionStart = 0.0f; ClearSelection();
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
} }
DrawPanelBox(clearButton, (Color){ 80, 50, 50, 255 }, RED); DrawPanelBox(clearButton, (Color){ 80, 50, 50, 255 }, RED);
DrawTextScaled("Clear Selection (ESC)", clearButton.x + 10 * scale, clearButton.y + 6 * scale, 14, WHITE); DrawTextScaled("Clear Selection (ESC)", clearButton.x + 10 * scale, clearButton.y + 6 * scale, 14, WHITE);