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:
2026-07-02 13:19:50 +03:00
co-authored by Claude Opus 4.8
parent 3fbde19b9f
commit 30a6616dbe
4 changed files with 177 additions and 102 deletions
+103 -101
View File
@@ -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;
TmpO: TVfoOverlay;
TmpI: Integer;
Moved: Boolean;
// Идеальный Left рядом с полосой фильтра [loHz..hiHz] вокруг несущей Carrier.
function IdealLeftFor(Carrier, loHz, hiHz: Double; OW: Integer): Integer;
var vfoX, sx1, sx2, nl: Integer;
begin
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 not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
OW := FVfoOverlay.Width;
FController.GetViewWindow(ViewCenter, ViewSpan);
if PbSpectrum = nil then Exit;
W := PbSpectrum.Width;
FController.GetViewWindow(VC, VS);
if (W <= 0) or (VS <= 0) then Exit;
if (PbSpectrum.Width > 0) and (ViewSpan > 0) then
// 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
VfoX := Round((FController.FVfoA - ViewCenter + ViewSpan / 2) / ViewSpan * PbSpectrum.Width)
end
else
VfoX := PbSpectrum.Width div 2;
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;
end;
Ovls[n] := FVfoOverlay;
OWs[n] := FVfoOverlay.Width;
Ideal[n] := IdealLeftFor(FController.FVfoA, LoHz, HiHz, OWs[n]);
Inc(n);
end;
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;
// Кромки полосы фильтра относительно несущей (для выбора стороны накладки).
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;
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;
end;
FilterStartX := Max(0, Min(PbSpectrum.Width - 1, FilterStartX));
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
// 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
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;
// По умолчанию справа от полосы фильтра; если не влезает — слева от неё.
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
// 3. Вперёд-проход: сосед слева толкает правого (тот не пересекает).
SetLength(Lefts, n);
for i := 0 to n - 1 do
begin
FVfoOverlay.Left := NewLeft;
FVfoOverlay.Top := NewTop;
OnVfoOverlayInvalidate(FVfoOverlay);
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;
// Заливает во флаг слайса его текущее состояние из контроллера.