ui(spectrum): вертикаль несущей не перечёркивает букву полосы фильтра

Буква слайса стоит по центру полосы фильтра у самого верха, и вертикаль
несущей шла туда же — от Y=0. В SSB/CW это не мешало (несущая лежит у кромки
полосы, буква — по центру), а в модуляциях с несущей (AM/FM/DSB) центр полосы
и есть несущая, и линия шла прямо по букве.

Одна функция на оба вида — CarrierTopY (CPU) / CarrierTopYGL (GL): смотрит,
попадает ли вертикаль под глиф, и если да, отдаёт Y ПОД буквой. Решение по
факту пересечения, а не по списку «модуляций с несущей»: список пришлось бы
вести и сопровождать, а в SSB зазор и так выходит нулевым сам собой.

- Слайсы: DrawSliceFilterLinesRaw и DrawSliceFilterMarkers.
- Главный VFO: вместе с линией вниз уезжает и её «шляпка» — треугольник в CPU
  (у RawTriangleDown появился параметр верхней координаты) и метка в GL. Без
  этого они остались бы поверх буквы и зазор не помог бы.
- Полоса TX при split сверяется с той же буквой — она нарисована на RX-полосе.
- В GL буква главного флага теперь рисуется ПОСЛЕ линий, как в CPU: порядок
  наложения у путей должен совпадать.

Кегль буквы вынесен в BAND_LETTER_FONT: рисование и расчёт зазора обязаны
брать его из одного места, иначе зазор разъедется с глифом.

Метрика проверена замером (Qt6, offscreen): глиф 8 bold = 7x12 px ⇒ вертикаль
стартует с Y=14, по горизонтали зазор ±6 px от центра полосы. Правится
константами CLEAR_X/CLEAR_Y рядом с функцией.

Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На экране не проверено: нужен эфир с
AM/FM и слайсами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 19:21:08 +03:00
co-authored by Claude Opus 5
parent 13b46f0b7d
commit d0c82ade73
2 changed files with 100 additions and 22 deletions
+54 -13
View File
@@ -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);