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 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:44:32 +03:00
co-authored by Claude Sonnet 4.6
parent a59501f2c4
commit 7b4ea733a2
2 changed files with 115 additions and 18 deletions
+109 -18
View File
@@ -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