diff --git a/SpectrumView.pas b/SpectrumView.pas index 2338dca..83d9264 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -160,7 +160,7 @@ type procedure RawHLine(Y, X1, X2: Integer; Color: TColor; OnPx: Integer = 0; OffPx: Integer = 0); procedure RawFillRect(X1, Y1, X2, Y2: Integer; Color: TColor); - procedure RawTriangleDown(CX, HalfW, Hgt: Integer; Color: TColor); + procedure RawTriangleDown(CX, Top, HalfW, Hgt: Integer; Color: TColor); procedure RawCurve(W: Integer; Color: TColor); function GetTextMask(const S: string; FontSize: Integer; Bold: Boolean): TBitmap; @@ -172,6 +172,8 @@ type procedure DrawSliceFilterBands(W, H: Integer); procedure DrawSliceFilterLinesRaw(W, H: Integer); procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor); + // Y, с которого начинать вертикаль несущей, чтобы не перечеркнуть букву. + function CarrierTopY(X1, X2, CarrierX: Integer; L: Char): Integer; procedure DrawBeaconMarkersRaw(W, H: Integer); // Штрихи DX-спотов ниже полосы подписей. Раскладка уже посчитана оверлеем // при пересборке кэша — здесь только N вертикальных линий. @@ -366,6 +368,9 @@ const // Полоса фильтра передающего тракта (главный VFO при TX / передающий слайс). // TColor = $00BBGGRR, младший байт = R (как в BlendBand/RawPack). CLR_TX_BAND = TColor($003030E0); + // Кегль буквы слайса на полосе фильтра. Один на рисование буквы и на расчёт + // зазора под неё (CarrierTopY) — иначе зазор разъедется с глифом. + BAND_LETTER_FONT = 8; // Упаковка TColor в пиксель кадра (непрозрачный). Раскладка байтов как в // BlendBand: non-Darwin = BGRA, Darwin = ARGB. @@ -955,14 +960,16 @@ begin end; // Треугольник остриём вниз (курсор VFO): вершина основания на Y=0. -procedure TSpectrumView.RawTriangleDown(CX, HalfW, Hgt: Integer; Color: TColor); +procedure TSpectrumView.RawTriangleDown(CX, Top, HalfW, Hgt: Integer; Color: TColor); +// Top — верх треугольника: «голова» несущей уезжает вниз, когда её иначе +// накрыла бы буква слайса (см. CarrierTopY). var Y, Half: Integer; begin for Y := 0 to Hgt do begin Half := Round(HalfW * (Hgt - Y) / Hgt); - RawHLine(Y, CX - Half, CX + Half + 1, Color); + RawHLine(Top + Y, CX - Half, CX + Half + 1, Color); end; end; @@ -1236,7 +1243,9 @@ begin CalcSliceBandX(O, W, SX1, SX2, SVfoX); RawVLine(SX1, 0, H - 1, Clr); RawVLine(SX2, 0, H - 1, Clr); - RawVLine(SVfoX, 0, H - 13, Clr, 2); // несущая (жирнее) + // Несущая (жирнее). В AM/FM/DSB она приходится ровно на букву — начинаем + // под ней, чтобы буква читалась (в SSB/CW несущая у кромки, зазор = 0). + RawVLine(SVfoX, CarrierTopY(SX1, SX2, SVfoX, O.SliceLetter), H - 13, Clr, 2); // Буква слайса внутри полосы фильтра сверху — привязка полосы к флагу, // цвет = цвет слайса (совпадает с бейджем во флаге). DrawBandLetterRaw(SX1, SX2, O.SliceLetter, SliceColor(O.SliceLetter)); @@ -1251,7 +1260,30 @@ var begin if (L < 'A') or (L > 'Z') then Exit; cx := (X1 + X2) div 2; - RawText(cx - RawTextWidth(L, 8, True) div 2, 0, L, Clr, 8, True, clNone); + RawText(cx - RawTextWidth(L, BAND_LETTER_FONT, True) div 2, 0, L, Clr, + BAND_LETTER_FONT, True, clNone); +end; + +function TSpectrumView.CarrierTopY(X1, X2, CarrierX: Integer; L: Char): Integer; +// Вертикаль несущей идёт от самого верха полосы — и в модуляциях с несущей +// (AM/FM/DSB) она приходится ровно на букву слайса: и буква, и несущая стоят по +// центру полосы. В SSB/CW несущая лежит у кромки, пересечения нет, поэтому +// раньше это в глаза не бросалось. Здесь считаем, накрывает ли вертикаль букву, +// и если да — начинаем её ПОД буквой. L = #0 (или полоса без буквы) → 0. +const + CLEAR_X = 3; // запас по бокам от глифа + CLEAR_Y = 2; // зазор между буквой и началом вертикали +var + M: TBitmap; + cx: Integer; +begin + Result := 0; + if (L < 'A') or (L > 'Z') or (X2 <= X1) then Exit; + M := GetTextMask(L, BAND_LETTER_FONT, True); + if M = nil then Exit; + cx := (X1 + X2) div 2; + if Abs(CarrierX - cx) <= M.Width div 2 + CLEAR_X then + Result := M.Height + CLEAR_Y; end; procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer); @@ -1569,7 +1601,8 @@ var DBmin, DBmax, dB: Double; VfoX, X1, X2: Integer; TXVfoX, TXX1, TXX2: Integer; - AGCy, AGCHangY: Integer; + AGCy, AGCHangY, CarrY: Integer; + MainLetter: Char; SrcF, Frac, dBv: Double; S0, S1: Integer; InvRange: Double; @@ -1768,11 +1801,15 @@ begin // Буква главного флага (A) на его полосе — только когда есть слайсы, - // иначе одиночный приём не засоряем. + // иначе одиночный приём не засоряем. Букву запоминаем: под неё ниже + // подстраиваются вертикаль несущей и её треугольник. + MainLetter := #0; if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and (FActiveVfo = 0) and Assigned(FVfoOverlay) then - DrawBandLetterRaw(X1, X2, FVfoOverlay.SliceLetter, - SliceColor(FVfoOverlay.SliceLetter)); + begin + MainLetter := FVfoOverlay.SliceLetter; + DrawBandLetterRaw(X1, X2, MainLetter, SliceColor(MainLetter)); + end; // Края/несущие/буквы слайсов (полосы уже нарисованы выше). DrawSliceFilterLinesRaw(W, H); @@ -1811,14 +1848,18 @@ begin // Края фильтра + VFO RawVLine(X1, 0, H - 1, FTheme.SpecFilterEdge); RawVLine(X2, 0, H - 1, FTheme.SpecFilterEdge); - RawVLine(VfoX, 0, H - 13, FTheme.SpecVfoCursor, 2); - RawTriangleDown(VfoX, 5, 8, FTheme.SpecVfoCursor); + // Несущая и её треугольник — под буквой полосы, если она там есть. + CarrY := CarrierTopY(X1, X2, VfoX, MainLetter); + RawVLine(VfoX, CarrY, H - 13, FTheme.SpecVfoCursor, 2); + RawTriangleDown(VfoX, CarrY, 5, 8, FTheme.SpecVfoCursor); if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then begin RawVLine(TXX1, 0, H - 1, TColor($002030E0)); RawVLine(TXX2, 0, H - 1, TColor($002030E0)); - RawVLine(TXVfoX, 0, H - 13, TColor($002030E0), 2); - RawTriangleDown(TXVfoX, 5, 8, TColor($002030E0)); + // Буква нарисована на RX-полосе (X1..X2) — с ней и сверяемся. + CarrY := CarrierTopY(X1, X2, TXVfoX, MainLetter); + RawVLine(TXVfoX, CarrY, H - 13, TColor($002030E0), 2); + RawTriangleDown(TXVfoX, CarrY, 5, 8, TColor($002030E0)); end; if FMarkerActive then DrawMarkerLineRaw(W, H); diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index 16775b5..4d1417d 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -106,6 +106,9 @@ type procedure DrawSliceOverlays(W, H: Integer); procedure DrawSliceFilterMarkers(W, H: Integer); procedure DrawBandLetterGL(X1, X2: Integer; L: Char); + // Y начала вертикали несущей, чтобы она не легла на букву (двойник + // CPU-CarrierTopY; метрику берём из той же текстуры буквы). + function CarrierTopYGL(X1, X2, CarrierX: Integer; L: Char): Integer; function SliceTexIndex(O: TVfoOverlay): Integer; // индекс в FSliceTex, -1 если нет function FormatFreqGL(Hz: Double): string; function ActiveVfoFrequency: Double; @@ -592,7 +595,8 @@ end; procedure TSpectrumViewOpenGL.DrawFilterAndCursors(W, H: Integer); var - X1, X2, VfoX, TXX1, TXX2, TXVfoX: Integer; + X1, X2, VfoX, TXX1, TXX2, TXVfoX, CarrY: Integer; + MainLetter: Char; begin TXX1 := 0; TXX2 := 0; @@ -620,22 +624,30 @@ begin else if X2 > X1 then DrawRect(X1, 0, X2, H, FTheme.SpecFilter, 1); + // Буква главного флага (A) на его полосе — только когда есть слайсы. Саму + // букву рисуем ниже, но знать о ней надо здесь: под неё уходит начало + // вертикали несущей и её «шляпка». + MainLetter := #0; + if (SliceOverlayCount > 0) and (FActiveVfo = 0) and Assigned(FVfoOverlay) then + MainLetter := FVfoOverlay.SliceLetter; + DrawLine(X1, 0, X1, H, FTheme.SpecFilterEdge, 1, False); DrawLine(X2, 0, X2, H, FTheme.SpecFilterEdge, 1, False); - DrawLine(VfoX, 0, VfoX, H - 12, FTheme.SpecVfoCursor, 2, False); - DrawRect(VfoX - 5, 0, VfoX + 5, 8, FTheme.SpecVfoCursor, 1); + CarrY := CarrierTopYGL(X1, X2, VfoX, MainLetter); + DrawLine(VfoX, CarrY, VfoX, H - 12, FTheme.SpecVfoCursor, 2, False); + DrawRect(VfoX - 5, CarrY, VfoX + 5, CarrY + 8, FTheme.SpecVfoCursor, 1); if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then begin DrawLine(TXX1, 0, TXX1, H, TColor($002030E0), 1, False); DrawLine(TXX2, 0, TXX2, H, TColor($002030E0), 1, False); - DrawLine(TXVfoX, 0, TXVfoX, H - 12, TColor($002030E0), 2, False); - DrawRect(TXVfoX - 5, 0, TXVfoX + 5, 8, TColor($002030E0), 1); + // Буква нарисована на RX-полосе (X1..X2) — с ней и сверяемся. + CarrY := CarrierTopYGL(X1, X2, TXVfoX, MainLetter); + DrawLine(TXVfoX, CarrY, TXVfoX, H - 12, TColor($002030E0), 2, False); + DrawRect(TXVfoX - 5, CarrY, TXVfoX + 5, CarrY + 8, TColor($002030E0), 1); end; - // Буква главного флага (A) на его полосе — только когда есть слайсы. - if (SliceOverlayCount > 0) and (FActiveVfo = 0) and Assigned(FVfoOverlay) then - DrawBandLetterGL(X1, X2, FVfoOverlay.SliceLetter); + if MainLetter <> #0 then DrawBandLetterGL(X1, X2, MainLetter); DrawSliceFilterMarkers(W, H); end; @@ -660,7 +672,9 @@ begin DrawRect(SX1, 0, SX2, H, Clr, IfThen(O.TxActive, 100, 60) / 255); DrawLine(SX1, 0, SX1, H, Clr, 1, False); DrawLine(SX2, 0, SX2, H, Clr, 1, False); - DrawLine(SVfoX, 0, SVfoX, H - 12, Clr, 2, False); + // Несущая — под буквой, если она приходится на неё (AM/FM/DSB). + DrawLine(SVfoX, CarrierTopYGL(SX1, SX2, SVfoX, O.SliceLetter), + SVfoX, H - 12, Clr, 2, False); DrawBandLetterGL(SX1, SX2, O.SliceLetter); end; end; @@ -679,6 +693,29 @@ begin DrawTexture(FLetterTex[idx], cx - FLetterTex[idx].W div 2, 0); end; +function TSpectrumViewOpenGL.CarrierTopYGL(X1, X2, CarrierX: Integer; + L: Char): Integer; +// То же правило, что в CPU-пути: в модуляциях с несущей (AM/FM/DSB) вертикаль +// приходится ровно на букву — начинаем её ПОД буквой; в SSB/CW несущая у кромки +// полосы, пересечения нет и зазор нулевой. Текстуру буквы при необходимости +// заливаем здесь же: она нужна нам раньше, чем до неё дойдёт DrawBandLetterGL +// (порядок вызовов в кадре — линии, потом буквы). +const + CLEAR_X = 3; + CLEAR_Y = 2; +var + idx: Integer; +begin + Result := 0; + if (L < 'A') or (L > 'H') or (X2 <= X1) then Exit; + idx := Ord(L) - Ord('A'); + if FLetterTex[idx].Tex = 0 then + UploadText(FLetterTex[idx], L, SliceColor(L), 8, True); + if FLetterTex[idx].H <= 0 then Exit; + if Abs(CarrierX - (X1 + X2) div 2) <= FLetterTex[idx].W div 2 + CLEAR_X then + Result := FLetterTex[idx].H + CLEAR_Y; +end; + procedure TSpectrumViewOpenGL.DrawAGCLines(W, H: Integer; DBmax, InvRange: Double); var