From 7b4ea733a26eef6ca8e1229660a89993370d39e4 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Mon, 18 May 2026 22:44:32 +0300 Subject: [PATCH] Align spectrum grid and ruler to FM tuning step When FM mode is active, vertical grid lines and frequency labels snap to FM channel boundaries (FM_STEP_HZ) instead of the fixed 8-division layout. Grid and label density auto-adapt: lines thinner than 4px are thinned, labels are spaced so they never overlap (short tick shown between labeled lines). Grid cache invalidates on center-freq, step, or span changes. Co-Authored-By: Claude Sonnet 4.6 --- MainForm.pas | 6 +++ SpectrumView.pas | 127 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 18 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 496b224..c773c86 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -2797,6 +2797,10 @@ begin FSpecView.SpanHz := FSpanHz; FSpecView.Mode := FMode; FSpecView.FilterBW := FFilterBW; + if FMode = MODE_FM then + FSpecView.FMGridStepHz := FM_STEP_HZ[FFMStepIdx] + else + FSpecView.FMGridStepHz := 0; // TX overlay следует за активной TX-частотой (учитывает split). При CTUN // спектр TX будет отрисован на VFO, а не в центре дисплея. FSpecView.TXFreq := ActiveTXFreqHz; @@ -4805,6 +4809,8 @@ begin if FBtnFMSteps[Idx] <> nil then FBtnFMSteps[Idx].Active := True; if FWebServer <> nil then FWebServer.FMStepIdx := Idx; + if (FMode = MODE_FM) and (FSpecView <> nil) then + FSpecView.FMGridStepHz := FM_STEP_HZ[Idx]; end; procedure TMainForm.BtnFMStepClick(Sender: TObject); diff --git a/SpectrumView.pas b/SpectrumView.pas index 16142c4..76bdbcf 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -38,6 +38,9 @@ type FGridBitmap: TBitmap; FGridBitmapW: Integer; FGridBitmapH: Integer; + FFMGridLastCenter: Double; + FFMGridLastStepHz: Double; + FFMGridLastSpan: Double; FRulerBitmap: TBitmap; FSpecGradImg: TLazIntfImage; FSpecGradBmp: TBitmap; @@ -79,6 +82,7 @@ type FSpecRefLevel: Double; FSpecRange: Double; FSpecGridStep: Double; + FFMGridStepHz: Double; // ── TX overlay (Thetis-style: при TX данные в FSpectrumBuf приходят // от TX-анализатора @ FTXSpanHz Гц вокруг FTXFreq, и должны быть @@ -128,6 +132,7 @@ type // ── Приватные методы рендеринга ─────────────────────────────────────────── function ActiveVfoFreq: Double; function ScaleX(X, Total, Width: Integer): Integer; + procedure SetFMGridStepHz(V: Double); procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure DrawMarkerLine(C: TCanvas; W, H: Integer); // Альфа-blending вертикальной полосы поверх FSpectrumBitmap (pf32bit, BGRA). @@ -164,9 +169,10 @@ type property WfAGCOffset: Double read FWfAGCOffset write FWfAGCOffset; // ── Отображение ─────────────────────────────────────────────────────────── - property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel; - property SpecRange: Double read FSpecRange write FSpecRange; - property SpecGridStep: Double read FSpecGridStep write FSpecGridStep; + property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel; + property SpecRange: Double read FSpecRange write FSpecRange; + property SpecGridStep: Double read FSpecGridStep write FSpecGridStep; + property FMGridStepHz: Double read FFMGridStepHz write SetFMGridStepHz; // ── TX overlay ──────────────────────────────────────────────────────────── property TXMode: Boolean read FTXMode write FTXMode; @@ -404,6 +410,8 @@ begin FWfIntfImg := nil; FWfBitmapW := 0; FWfBitmapH := 0; FGridBitmapW := 0; FGridBitmapH := 0; + FFMGridLastCenter := -1.0; FFMGridLastStepHz := -1.0; FFMGridLastSpan := -1.0; + FFMGridStepHz := 0.0; FSpPtsLen := 0; FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; // Водопад AGC defaults @@ -467,6 +475,16 @@ begin else Result := Round(X / Total * Width); end; +procedure TSpectrumView.SetFMGridStepHz(V: Double); +begin + if Abs(FFMGridStepHz - V) > 0.5 then + begin + FFMGridStepHz := V; + InvalidateGridCache; + InvalidateRulerCache; + end; +end; + procedure TSpectrumView.DrawMarkerLine(C: TCanvas; W, H: Integer); var MX: Integer; MarkerFreq: Double; MarkerLbl: string; begin @@ -734,6 +752,8 @@ var S0, S1: Integer; InvRange: Double; LabelBandW, SrcCount: Integer; + GridFreqS, GridLine, pixPerStep: Double; + N, gridMult: Integer; begin if FSpectrumBitmap = nil then Exit; W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height; @@ -744,9 +764,16 @@ begin InvRange := 1.0 / (DBmax - DBmin); // ── 1. Фон+сетка из кэша ────────────────────────────────────────────────── - if (FGridBitmapW <> W) or (FGridBitmapH <> H) then + if (FGridBitmapW <> W) or (FGridBitmapH <> H) or + ((FFMGridStepHz > 0) and + ((Abs(FCenterFreq - FFMGridLastCenter) > 0.5) or + (FFMGridStepHz <> FFMGridLastStepHz) or + (Abs(FSpanHz - FFMGridLastSpan) > 1.0))) then begin FGridBitmapW := W; FGridBitmapH := H; + FFMGridLastCenter := FCenterFreq; + FFMGridLastStepHz := FFMGridStepHz; + FFMGridLastSpan := FSpanHz; FGridBitmap.SetSize(W, H); GC := FGridBitmap.Canvas; PaintVerticalGradient(GC, W, H, FTheme.SpecGradTop, FTheme.SpecGradBot); @@ -767,11 +794,41 @@ begin GC.TextOut(1, Yp - 9, Format('%4.0f', [dB])); dB := dB - FSpecGridStep; end; - for i := 0 to 8 do + if FFMGridStepHz > 0 then begin - GX := ScaleX(i, 8, W); - GC.Pen.Color := FTheme.SpecGrid; - GC.MoveTo(GX, 0); GC.LineTo(GX, H); + GridFreqS := FCenterFreq - FSpanHz / 2; + pixPerStep := W * FFMGridStepHz / FSpanHz; + if pixPerStep >= 1.0 then + gridMult := Max(1, Ceil(4.0 / pixPerStep)) + else + gridMult := 0; + if gridMult > 0 then + begin + N := Ceil(GridFreqS / FFMGridStepHz); + GridLine := N * FFMGridStepHz; + while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do + begin + if (N mod gridMult) = 0 then + begin + GX := Round((GridLine - GridFreqS) / FSpanHz * W); + if (GX >= 0) and (GX < W) then + begin + GC.Pen.Color := FTheme.SpecGrid; + GC.MoveTo(GX, 0); GC.LineTo(GX, H); + end; + end; + Inc(N); + GridLine := GridLine + FFMGridStepHz; + end; + end; + end else + begin + for i := 0 to 8 do + begin + GX := ScaleX(i, 8, W); + GC.Pen.Color := FTheme.SpecGrid; + GC.MoveTo(GX, 0); GC.LineTo(GX, H); + end; end; end; LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020); @@ -1074,8 +1131,9 @@ end; procedure TSpectrumView.DrawRuler; var C: TCanvas; W, H, i, X: Integer; - FreqStart, FreqHz: Double; VfoX: Integer; + FreqStart, FreqHz, GridLine, pixPerStep: Double; VfoX: Integer; Lbl: string; TW: Integer; + N, labelMult: Integer; begin if FPbRuler = nil then Exit; W := FPbRuler.Width; H := FPbRuler.Height; @@ -1094,16 +1152,49 @@ begin C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Brush.Style := bsClear; FreqStart := FCenterFreq - FSpanHz / 2; - for i := 0 to 8 do + if FFMGridStepHz > 0 then begin - X := ScaleX(i, 8, W); - C.Pen.Color := FTheme.RulerBorder; - C.MoveTo(X, 0); C.LineTo(X, H div 2); - FreqHz := FreqStart + i * FSpanHz / 8; - Lbl := Format('%.3f', [FreqHz / 1e6]); - TW := C.TextWidth(Lbl); - C.Font.Color := FTheme.RulerText; - C.TextOut(X - TW div 2, H div 2 - 1, Lbl); + pixPerStep := W * FFMGridStepHz / FSpanHz; + if pixPerStep >= 1.0 then + labelMult := Max(1, Ceil((C.TextWidth('000.000') + 6) / pixPerStep)) + else + labelMult := MaxInt; + N := Ceil(FreqStart / FFMGridStepHz); + GridLine := N * FFMGridStepHz; + while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do + begin + X := Round((GridLine - FreqStart) / FSpanHz * W); + if (X >= 0) and (X < W) then + begin + C.Pen.Color := FTheme.RulerBorder; + if (N mod labelMult) = 0 then + begin + C.MoveTo(X, 0); C.LineTo(X, H div 2); + Lbl := Format('%.3f', [GridLine / 1e6]); + TW := C.TextWidth(Lbl); + C.Font.Color := FTheme.RulerText; + C.TextOut(X - TW div 2, H div 2 - 1, Lbl); + end else + begin + C.MoveTo(X, 0); C.LineTo(X, H div 4); + end; + end; + Inc(N); + GridLine := GridLine + FFMGridStepHz; + end; + end else + begin + for i := 0 to 8 do + begin + X := ScaleX(i, 8, W); + C.Pen.Color := FTheme.RulerBorder; + C.MoveTo(X, 0); C.LineTo(X, H div 2); + FreqHz := FreqStart + i * FSpanHz / 8; + Lbl := Format('%.3f', [FreqHz / 1e6]); + TW := C.TextWidth(Lbl); + C.Font.Color := FTheme.RulerText; + C.TextOut(X - TW div 2, H div 2 - 1, Lbl); + end; end; VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W); if (VfoX >= 0) and (VfoX < W) then