Files
ewsdr/DXSpotOverlay.pas
ew8bakandClaude Opus 5 13b46f0b7d ui(dxcluster): системный шрифт вместо захардкоженного 'Courier New'
По проекту гарнитуры не задаём — работает системный шрифт (907c2c0 выпилил
'Courier New' из flat-контролов, 19babef из статус-бара). Новый код DX-кластера
это правило нарушал в трёх местах, все три введены в a2fdc3a.

- DXSpotOverlay: подписи спотов на спектре рисуются системным шрифтом —
  гарнитура убрана совсем. Моноширинность там ни к чему: позывные не в
  колонках, ширина чипа и так меряется по TextWidth. Размер по-прежнему задан
  через Font.Height от высоты ряда — иначе подпись не влезала бы при DPI.
- DXClusterForm: список спотов и лог соединения ОСТАЮТСЯ моноширинными
  осознанно (колонки частота/позывной/мода/UTC/спотер выровнены пробелами в
  FormatSpotLine, пропорциональный шрифт их развалит), но имя теперь
  платформенное — MONO_FONT = Consolas/Monospace, как в CWTerminalForm и в
  полосе CW у PanafallPanel. 'Courier New' на Linux не существует вовсе,
  fontconfig молча подменял его.

Font.Size/Font.Style не трогал: их проект оставляет и после перехода на
системный шрифт (в StatusBar размер сохранён).

Дифы всех четырёх коммитов ветки проверены по Font.Name — других мест, где
DX-код задаёт гарнитуру, нет. Сборка ewsdr (--ws=qt6) — ОК.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 18:43:36 +03:00

