From 30a6616dbe5baf17df50e7e02240bd4199484697 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Thu, 2 Jul 2026 13:19:50 +0300 Subject: [PATCH] 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 --- MainForm.pas | 204 +++++++++++++++++++++-------------------- SpectrumView.pas | 27 ++++++ SpectrumViewOpengl.pas | 23 +++++ VfoOverlay.pas | 25 ++++- 4 files changed, 177 insertions(+), 102 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index e0eb0bd..c3bc315 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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; // Заливает во флаг слайса его текущее состояние из контроллера. diff --git a/SpectrumView.pas b/SpectrumView.pas index f607df3..d937438 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -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); diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index 5ca6a8f..a432df5 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -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 diff --git a/VfoOverlay.pas b/VfoOverlay.pas index 6c8f3f2..7b3e348 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -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