mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
feat(slices): non-overlapping flag packing + per-slice colored letters
Flags (A + B+) now lay out in one pass: each sits beside its own filter band as before, but a left neighbour pushes the next flag right so they never cross (forward pack + backward pull-in on right-edge overflow). LayoutFlags replaces the per-flag positioning; PositionVfoOverlay/ PositionSliceFlags are thin wrappers. Identity is shown by a letter drawn at the top of each filter band in the spectrum (both CPU + GL renderers), so a flag pushed away from its band is still traceable. Letters and the flag letter-badge share one per-slice palette (SliceColor); F is violet, not red, to avoid clashing with the TX indicator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+99
-97
@@ -523,6 +523,7 @@ type
|
||||
// -------------------------------------------------------------------------
|
||||
procedure BtnSettingsClick(Sender: TObject);
|
||||
procedure PositionVfoOverlay;
|
||||
procedure LayoutFlags; // единый packing всех флагов (A+B+): не наезжают друг на друга
|
||||
procedure UpdateVfoOverlayVisibility; // флаг A виден: панель скрыта ИЛИ есть слайсы B+
|
||||
procedure OnModeFilterSelect(Mode: Integer; FilterBW: Integer);
|
||||
procedure OnVfoOverlayDSPChange(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean);
|
||||
@@ -5145,55 +5146,116 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PositionVfoOverlay;
|
||||
// Позиционирование всех флагов (главный A + слайсы B+) единым проходом.
|
||||
// Идеал каждого флага — рядом со СВОЕЙ полосой фильтра (справа, флип влево
|
||||
// если не влезает), как раньше. Но флаги не наезжают друг на друга: сосед
|
||||
// слева «толкает» правого, тот останавливается и не пересекает. Какому флагу
|
||||
// принадлежит уехавшая полоса — видно по букве на самой полосе (рендерер).
|
||||
procedure TMainForm.LayoutFlags;
|
||||
const
|
||||
FLAG_TOP = 35; // все флаги на одном уровне
|
||||
GAPX = 4; // минимальный зазор между соседними флагами
|
||||
var
|
||||
VfoX, FilterStartX, FilterEndX, OW, NewLeft, NewTop: Integer;
|
||||
Ovls: array of TVfoOverlay;
|
||||
Ideal, OWs, Lefts: array of Integer;
|
||||
n, i, j, W: Integer;
|
||||
VC, VS: Double;
|
||||
S: TCtrlSlice;
|
||||
LoHz, HiHz: Double;
|
||||
ViewCenter, ViewSpan: Double;
|
||||
begin
|
||||
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
||||
OW := FVfoOverlay.Width;
|
||||
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||
TmpO: TVfoOverlay;
|
||||
TmpI: Integer;
|
||||
Moved: Boolean;
|
||||
|
||||
if (PbSpectrum.Width > 0) and (ViewSpan > 0) then
|
||||
// Идеальный Left рядом с полосой фильтра [loHz..hiHz] вокруг несущей Carrier.
|
||||
function IdealLeftFor(Carrier, loHz, hiHz: Double; OW: Integer): Integer;
|
||||
var vfoX, sx1, sx2, nl: Integer;
|
||||
begin
|
||||
VfoX := Round((FController.FVfoA - ViewCenter + ViewSpan / 2) / ViewSpan * PbSpectrum.Width)
|
||||
end
|
||||
else
|
||||
VfoX := PbSpectrum.Width div 2;
|
||||
vfoX := Round((Carrier - VC + VS / 2) / VS * W);
|
||||
sx1 := Max(0, Min(W - 1, vfoX + Round(loHz / VS * W)));
|
||||
sx2 := Max(0, Min(W - 1, vfoX + Round(hiHz / VS * W)));
|
||||
nl := sx2 + 6; // по умолчанию справа от полосы
|
||||
if nl + OW > W - 4 then nl := sx1 - 6 - OW; // не влезает → слева
|
||||
Result := nl;
|
||||
end;
|
||||
|
||||
// Кромки полосы фильтра относительно несущей (для выбора стороны накладки).
|
||||
begin
|
||||
if PbSpectrum = nil then Exit;
|
||||
W := PbSpectrum.Width;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
if (W <= 0) or (VS <= 0) then Exit;
|
||||
|
||||
// 1. Собрать видимые флаги + их идеальный Left рядом со своей полосой.
|
||||
n := 0;
|
||||
SetLength(Ovls, 1 + Length(FSliceFlags));
|
||||
SetLength(Ideal, 1 + Length(FSliceFlags));
|
||||
SetLength(OWs, 1 + Length(FSliceFlags));
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||
begin
|
||||
case FController.FMode of
|
||||
MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
|
||||
MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; end;
|
||||
else
|
||||
begin LoHz := -FController.FFilterBW / 2; HiHz := FController.FFilterBW / 2; end;
|
||||
else begin LoHz := -FController.FFilterBW / 2; HiHz := FController.FFilterBW / 2; end;
|
||||
end;
|
||||
if (PbSpectrum.Width > 0) and (ViewSpan > 0) then
|
||||
begin
|
||||
FilterStartX := VfoX + Round(LoHz / ViewSpan * PbSpectrum.Width);
|
||||
FilterEndX := VfoX + Round(HiHz / ViewSpan * PbSpectrum.Width);
|
||||
end
|
||||
else
|
||||
begin
|
||||
FilterStartX := VfoX;
|
||||
FilterEndX := VfoX;
|
||||
Ovls[n] := FVfoOverlay;
|
||||
OWs[n] := FVfoOverlay.Width;
|
||||
Ideal[n] := IdealLeftFor(FController.FVfoA, LoHz, HiHz, OWs[n]);
|
||||
Inc(n);
|
||||
end;
|
||||
FilterStartX := Max(0, Min(PbSpectrum.Width - 1, FilterStartX));
|
||||
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
begin
|
||||
if (FSliceFlags[i] = nil) or (not FSliceFlags[i].Visible) then Continue;
|
||||
if not FController.GetSlice(FSliceFlags[i].SliceId, S) then Continue;
|
||||
Ovls[n] := FSliceFlags[i];
|
||||
OWs[n] := FSliceFlags[i].Width;
|
||||
Ideal[n] := IdealLeftFor(S.TargetHz, S.FilterLo, S.FilterHi, OWs[n]);
|
||||
Inc(n);
|
||||
end;
|
||||
if n = 0 then Exit;
|
||||
|
||||
// По умолчанию справа от полосы фильтра; если не влезает — слева от неё.
|
||||
NewLeft := FilterEndX + 6;
|
||||
if NewLeft + OW > PbSpectrum.Width - 4 then
|
||||
NewLeft := FilterStartX - 6 - OW;
|
||||
NewLeft := Max(4, Min(PbSpectrum.Width - OW - 4, NewLeft));
|
||||
NewTop := 35;
|
||||
if (FVfoOverlay.Left <> NewLeft) or (FVfoOverlay.Top <> NewTop) then
|
||||
// 2. Сортировка по идеальному Left (≈ по частоте) — пузырьком, n мал.
|
||||
for i := 0 to n - 2 do
|
||||
for j := 0 to n - 2 - i do
|
||||
if Ideal[j] > Ideal[j + 1] then
|
||||
begin
|
||||
FVfoOverlay.Left := NewLeft;
|
||||
FVfoOverlay.Top := NewTop;
|
||||
OnVfoOverlayInvalidate(FVfoOverlay);
|
||||
TmpI := Ideal[j]; Ideal[j] := Ideal[j + 1]; Ideal[j + 1] := TmpI;
|
||||
TmpI := OWs[j]; OWs[j] := OWs[j + 1]; OWs[j + 1] := TmpI;
|
||||
TmpO := Ovls[j]; Ovls[j] := Ovls[j + 1]; Ovls[j + 1] := TmpO;
|
||||
end;
|
||||
|
||||
// 3. Вперёд-проход: сосед слева толкает правого (тот не пересекает).
|
||||
SetLength(Lefts, n);
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
Lefts[i] := Ideal[i];
|
||||
if (i > 0) and (Lefts[i] < Lefts[i - 1] + OWs[i - 1] + GAPX) then
|
||||
Lefts[i] := Lefts[i - 1] + OWs[i - 1] + GAPX;
|
||||
end;
|
||||
// 4. Если ряд вылез за правый край — обратный проход поджимает влево.
|
||||
if Lefts[n - 1] + OWs[n - 1] > W - 4 then
|
||||
for i := n - 1 downto 0 do
|
||||
if i = n - 1 then
|
||||
Lefts[i] := Min(Lefts[i], W - 4 - OWs[i])
|
||||
else
|
||||
Lefts[i] := Min(Lefts[i], Lefts[i + 1] - GAPX - OWs[i]);
|
||||
|
||||
// 5. Применить (зажать в поле), инвалидировать при реальном сдвиге.
|
||||
Moved := False;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
Lefts[i] := Max(4, Lefts[i]);
|
||||
if (Ovls[i].Left <> Lefts[i]) or (Ovls[i].Top <> FLAG_TOP) then
|
||||
begin
|
||||
Ovls[i].Left := Lefts[i];
|
||||
Ovls[i].Top := FLAG_TOP;
|
||||
Moved := True;
|
||||
end;
|
||||
end;
|
||||
if Moved then OnVfoOverlayInvalidate(nil);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PositionVfoOverlay;
|
||||
begin
|
||||
LayoutFlags;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PbSpectrumDblClick(Sender: TObject);
|
||||
@@ -5484,68 +5546,8 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.PositionSliceFlags;
|
||||
var
|
||||
i, VfoX, OW, NewLeft, NewTop, FilterStartX, FilterEndX, BWs: Integer;
|
||||
VC, VS, LoHz, HiHz: Double;
|
||||
S: TCtrlSlice;
|
||||
O: TVfoOverlay;
|
||||
Moved: Boolean;
|
||||
begin
|
||||
if (Length(FSliceFlags) = 0) or (PbSpectrum = nil) then Exit;
|
||||
Moved := False;
|
||||
FController.GetViewWindow(VC, VS);
|
||||
for i := 0 to High(FSliceFlags) do
|
||||
begin
|
||||
O := FSliceFlags[i];
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
if not FController.GetSlice(O.SliceId, S) then Continue;
|
||||
OW := O.Width;
|
||||
if (PbSpectrum.Width > 0) and (VS > 0) then
|
||||
VfoX := Round((S.TargetHz - VC + VS / 2) / VS * PbSpectrum.Width)
|
||||
else
|
||||
VfoX := PbSpectrum.Width div 2;
|
||||
// Кромки полосы фильтра слайса относительно несущей — как у главного флага
|
||||
// (PositionVfoOverlay): флаг ставится СБОКУ от полосы, а не от несущей.
|
||||
case S.Mode of
|
||||
MODE_LSB: BWs := -S.FilterLo;
|
||||
MODE_USB: BWs := S.FilterHi;
|
||||
else BWs := S.FilterHi - S.FilterLo;
|
||||
end;
|
||||
case S.Mode of
|
||||
MODE_LSB: begin LoHz := -BWs; HiHz := -100; end;
|
||||
MODE_USB: begin LoHz := 100; HiHz := BWs; end;
|
||||
else begin LoHz := -BWs / 2; HiHz := BWs / 2; end;
|
||||
end;
|
||||
if (PbSpectrum.Width > 0) and (VS > 0) then
|
||||
begin
|
||||
FilterStartX := VfoX + Round(LoHz / VS * PbSpectrum.Width);
|
||||
FilterEndX := VfoX + Round(HiHz / VS * PbSpectrum.Width);
|
||||
end
|
||||
else
|
||||
begin
|
||||
FilterStartX := VfoX;
|
||||
FilterEndX := VfoX;
|
||||
end;
|
||||
FilterStartX := Max(0, Min(PbSpectrum.Width - 1, FilterStartX));
|
||||
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
||||
// По умолчанию справа от полосы; если не влезает — слева от неё.
|
||||
NewLeft := FilterEndX + 6;
|
||||
if NewLeft + OW > PbSpectrum.Width - 4 then NewLeft := FilterStartX - 6 - OW;
|
||||
NewLeft := Max(4, Min(PbSpectrum.Width - OW - 4, NewLeft));
|
||||
NewTop := 35; // все флаги на одном уровне
|
||||
if (O.Left <> NewLeft) or (O.Top <> NewTop) then
|
||||
begin
|
||||
O.Left := NewLeft;
|
||||
O.Top := NewTop;
|
||||
Moved := True;
|
||||
end;
|
||||
end;
|
||||
// Инвалидируем спектр только если флаг реально сдвинулся (ретюн/зум/пан).
|
||||
// Раньше звали безусловно каждый тик → форс полной перерисовки на пустом месте.
|
||||
if Moved then
|
||||
begin
|
||||
OnVfoOverlayInvalidate(nil);
|
||||
end;
|
||||
LayoutFlags;
|
||||
end;
|
||||
|
||||
// Заливает во флаг слайса его текущее состояние из контроллера.
|
||||
|
||||
@@ -110,6 +110,7 @@ type
|
||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||
procedure DrawSliceFilterMarkers(C: TCanvas; W, H: Integer);
|
||||
procedure DrawBandLetter(C: TCanvas; X1, X2: Integer; L: Char; Clr: TColor);
|
||||
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
procedure CopyGridToSpectrum(W, H: Integer);
|
||||
@@ -640,9 +641,29 @@ begin
|
||||
C.Pen.Width := 2;
|
||||
C.MoveTo(SVfoX, 0); C.LineTo(SVfoX, H - 12);
|
||||
C.Pen.Width := 1;
|
||||
// Буква слайса внутри полосы фильтра сверху — привязка полосы к флагу,
|
||||
// цвет = цвет слайса (совпадает с бейджем во флаге).
|
||||
DrawBandLetter(C, SX1, SX2, O.SliceLetter, SliceColor(O.SliceLetter));
|
||||
end;
|
||||
end;
|
||||
|
||||
// Буква слайса (A/B/…) по центру полосы фильтра, у самого верха — чтобы
|
||||
// понять, какому флагу принадлежит полоса, даже когда флаги стоят рядом.
|
||||
procedure TSpectrumView.DrawBandLetter(C: TCanvas; X1, X2: Integer;
|
||||
L: Char; Clr: TColor);
|
||||
var
|
||||
cx: Integer;
|
||||
begin
|
||||
if (L < 'A') or (L > 'Z') then Exit;
|
||||
cx := (X1 + X2) div 2;
|
||||
C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := [fsBold];
|
||||
C.Font.Color := Clr;
|
||||
C.Brush.Style := bsClear;
|
||||
C.TextOut(cx - C.TextWidth(L) div 2, 0, L);
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Font.Style := [];
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
|
||||
var
|
||||
Y: Integer;
|
||||
@@ -979,6 +1000,12 @@ begin
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
|
||||
// Буква главного флага (A) на его полосе — только когда есть слайсы,
|
||||
// иначе одиночный приём не засоряем.
|
||||
if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and
|
||||
(FActiveVfo = 0) and Assigned(FVfoOverlay) then
|
||||
DrawBandLetter(C, X1, X2, FVfoOverlay.SliceLetter, SliceColor(FVfoOverlay.SliceLetter));
|
||||
|
||||
// Полосы фильтра слайсов (B+) — под кривой спектра, как в GL-вьюхе.
|
||||
DrawSliceFilterMarkers(C, W, H);
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ type
|
||||
Tex: TGLTextureCache;
|
||||
LastX, LastY, LastW, LastH: Integer;
|
||||
end;
|
||||
// Кэш текстур букв A..H для меток на полосе фильтра (грузятся лениво).
|
||||
FLetterTex: array[0..7] of TGLTextureCache;
|
||||
FLastAGCText: string;
|
||||
FLastAGCY: Integer;
|
||||
FLastAGCHangY: Integer;
|
||||
@@ -91,6 +93,7 @@ type
|
||||
procedure DrawCachedOverlays(W, H: Integer);
|
||||
procedure DrawSliceOverlays(W, H: Integer);
|
||||
procedure DrawSliceFilterMarkers(W, H: Integer);
|
||||
procedure DrawBandLetterGL(X1, X2: Integer; L: Char);
|
||||
function SliceTexIndex(O: TVfoOverlay): Integer; // индекс в FSliceTex, -1 если нет
|
||||
function FormatFreqGL(Hz: Double): string;
|
||||
function ActiveVfoFrequency: Double;
|
||||
@@ -161,6 +164,7 @@ begin
|
||||
DeleteTexture(FVfoOverlayTex);
|
||||
DeleteTexture(FBandOverlayTex);
|
||||
for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex);
|
||||
for i := 0 to High(FLetterTex) do DeleteTexture(FLetterTex[i]);
|
||||
end;
|
||||
SetLength(FSliceTex, 0);
|
||||
inherited Destroy;
|
||||
@@ -557,6 +561,10 @@ begin
|
||||
DrawRect(TXVfoX - 5, 0, TXVfoX + 5, 8, TColor($002030E0), 1);
|
||||
end;
|
||||
|
||||
// Буква главного флага (A) на его полосе — только когда есть слайсы.
|
||||
if (SliceOverlayCount > 0) and (FActiveVfo = 0) and Assigned(FVfoOverlay) then
|
||||
DrawBandLetterGL(X1, X2, FVfoOverlay.SliceLetter);
|
||||
|
||||
DrawSliceFilterMarkers(W, H);
|
||||
end;
|
||||
|
||||
@@ -578,9 +586,24 @@ begin
|
||||
DrawLine(SX1, 0, SX1, H, SLICE_CLR, 1, False);
|
||||
DrawLine(SX2, 0, SX2, H, SLICE_CLR, 1, False);
|
||||
DrawLine(SVfoX, 0, SVfoX, H - 12, SLICE_CLR, 2, False);
|
||||
DrawBandLetterGL(SX1, SX2, O.SliceLetter);
|
||||
end;
|
||||
end;
|
||||
|
||||
// Буква слайса по центру полосы фильтра сверху (аналог CPU DrawBandLetter).
|
||||
procedure TSpectrumViewOpenGL.DrawBandLetterGL(X1, X2: Integer; L: Char);
|
||||
var
|
||||
idx, cx: Integer;
|
||||
begin
|
||||
if (L < 'A') or (L > 'H') then Exit;
|
||||
if X2 <= X1 then Exit;
|
||||
idx := Ord(L) - Ord('A');
|
||||
if FLetterTex[idx].Tex = 0 then
|
||||
UploadText(FLetterTex[idx], L, SliceColor(L), 8, True); // цвет слайса
|
||||
cx := (X1 + X2) div 2;
|
||||
DrawTexture(FLetterTex[idx], cx - FLetterTex[idx].W div 2, 0);
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.DrawAGCLines(W, H: Integer; DBmax,
|
||||
InvRange: Double);
|
||||
var
|
||||
|
||||
+24
-1
@@ -23,6 +23,21 @@ const
|
||||
VFO_OVERLAY_ALPHA = 210;
|
||||
OVL_W = 280; // ширина карточки
|
||||
|
||||
// Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для
|
||||
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
|
||||
SLICE_COLORS: array[0..7] of TColor = (
|
||||
TColor($0060E040), // A — зелёный
|
||||
TColor($0010A8FF), // B — оранжевый
|
||||
TColor($00F0D030), // C — голубой
|
||||
TColor($00E070F0), // D — розовый/маджента
|
||||
TColor($0030E0F0), // E — жёлтый
|
||||
TColor($00F050B0), // F — фиолетовый (не красный — чтобы не путать с TX)
|
||||
TColor($00E0C060), // G — бирюзовый
|
||||
TColor($00C0C0C0)); // H — серый
|
||||
|
||||
// Цвет слайса по его букве (A..). За пределами таблицы — по кругу.
|
||||
function SliceColor(L: Char): TColor;
|
||||
|
||||
type
|
||||
TModeFilterEvent = procedure(Mode: Integer; FilterBW: Integer) of object;
|
||||
TDSPChangeEvent = procedure(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean) of object;
|
||||
@@ -146,6 +161,7 @@ type
|
||||
procedure SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean);
|
||||
property SliceId: Integer read FSliceId;
|
||||
property IsMain: Boolean read FIsMain;
|
||||
property SliceLetter: Char read FSlice; // буква слайса (A/B/…) для меток на полосе фильтра
|
||||
// Для маркера фильтра слайса на спектре (читает GL-вьюха).
|
||||
property DisplayVfoHz: Double read FVfoHz;
|
||||
property DisplayMode: Integer read FMode;
|
||||
@@ -173,6 +189,13 @@ type
|
||||
|
||||
implementation
|
||||
|
||||
function SliceColor(L: Char): TColor;
|
||||
var idx: Integer;
|
||||
begin
|
||||
if (L >= 'A') and (L <= 'Z') then idx := Ord(L) - Ord('A') else idx := 0;
|
||||
Result := SLICE_COLORS[idx mod (High(SLICE_COLORS) + 1)];
|
||||
end;
|
||||
|
||||
const
|
||||
KEY_COLOR = TColor($00FF00FF);
|
||||
|
||||
@@ -886,7 +909,7 @@ begin
|
||||
CurY := PAD;
|
||||
|
||||
{ --- Шапка: [A] BW ... [×] SPLIT [TX] --- }
|
||||
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, CLR_ACCENT, CLR_CARD_BG);
|
||||
DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_CARD_BG);
|
||||
RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса
|
||||
// Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть).
|
||||
if not FIsMain then
|
||||
|
||||
Reference in New Issue
Block a user