581 lines
24 KiB
ObjectPascal
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit DXSpotOverlay;
{
Оверлей DX-спотов на спектре: позывной в «чипе» + вертикальный штрих к своей
частоте. Пересекающиеся подписи раскладываются лесенкой по рядам сверху вниз.
Производительность — ровно модель BandPlanOverlay/VfoOverlay, ничего нового:
• Кэшируется ТОЛЬКО полоса подписей (W × FBandH, вверху спектра) в key-color
битмап; пересборка идёт исключительно по dirty-ключу (версия стора / вид /
ширина / тема / фильтры / тик затухания), а каждый кадр — один композит
W × FBandH с постоянной альфой (BlendBitmapKey, тот же дворд-блендинг).
Полноэкранный кэш W × H на кадр стоил бы в разы дороже — поэтому штрихи
ниже полосы в кэш НЕ входят.
• Версия стора глобальна, а окно у каждого пана своё, поэтому на смену
версии сначала берётся снимок окна и сверяется его ПОДПИСЬ: спот, севший
на чужой диапазон, до полосы подписей этого пана не доходит. Без этого
каждый пан пересобирал бы кэш (и перезаливал GL-текстуру) на любой спот
откуда угодно — цена, которая множится на число панов.
• Штрихи от полосы до низа спектра рисует вызывающая сторона теми же
примитивами, что и все прочие маркеры: 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;
FKeyStamp: Int64; // подпись набора, по которому собран кэш
FAgeTick: Int64; // бампается таймером UI — обновить затухание
FRenderVer: Int64; // ++ на каждую пересборку кэша — ключ GL-текстуры
FLayoutTTL: Integer; // горизонт затухания, снят один раз на раскладку
// Снимок окна: берётся при смене вида/версии стора, живёт до пересборки —
// подпись считается по нему же, второй раз стор не дёргаем.
FArr: TDXSpotArray;
FArrN: Integer;
FArrStamp: Int64;
FItems: array of TDXSpotItem;
FItemCount: Integer;
FHidden: Integer; // сколько спотов не влезло в лесенку
function FreqToX(FreqHz: Double; W: Integer): Integer;
function AgeColor(const S: TDXSpot): TColor;
procedure TakeSnapshot;
procedure Layout(C: TCanvas; W: Integer);
procedure DrawSelf(C: TCanvas; W: Integer);
procedure RebuildCache(W: Integer; Ver: Int64);
function ViewKeysChanged(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; // минимальный зазор между чипами в одном ряду
// Подпись видимого набора (FNV-1a 64). Базис — канонический $CBF29CE4…,
// сложенный из половин: цельным литералом он не влезает в Int64, а
// сравниваем мы подпись только сама с собой, так что важна лишь стабильность.
FNV_BASIS_HI = QWord($CBF29CE4);
FNV_BASIS_LO = QWord($84222325);
FNV_PRIME: QWord = $00000100000001B3;
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;
FKeyStamp := 0;
FArrN := 0;
FArrStamp := 0;
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.TakeSnapshot;
// Снимок видимого окна + его подпись. Подпись складывает всё, от чего зависит
// картинка полосы: позывной, частоту, моду и метку времени (она правит цвет
// затухания и обновляется при пере-споте). Совпала с прошлой — пересобирать
// нечего, какой бы ни стала версия стора.
var
i, k: Integer;
H: QWord;
LoHz, HiHz: Double;
begin
FArrN := 0;
SetLength(FArr, 0);
FArrStamp := 0;
if (FStore = nil) or (FSpanHz <= 0) then Exit;
// Горизонт затухания: явный фильтр по возрасту, иначе TTL стора (чтение
// под локом — поэтому один раз на снимок, а не на каждый спот).
FLayoutTTL := FMaxAgeMin;
if FLayoutTTL <= 0 then FLayoutTTL := FStore.TTLMinutes;
LoHz := FCenterFreq - FSpanHz / 2;
HiHz := FCenterFreq + FSpanHz / 2;
FArrN := FStore.SnapshotRange(LoHz, HiHz, FModes, FMaxAgeMin, FArr);
// Хэш живёт переполнением — проверки снимаем локально, чтобы сборка с
// {$Q+}/{$R+} (у нас их нет, но режимы правятся в .lpi) не падала на нём.
{$PUSH}{$Q-}{$R-}
H := (FNV_BASIS_HI shl 32) or FNV_BASIS_LO; // FNV-1a 64
for i := 0 to FArrN - 1 do
begin
for k := 1 to Length(FArr[i].Call) do
H := (H xor QWord(Ord(FArr[i].Call[k]))) * FNV_PRIME;
H := (H xor QWord(Round(FArr[i].FreqHz))) * FNV_PRIME;
H := (H xor QWord(Round(FArr[i].Stamp * 86400))) * FNV_PRIME;
H := (H xor QWord(Ord(FArr[i].Mode))) * FNV_PRIME;
end;
FArrStamp := Int64(H);
{$POP}
end;
procedure TDXSpotOverlay.Layout(C: TCanvas; W: Integer);
// Споты приходят из стора отсортированными по частоте; каждому даём ПЕРВЫЙ
// ряд сверху, где подпись не наезжает на предыдущую. Не нашлось ряда —
// спот скрыт (считаем в FHidden), в лесенке дырок не оставляем. Работаем по
// снимку FArr — его взял TakeSnapshot перед пересборкой.
var
N, i, r, TW, ChipW, ChipX, Row: Integer;
RowRight: array[0..DXSPOT_MAX_ROWS-1] of Integer;
begin
FItemCount := 0;
FHidden := 0;
if (FStore = nil) or (FSpanHz <= 0) or (W <= 0) then Exit;
N := FArrN;
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(FArr[i].Call);
ChipW := TW + CHIP_PAD_X * 2;
ChipX := FreqToX(FArr[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(FArr[i].FreqHz, W);
FItems[FItemCount].Chip := Rect(ChipX, Row * FRowH + 2,
ChipX + ChipW, Row * FRowH + FRowH);
FItems[FItemCount].Color := AgeColor(FArr[i]);
FItems[FItemCount].Spot := FArr[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));
// Гарнитуру НЕ задаём — системная, как договорено по проекту: позывные тут
// не в колонках, ширина чипа и так меряется по TextWidth. Задан только
// размер, привязанный к высоте ряда (а не к point-size), — подпись влезает
// при любом DPI-масштабе, как в бэндплане.
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.ViewKeysChanged(W: Integer): Boolean;
// Всё, что делает картинку заведомо другой, КРОМЕ версии стора: её разбирает
// EnsureRendered отдельно (версия глобальна, окно — наше).
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 (FKeyLight <> FLightTheme)
or (FKeyRows <> FMaxRows)
or (FKeyAgeTick <> FAgeTick)
or (Abs(FKeyCenter - FCenterFreq) >= HalfPxHz)
or (Abs(FKeySpan - FSpanHz) >= HalfPxHz);
end;
procedure TDXSpotOverlay.RebuildCache(W: Integer; Ver: Int64);
// Снимок (TakeSnapshot) к этому моменту уже взят вызывающим — здесь только
// рисование и запись ключей.
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 := Ver;
FKeyStamp := FArrStamp;
FKeyLight := FLightTheme;
FKeyRows := FMaxRows;
FKeyAgeTick := FAgeTick;
FCacheDirty := False;
end;
{ ── Рисование ────────────────────────────────────────────────────────────── }
procedure TDXSpotOverlay.EnsureRendered(W: Integer);
var Ver: Int64;
begin
if (not Active) or (W <= 0) then Exit;
Ver := FStore.Version;
if ViewKeysChanged(W) then
begin
TakeSnapshot;
RebuildCache(W, Ver);
Exit;
end;
if FKeyVersion = Ver then Exit; // в сторе с прошлого кадра ничего
// Стор правили — но, может, не в нашем окне: снимок с подписью дешевле
// пересборки полосы (и, в GL, перезаливки текстуры). Версию запоминаем в
// любом случае: этот вопрос уже разобран.
FKeyVersion := Ver;
TakeSnapshot;
if FArrStamp = FKeyStamp then Exit;
RebuildCache(W, Ver);
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.