mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(dxcluster): споты DX-кластера на панадаптере
Telnet-клиент DX-кластера, база спотов и подписи позывных прямо на спектре на своих частотах. Разложено на три слоя, как бэндплан и лупа маяка. DXClusterClient.pas — один рабочий поток: резолв, неблокирующий connect с select квантами по 200 мс (Stop не ждёт таймаут соединения), логин позывным, чтение строк, реконнект с backoff. Приглашения логина И пароля ловятся в незавершённом хвосте буфера — типичный telnet-prompt приходит без CR/LF. Ошибки recv отличаются от таймаута кванта (EAGAIN/EINTR/WSAETIMEDOUT), иначе на ECONNRESET поток крутился бы в пустом цикле вместо реконнекта. Отправка дописывает частичный send. LCL-free. DXSpotStore.pas — потокобезопасная база: дедуп по позывному, TTL, потолок записей, монотонный Version. Единственная точка обмена потока с UI: никакого Synchronize, UI сам замечает правки по Version, как оверлеи — по ключам кэша. DXSpotOverlay.pas — рендер по модели BandPlanOverlay/VfoOverlay: кэшируется только полоса подписей (W × BandH) в key-color битмап, пересборка строго по dirty-ключу, на кадр — один keyed-композит. Штрихи от полосы до низа спектра рисует вызывающая сторона теми же примитивами, что и прочие маркеры: CPU — RawVLine внутри RawBegin/RawEnd, GL — DrawLine по готовому списку X/цвет. GL берёт тот же битмап текстурой и заливает её только при смене RenderVersion. Пересекающиеся подписи раскладываются лесенкой, цвет гаснет с возрастом, свой позывной выделен; палитра парная под тёмную и светлую тему. DXClusterForm.pas — окно списка: споты, лог соединения, строка команды кластеру (диалект set/filter у всех свой — не угадываем). Двойной клик или Enter = QSY. Данные тянутся поллингом по Version/LogVersion. Интеграция: кнопка DX в тулбаре (ЛКМ — подписи на спектре, ПКМ — окно), клик по подписи спота = QSY с автовыбором моды по комментарию кластера, страница SETUP → DX Cluster с персистом в секции "dxcluster". Правки SETUP прилетают посимвольно, поэтому запись конфига, TTL стора и переподключение откладываются до паузы в наборе — иначе набор позывного стоил бы шесть реконнектов, а промежуточный TTL «3» необратимо выбросил бы споты. Частота спота кладётся как есть и сравнивается с GetViewWindow: на QO-100 кластеры постят downlink 10489.xxx, что совпадает со шкалой пана само собой. Побочно в общих юнитах: WebUtils.SockSetRcvTimeout, FlatMemo.OnChange, FlatListBox.OnKeyDown, BlendBitmapKey вынесен в interface VfoOverlay (одна копия дворд-блендера на проект). Проверено на локальном фейковом кластере: логин и пароль по prompt без CR/LF, разбор спотов (включая QO-100), уход в RETRY по RST, Stop за 200 мс на висящем connect. На железе рендер не проверялся. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
unit DXSpotOverlay;
|
||||
|
||||
{
|
||||
Оверлей DX-спотов на спектре: позывной в «чипе» + вертикальный штрих к своей
|
||||
частоте. Пересекающиеся подписи раскладываются лесенкой по рядам сверху вниз.
|
||||
|
||||
Производительность — ровно модель BandPlanOverlay/VfoOverlay, ничего нового:
|
||||
|
||||
• Кэшируется ТОЛЬКО полоса подписей (W × FBandH, вверху спектра) в key-color
|
||||
битмап; пересборка идёт исключительно по dirty-ключу (версия стора / вид /
|
||||
ширина / тема / фильтры / тик затухания), а каждый кадр — один композит
|
||||
W × FBandH с постоянной альфой (BlendBitmapKey, тот же дворд-блендинг).
|
||||
Полноэкранный кэш W × H на кадр стоил бы в разы дороже — поэтому штрихи
|
||||
ниже полосы в кэш НЕ входят.
|
||||
• Штрихи от полосы до низа спектра рисует вызывающая сторона теми же
|
||||
примитивами, что и все прочие маркеры: CPU — RawVLine внутри RawBegin/
|
||||
RawEnd (никакого Canvas на битмапе кадра), GL — DrawLine. Оверлей отдаёт
|
||||
только готовый список X/цвет (TickCount/Tick), посчитанный при пересборке
|
||||
кэша, — на кадр никакой арифметики по спотам.
|
||||
• GL-путь берёт ТОТ ЖЕ битмап текстурой (DrawOverlayBitmap + UploadBitmap с
|
||||
color-key), заливка — только при dirty. Вид CPU и GL совпадает.
|
||||
|
||||
Домен частот — display-Гц, как их видит спектр (GetViewWindow). Частота спота
|
||||
кладётся как есть: на QO-100 кластеры постят downlink 10489.xxx, что совпадает
|
||||
со шкалой панадаптера само собой.
|
||||
}
|
||||
|
||||
{$mode objfpc}{$H+}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Graphics, Types, Math, Forms,
|
||||
VfoOverlay, // BlendBitmapKey — общий keyed-композит (одна копия на проект)
|
||||
DXSpotStore;
|
||||
|
||||
const
|
||||
DXSPOT_ALPHA = 225; // альфа композита полосы подписей
|
||||
DXSPOT_ROW_BASE_H = 15; // базовая высота ряда @96dpi
|
||||
DXSPOT_MAX_ROWS = 6; // потолок рядов лесенки (настройка кладётся сюда)
|
||||
|
||||
type
|
||||
// Один разложенный спот: где чип, где штрих, каким цветом.
|
||||
TDXSpotItem = record
|
||||
X: Integer; // X штриха (пиксель частоты)
|
||||
Chip: TRect; // прямоугольник подписи (для хит-теста)
|
||||
Color: TColor; // цвет с учётом возраста/своего позывного
|
||||
Spot: TDXSpot;
|
||||
end;
|
||||
|
||||
TDXSpotOverlay = class(TComponent)
|
||||
private
|
||||
FStore: TDXSpotStore; // не владеет
|
||||
FCenterFreq: Double;
|
||||
FSpanHz: Double;
|
||||
FEnabled: Boolean;
|
||||
FLightTheme: Boolean;
|
||||
FOwnCall: string;
|
||||
|
||||
// фильтры
|
||||
FModes: TDXModeSet;
|
||||
FMaxAgeMin: Integer;
|
||||
FMaxRows: Integer;
|
||||
|
||||
FCache: TBitmap;
|
||||
FCacheW: Integer;
|
||||
FCacheDirty: Boolean;
|
||||
FBandH: Integer; // высота полосы подписей (px, масштаб по DPI)
|
||||
FRowH: Integer;
|
||||
|
||||
// ключ кэша
|
||||
FKeyCenter: Double;
|
||||
FKeySpan: Double;
|
||||
FKeyW: Integer;
|
||||
FKeyEnabled: Boolean;
|
||||
FKeyVersion: Int64;
|
||||
FKeyLight: Boolean;
|
||||
FKeyRows: Integer;
|
||||
FKeyAgeTick: Int64;
|
||||
FAgeTick: Int64; // бампается таймером UI — обновить затухание
|
||||
|
||||
FRenderVer: Int64; // ++ на каждую пересборку кэша — ключ GL-текстуры
|
||||
FLayoutTTL: Integer; // горизонт затухания, снят один раз на раскладку
|
||||
FItems: array of TDXSpotItem;
|
||||
FItemCount: Integer;
|
||||
FHidden: Integer; // сколько спотов не влезло в лесенку
|
||||
|
||||
function FreqToX(FreqHz: Double; W: Integer): Integer;
|
||||
function AgeColor(const S: TDXSpot): TColor;
|
||||
procedure Layout(C: TCanvas; W: Integer);
|
||||
procedure DrawSelf(C: TCanvas; W: Integer);
|
||||
procedure RebuildCache(W: Integer);
|
||||
function NeedsRebuild(W: Integer): Boolean;
|
||||
procedure SetMaxRows(V: Integer);
|
||||
public
|
||||
constructor Create(AOwner: TComponent); override;
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure Attach(AStore: TDXSpotStore);
|
||||
procedure SetView(ACenterHz, ASpanHz: Double);
|
||||
procedure SetEnabled(En: Boolean);
|
||||
procedure SetLightTheme(Light: Boolean);
|
||||
procedure SetOwnCall(const ACall: string);
|
||||
procedure SetFilters(AModes: TDXModeSet; AMaxAgeMin: Integer);
|
||||
// Тик затухания: дёргается редко (раз в десятки секунд) — только чтобы
|
||||
// старые споты потускнели и ушедшие по TTL исчезли.
|
||||
procedure TickAge;
|
||||
procedure Invalidate;
|
||||
|
||||
function Active: Boolean;
|
||||
|
||||
// Пересобрать кэш+раскладку, если протух ключ. Вызывается В НАЧАЛЕ кадра:
|
||||
// штрихи рисуются РАНЬШЕ композита полосы, а список штрихов рождается
|
||||
// именно при пересборке — иначе первый кадр после нового спота рисовал бы
|
||||
// старую раскладку.
|
||||
procedure EnsureRendered(W: Integer);
|
||||
|
||||
// CPU-путь: композит полосы подписей вверху Target.
|
||||
procedure DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
// GL-путь: полоса подписей в Target-битмап (W × BandHeight) под текстуру.
|
||||
procedure DrawOverlayBitmap(Target: TBitmap; W: Integer);
|
||||
|
||||
// Штрихи ниже полосы — рисует вызывающая сторона (RawVLine / GL DrawLine).
|
||||
function TickCount: Integer;
|
||||
procedure Tick(Idx: Integer; out X: Integer; out Color: TColor);
|
||||
|
||||
// Хит-тест по подписи (клик = настроиться на спот).
|
||||
function SpotAtPixel(X, Y: Integer; out Spot: TDXSpot): Boolean;
|
||||
|
||||
property BandHeight: Integer read FBandH;
|
||||
property CenterFreq: Double read FCenterFreq; // для GL-инвалидизации
|
||||
property SpanHz: Double read FSpanHz;
|
||||
property MaxRows: Integer read FMaxRows write SetMaxRows;
|
||||
property Hidden: Integer read FHidden;
|
||||
property CacheDirty: Boolean read FCacheDirty;
|
||||
// Меняется ровно тогда, когда картинка полосы стала другой. GL-путь по нему
|
||||
// решает, перезаливать ли текстуру: он не зависит от того, кто первым
|
||||
// дёрнул пересборку в этом кадре (штрихи или композит).
|
||||
property RenderVersion: Int64 read FRenderVer;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
const
|
||||
KEY_COLOR = TColor($00FF00FF); // тот же ключ прозрачности, что у VfoOverlay
|
||||
|
||||
// Палитра (TColor = $00BBGGRR). Своя пара на тему: на тёмном спектре чип
|
||||
// тёмный со светлой подписью, на светлом — наоборот, иначе подписи спотов
|
||||
// на светлой теме сливались бы в грязное пятно.
|
||||
// тёмная тема светлая тема
|
||||
CLR_CHIP_BG: array[Boolean] of TColor = ($00202018, $00F2F2EC);
|
||||
CLR_CHIP_TEXT: array[Boolean] of TColor = ($00F0F0F0, $00181818);
|
||||
CLR_FRESH: array[Boolean] of TColor = ($00E0C040, $00A05800); // свежий
|
||||
CLR_STALE: array[Boolean] of TColor = ($00706850, $00B0A898); // на исходе TTL
|
||||
CLR_OWN: array[Boolean] of TColor = ($0040D0FF, $000060C8); // свой позывной
|
||||
|
||||
CHIP_PAD_X = 3;
|
||||
CHIP_GAP = 3; // минимальный зазор между чипами в одном ряду
|
||||
|
||||
function Mix(A, B: TColor; Num, Den: Integer): TColor;
|
||||
// A→B на Num/Den (0 = A, Den = B).
|
||||
var ra, ga, ba, rb, gb, bb: Integer;
|
||||
begin
|
||||
if Den <= 0 then Exit(A);
|
||||
ra := A and $FF; ga := (A shr 8) and $FF; ba := (A shr 16) and $FF;
|
||||
rb := B and $FF; gb := (B shr 8) and $FF; bb := (B shr 16) and $FF;
|
||||
ra := ra + (rb - ra) * Num div Den;
|
||||
ga := ga + (gb - ga) * Num div Den;
|
||||
ba := ba + (bb - ba) * Num div Den;
|
||||
Result := TColor((ba shl 16) or (ga shl 8) or ra);
|
||||
end;
|
||||
|
||||
{ ── Создание / параметры ─────────────────────────────────────────────────── }
|
||||
|
||||
constructor TDXSpotOverlay.Create(AOwner: TComponent);
|
||||
begin
|
||||
inherited Create(AOwner);
|
||||
FRowH := Round(DXSPOT_ROW_BASE_H * Screen.PixelsPerInch / 96);
|
||||
if FRowH < DXSPOT_ROW_BASE_H then FRowH := DXSPOT_ROW_BASE_H;
|
||||
FMaxRows := 4;
|
||||
FBandH := FRowH * FMaxRows + 4;
|
||||
FCenterFreq := 0;
|
||||
FSpanHz := 0;
|
||||
FEnabled := False;
|
||||
FModes := [];
|
||||
FMaxAgeMin := 0;
|
||||
FCache := TBitmap.Create;
|
||||
FCache.PixelFormat := pf32bit;
|
||||
FCacheW := 0;
|
||||
FCacheDirty := True;
|
||||
FKeyCenter := -1; FKeySpan := -1; FKeyW := -1; FKeyVersion := -1;
|
||||
FKeyEnabled := False; FKeyLight := False; FKeyRows := -1; FKeyAgeTick := -1;
|
||||
FAgeTick := 0;
|
||||
FRenderVer := 0;
|
||||
end;
|
||||
|
||||
destructor TDXSpotOverlay.Destroy;
|
||||
begin
|
||||
FCache.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.Attach(AStore: TDXSpotStore);
|
||||
begin
|
||||
FStore := AStore;
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.SetView(ACenterHz, ASpanHz: Double);
|
||||
begin
|
||||
if (Abs(ACenterHz - FCenterFreq) < 1.0) and (Abs(ASpanHz - FSpanHz) < 1.0) then Exit;
|
||||
FCenterFreq := ACenterHz;
|
||||
FSpanHz := ASpanHz;
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.SetEnabled(En: Boolean);
|
||||
begin
|
||||
if FEnabled = En then Exit;
|
||||
FEnabled := En;
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.SetLightTheme(Light: Boolean);
|
||||
begin
|
||||
if FLightTheme = Light then Exit;
|
||||
FLightTheme := Light;
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.SetOwnCall(const ACall: string);
|
||||
begin
|
||||
if SameText(FOwnCall, ACall) then Exit;
|
||||
FOwnCall := UpperCase(Trim(ACall));
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.SetFilters(AModes: TDXModeSet; AMaxAgeMin: Integer);
|
||||
begin
|
||||
if (FModes = AModes) and (FMaxAgeMin = AMaxAgeMin) then Exit;
|
||||
FModes := AModes;
|
||||
FMaxAgeMin := AMaxAgeMin;
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.SetMaxRows(V: Integer);
|
||||
begin
|
||||
V := EnsureRange(V, 1, DXSPOT_MAX_ROWS);
|
||||
if V = FMaxRows then Exit;
|
||||
FMaxRows := V;
|
||||
FBandH := FRowH * FMaxRows + 4;
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.TickAge;
|
||||
begin
|
||||
Inc(FAgeTick);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.Invalidate;
|
||||
begin
|
||||
FCacheDirty := True;
|
||||
end;
|
||||
|
||||
function TDXSpotOverlay.Active: Boolean;
|
||||
begin
|
||||
Result := FEnabled and (FStore <> nil);
|
||||
end;
|
||||
|
||||
function TDXSpotOverlay.FreqToX(FreqHz: Double; W: Integer): Integer;
|
||||
begin
|
||||
if FSpanHz <= 0 then Result := -1
|
||||
else Result := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
end;
|
||||
|
||||
function TDXSpotOverlay.AgeColor(const S: TDXSpot): TColor;
|
||||
// Свежий → CLR_FRESH, к концу окна TTL плавно уходит в CLR_STALE. Свой
|
||||
// позывной выделен цветом (и тоже гаснет — иначе старый спот кричал бы
|
||||
// громче свежих). TTL берётся из FLayoutTTL: он посчитан один раз на
|
||||
// раскладку, чтобы не дёргать лок стора на каждый спот.
|
||||
var
|
||||
AgeMin, K: Integer;
|
||||
Base: TColor;
|
||||
begin
|
||||
Base := CLR_FRESH[FLightTheme];
|
||||
if (FOwnCall <> '') and SameText(S.Call, FOwnCall) then
|
||||
Base := CLR_OWN[FLightTheme];
|
||||
if FLayoutTTL <= 0 then Exit(Base);
|
||||
AgeMin := Round((Now - S.Stamp) * 24 * 60);
|
||||
K := EnsureRange(AgeMin * 100 div Max(FLayoutTTL, 1), 0, 100);
|
||||
Result := Mix(Base, CLR_STALE[FLightTheme], K, 100);
|
||||
end;
|
||||
|
||||
{ ── Раскладка лесенки ────────────────────────────────────────────────────── }
|
||||
|
||||
procedure TDXSpotOverlay.Layout(C: TCanvas; W: Integer);
|
||||
// Споты приходят из стора отсортированными по частоте; каждому даём ПЕРВЫЙ
|
||||
// ряд сверху, где подпись не наезжает на предыдущую. Не нашлось ряда —
|
||||
// спот скрыт (считаем в FHidden), в лесенке дырок не оставляем.
|
||||
var
|
||||
Arr: TDXSpotArray;
|
||||
N, i, r, TW, ChipW, ChipX, Row: Integer;
|
||||
RowRight: array[0..DXSPOT_MAX_ROWS-1] of Integer;
|
||||
LoHz, HiHz: Double;
|
||||
begin
|
||||
FItemCount := 0;
|
||||
FHidden := 0;
|
||||
if (FStore = nil) or (FSpanHz <= 0) or (W <= 0) then Exit;
|
||||
|
||||
// Горизонт затухания: явный фильтр по возрасту, иначе TTL стора (чтение
|
||||
// под локом — поэтому один раз на раскладку, а не на каждый спот).
|
||||
FLayoutTTL := FMaxAgeMin;
|
||||
if FLayoutTTL <= 0 then FLayoutTTL := FStore.TTLMinutes;
|
||||
|
||||
LoHz := FCenterFreq - FSpanHz / 2;
|
||||
HiHz := FCenterFreq + FSpanHz / 2;
|
||||
N := FStore.SnapshotRange(LoHz, HiHz, FModes, FMaxAgeMin, Arr);
|
||||
if N = 0 then Exit;
|
||||
|
||||
for r := 0 to FMaxRows - 1 do RowRight[r] := -CHIP_GAP - 1;
|
||||
SetLength(FItems, N);
|
||||
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
TW := C.TextWidth(Arr[i].Call);
|
||||
ChipW := TW + CHIP_PAD_X * 2;
|
||||
ChipX := FreqToX(Arr[i].FreqHz, W) - ChipW div 2;
|
||||
if ChipX < 0 then ChipX := 0;
|
||||
if ChipX + ChipW > W then ChipX := W - ChipW;
|
||||
if ChipX < 0 then Continue; // подпись шире окна — не рисуем
|
||||
|
||||
Row := -1;
|
||||
for r := 0 to FMaxRows - 1 do
|
||||
if ChipX > RowRight[r] + CHIP_GAP then
|
||||
begin
|
||||
Row := r;
|
||||
Break;
|
||||
end;
|
||||
if Row < 0 then
|
||||
begin
|
||||
Inc(FHidden);
|
||||
Continue;
|
||||
end;
|
||||
RowRight[Row] := ChipX + ChipW;
|
||||
|
||||
FItems[FItemCount].X := FreqToX(Arr[i].FreqHz, W);
|
||||
FItems[FItemCount].Chip := Rect(ChipX, Row * FRowH + 2,
|
||||
ChipX + ChipW, Row * FRowH + FRowH);
|
||||
FItems[FItemCount].Color := AgeColor(Arr[i]);
|
||||
FItems[FItemCount].Spot := Arr[i];
|
||||
Inc(FItemCount);
|
||||
end;
|
||||
SetLength(FItems, FItemCount);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.DrawSelf(C: TCanvas; W: Integer);
|
||||
// Рендер полосы подписей в канвас W × FBandH. Фон — KEY_COLOR (прозрачный
|
||||
// при композите и в GL-текстуре).
|
||||
var
|
||||
i: Integer;
|
||||
R: TRect;
|
||||
Col: TColor;
|
||||
begin
|
||||
C.Brush.Color := KEY_COLOR;
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Pen.Style := psClear;
|
||||
C.FillRect(Rect(0, 0, W, FBandH));
|
||||
|
||||
// Шрифт привязан к высоте ряда (а не к point-size) — подпись влезает при
|
||||
// любом DPI-масштабе, как в бэндплане.
|
||||
C.Font.Name := 'Courier New';
|
||||
C.Font.Style := [fsBold];
|
||||
C.Font.Height := -Max(8, Round(FRowH * 0.62));
|
||||
|
||||
Layout(C, W);
|
||||
|
||||
for i := 0 to FItemCount - 1 do
|
||||
begin
|
||||
R := FItems[i].Chip;
|
||||
Col := FItems[i].Color;
|
||||
|
||||
// Штрих от низа чипа до низа полосы (дальше его продолжает вызывающая
|
||||
// сторона по TickCount/Tick — уже прямо в кадре).
|
||||
if (FItems[i].X >= 0) and (FItems[i].X < W) then
|
||||
begin
|
||||
C.Pen.Style := psSolid; C.Pen.Color := Col; C.Pen.Width := 1;
|
||||
C.MoveTo(FItems[i].X, R.Bottom);
|
||||
C.LineTo(FItems[i].X, FBandH);
|
||||
end;
|
||||
|
||||
// Чип: тёмная подложка + кромка цветом спота + позывной.
|
||||
C.Brush.Color := CLR_CHIP_BG[FLightTheme]; C.Brush.Style := bsSolid;
|
||||
C.Pen.Style := psSolid; C.Pen.Color := Col; C.Pen.Width := 1;
|
||||
C.Rectangle(R.Left, R.Top, R.Right, R.Bottom);
|
||||
|
||||
C.Brush.Style := bsClear;
|
||||
C.Font.Color := CLR_CHIP_TEXT[FLightTheme];
|
||||
C.TextOut(R.Left + CHIP_PAD_X,
|
||||
R.Top + (R.Bottom - R.Top - C.TextHeight('W')) div 2,
|
||||
FItems[i].Spot.Call);
|
||||
end;
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
|
||||
{ ── Кэш ──────────────────────────────────────────────────────────────────── }
|
||||
|
||||
function TDXSpotOverlay.NeedsRebuild(W: Integer): Boolean;
|
||||
var HalfPxHz: Double;
|
||||
begin
|
||||
if FStore = nil then Exit(False);
|
||||
// Порог по центру/спану — полпикселя (как в бэндплане): суб-пиксельный
|
||||
// дрейф центра при QO-100 decoder-lock не должен гонять полную пересборку.
|
||||
HalfPxHz := 0.5 * FSpanHz / Max(W, 1);
|
||||
if HalfPxHz < 1.0 then HalfPxHz := 1.0;
|
||||
Result := FCacheDirty or (FCacheW <> W) or (FKeyW <> W)
|
||||
or (FKeyEnabled <> FEnabled)
|
||||
or (FKeyVersion <> FStore.Version)
|
||||
or (FKeyLight <> FLightTheme)
|
||||
or (FKeyRows <> FMaxRows)
|
||||
or (FKeyAgeTick <> FAgeTick)
|
||||
or (Abs(FKeyCenter - FCenterFreq) >= HalfPxHz)
|
||||
or (Abs(FKeySpan - FSpanHz) >= HalfPxHz);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.RebuildCache(W: Integer);
|
||||
begin
|
||||
if (W <= 0) or (FStore = nil) then
|
||||
begin
|
||||
FCacheW := 0; FCacheDirty := False; FItemCount := 0;
|
||||
Inc(FRenderVer);
|
||||
Exit;
|
||||
end;
|
||||
if (FCache.Width <> W) or (FCache.Height <> FBandH) then
|
||||
FCache.SetSize(W, FBandH);
|
||||
DrawSelf(FCache.Canvas, W);
|
||||
Inc(FRenderVer);
|
||||
FCacheW := W;
|
||||
FKeyCenter := FCenterFreq;
|
||||
FKeySpan := FSpanHz;
|
||||
FKeyW := W;
|
||||
FKeyEnabled := FEnabled;
|
||||
FKeyVersion := FStore.Version;
|
||||
FKeyLight := FLightTheme;
|
||||
FKeyRows := FMaxRows;
|
||||
FKeyAgeTick := FAgeTick;
|
||||
FCacheDirty := False;
|
||||
end;
|
||||
|
||||
{ ── Рисование ────────────────────────────────────────────────────────────── }
|
||||
|
||||
procedure TDXSpotOverlay.EnsureRendered(W: Integer);
|
||||
begin
|
||||
if (not Active) or (W <= 0) then Exit;
|
||||
if NeedsRebuild(W) then RebuildCache(W);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
begin
|
||||
if (not Active) or (Target = nil) or (W <= 0) or (H <= FBandH) then Exit;
|
||||
EnsureRendered(W);
|
||||
if FCacheW <= 0 then Exit;
|
||||
BlendBitmapKey(Target, FCache, 0, 0, DXSPOT_ALPHA);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.DrawOverlayBitmap(Target: TBitmap; W: Integer);
|
||||
begin
|
||||
if (Target = nil) or (W <= 0) then Exit;
|
||||
// Раскладка/список штрихов должны быть свежими и в GL-пути.
|
||||
EnsureRendered(W);
|
||||
Target.PixelFormat := pf32bit;
|
||||
Target.SetSize(W, FBandH);
|
||||
Target.Canvas.Draw(0, 0, FCache);
|
||||
end;
|
||||
|
||||
function TDXSpotOverlay.TickCount: Integer;
|
||||
begin
|
||||
if not Active then Result := 0 else Result := FItemCount;
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.Tick(Idx: Integer; out X: Integer; out Color: TColor);
|
||||
begin
|
||||
if (Idx < 0) or (Idx >= FItemCount) then
|
||||
begin
|
||||
X := -1; Color := clNone;
|
||||
Exit;
|
||||
end;
|
||||
X := FItems[Idx].X;
|
||||
Color := FItems[Idx].Color;
|
||||
end;
|
||||
|
||||
function TDXSpotOverlay.SpotAtPixel(X, Y: Integer; out Spot: TDXSpot): Boolean;
|
||||
// Хит только по полосе подписей: ниже неё живут тюнинг/маркеры/слайсы, и
|
||||
// перехватывать там клик было бы поперёк привычного поведения спектра.
|
||||
var i: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
if (not Active) or (Y < 0) or (Y > FBandH) then Exit;
|
||||
for i := 0 to FItemCount - 1 do
|
||||
if (X >= FItems[i].Chip.Left - 2) and (X <= FItems[i].Chip.Right + 2) and
|
||||
(Y >= FItems[i].Chip.Top - 2) and (Y <= FItems[i].Chip.Bottom + 2) then
|
||||
begin
|
||||
Spot := FItems[i].Spot;
|
||||
Exit(True);
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
Reference in New Issue
Block a user