mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(spectrum): zoom + pan для спектра и водопада (WDSP span-clip)
Зум через нативный span-clip WDSP (fscLin/fscHin в SetAnalyzer): clp=0, видимое окно сужается клипом, при зуме 0 полоса == sample rate (старое поведение сохранено). Конвенция как в Thetis: ZoomFactor 0=без зума..1=макс. - WDSPEngine: SetZoomPan + расчёт span-clip, ViewLowHz/ViewHighHz. - RadioController: GetViewWindow (единая истина видимого окна для оверлеев и мыши), ApplyZoom, ZoomIn/Out по лестнице кратностей, ZoomDefault. Якорь зума кнопками — на маркере активного VFO. rfZoom. - Per-band персист зума/пана (TBandSettings + Save/Load/Restore). - MainForm: маршрутизация оверлеев/кликов через GetViewWindow (покрывает CPU и OpenGL), резерв строки PANZOOM_H в layout + сплиттере, rfZoom-рендер. - WebAdapter: web-спектр/наведение на view-окно (делит те же WDSP-пиксели). - PanZoomBar (новый): нижняя полоса = линейка полного диапазона (частоты + деления), поверх окно-селектор «стекло» (полупрозрачное, объёмная фаска). Середина=пан, края=зум, клик мимо=центрировать, двойной клик=сброс. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+140
-1
@@ -66,7 +66,7 @@ type
|
||||
rfRxGain, // Pluto: hw-gain (auto/manual dB)
|
||||
rfVolume, rfMute, rfDrive, rfAtten,
|
||||
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
|
||||
rfCenterFreq, rfSpan, rfSampleRate,
|
||||
rfCenterFreq, rfSpan, rfSampleRate, rfZoom,
|
||||
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
|
||||
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
||||
rfNR, rfNB, rfSNB, rfANF,
|
||||
@@ -138,6 +138,7 @@ type
|
||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||||
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
||||
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
||||
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
||||
public
|
||||
// ============================================================
|
||||
@@ -181,6 +182,8 @@ type
|
||||
FCenterFreq: Double;
|
||||
FSpanHz: Double;
|
||||
FSampleRate: Integer;
|
||||
FZoomFactor: Double; // зум спектра/водопада 0.05..1.0 (1.0 = без зума)
|
||||
FPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
||||
FCurrentBand: Integer;
|
||||
FCurrentXvtr: Integer;
|
||||
|
||||
@@ -480,6 +483,18 @@ type
|
||||
procedure SetSpan(Hz: Integer);
|
||||
procedure SetSampleRate(Hz: Integer);
|
||||
|
||||
// ---- Zoom/pan спектра и водопада ----
|
||||
// GetViewWindow — единая истина «видимого окна» (центр+span в Гц) для всех
|
||||
// оверлеев и маппинга мыши: при зуме отдаёт окно из WDSP-анализатора, иначе
|
||||
// базовые FCenterFreq/FSpanHz.
|
||||
procedure GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||||
procedure ApplyZoom(AZoom, APan: Double); // задать зум+пан, сохранить в band, Changed(rfZoom)
|
||||
procedure ZoomIn; // шаг увеличения (якорь — маркер активного VFO)
|
||||
procedure ZoomOut; // шаг уменьшения
|
||||
procedure ZoomDefault; // сброс в 1.0 (весь span)
|
||||
function ZoomFactor: Double;
|
||||
function PanSlider: Double;
|
||||
|
||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||
// QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
|
||||
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
|
||||
@@ -610,6 +625,7 @@ begin
|
||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||||
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||||
FCurrentBand := 5; FCurrentXvtr := -1;
|
||||
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
||||
FPAMaxPower := 100.0;
|
||||
@@ -1452,6 +1468,8 @@ begin
|
||||
Result.CTun := FCTun;
|
||||
Result.CenterHz := FCenterFreq; // DDC-центр → восстановим offset CTun при загрузке
|
||||
Result.SpanHz := FSpanHz;
|
||||
Result.ZoomFactor := FZoomFactor;
|
||||
Result.PanSlider := FPanSlider;
|
||||
Result.FMSQOn := FFMSQOn;
|
||||
Result.FMSQLevel := FFMSQLevel;
|
||||
Result.CTCSSOn := FFMCTCSSOn;
|
||||
@@ -1667,6 +1685,12 @@ begin
|
||||
|
||||
ApplyBandDSP(B);
|
||||
|
||||
// --- Zoom/pan диапазона: восстанавливаем и переармируем анализатор ---
|
||||
FZoomFactor := EnsureRange(B.ZoomFactor, 0.0, 1.0);
|
||||
FPanSlider := EnsureRange(B.PanSlider, 0.0, 1.0);
|
||||
if Assigned(FDSPEngine) then FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||||
Changed(rfZoom);
|
||||
|
||||
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
|
||||
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
|
||||
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
|
||||
@@ -2327,6 +2351,121 @@ end;
|
||||
procedure TRadioController.SetSpan(Hz: Integer);
|
||||
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Zoom / pan спектра и водопада
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function TRadioController.ZoomFactor: Double;
|
||||
begin Result := FZoomFactor; end;
|
||||
|
||||
function TRadioController.PanSlider: Double;
|
||||
begin Result := FPanSlider; end;
|
||||
|
||||
procedure TRadioController.GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||||
// Видимое окно (центр+span, Гц) с учётом зума. Формула совпадает с расчётом
|
||||
// span-clip в WDSPEngine.ApplyRXAnalyzerSettings (bw=FSpanHz, clp=0), поэтому
|
||||
// оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
|
||||
var
|
||||
FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double;
|
||||
begin
|
||||
if (FZoomFactor <= 0.0) or (FSpanHz <= 0.0) then
|
||||
begin
|
||||
ACenter := FCenterFreq;
|
||||
ASpan := FSpanHz;
|
||||
Exit;
|
||||
end;
|
||||
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(FZoomFactor, 0.0, 1.0) + 1.0);
|
||||
FVis := 1.0 - OneMinusFVis; // доля span после зума
|
||||
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0; // ZOOM_LIMIT
|
||||
VisSpan := FVis * FSpanHz;
|
||||
LowEdgeOff := -FSpanHz / 2.0 + EnsureRange(FPanSlider, 0.0, 1.0) * OneMinusFVis * FSpanHz;
|
||||
CenterOff := LowEdgeOff + VisSpan / 2.0;
|
||||
ACenter := FCenterFreq + CenterOff;
|
||||
ASpan := VisSpan;
|
||||
end;
|
||||
|
||||
function TRadioController.AnchoredPan(AZoom: Double): Double;
|
||||
// Положение пана, при котором видимое окно центрировано на маркере активного
|
||||
// VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер
|
||||
// перестаёт быть строго по центру — окно упирается в край).
|
||||
var OneMinusFVis, U: Double;
|
||||
begin
|
||||
if (AZoom <= 0.0) or (FSpanHz <= 0.0) then Exit(0.5);
|
||||
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 0.0, 1.0) + 1.0);
|
||||
if OneMinusFVis <= 1e-9 then Exit(0.5);
|
||||
U := (ActiveVfoHz - FCenterFreq) / FSpanHz; // нормализованный сдвиг маркера
|
||||
Result := EnsureRange(0.5 + U / OneMinusFVis, 0.0, 1.0);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyZoom(AZoom, APan: Double);
|
||||
begin
|
||||
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
|
||||
FPanSlider := EnsureRange(APan, 0.0, 1.0);
|
||||
if Assigned(FDSPEngine) then
|
||||
FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||||
if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||||
begin
|
||||
FBandCache[FCurrentBand].ZoomFactor := FZoomFactor;
|
||||
FBandCache[FCurrentBand].PanSlider := FPanSlider;
|
||||
end;
|
||||
Changed(rfZoom);
|
||||
end;
|
||||
|
||||
// Лестница кратностей увеличения: мелкий шаг в начале (1.0→1.2→1.45…), крупнее
|
||||
// к концу. Кнопки [+]/[-] шагают по ней; ZoomFactor (0..1) хранит соответствующий
|
||||
// z. Перевод кратность↔z — инверсия формулы анализатора (см. ApplyRXAnalyzerSettings).
|
||||
const
|
||||
ZOOM_MAG_LADDER: array[0..16] of Double =
|
||||
(1.0, 1.1, 1.15, 1.2, 1.45, 1.75, 2.1, 2.5, 3.0, 3.7, 4.7, 6.2, 8.5, 12.0, 20.0, 40.0, 100.0);
|
||||
|
||||
function ZoomMagToZ(M: Double): Double;
|
||||
begin
|
||||
if M <= 1.0 then Exit(0.0);
|
||||
// M = 1/(1-0.99*log10(9z+1)) ⇒ z = (10^((1-1/M)/0.99) - 1)/9
|
||||
Result := EnsureRange((Power(10.0, (1.0 - 1.0 / M) / 0.99) - 1.0) / 9.0, 0.0, 1.0);
|
||||
end;
|
||||
|
||||
function ZoomZToMag(Z: Double): Double;
|
||||
begin
|
||||
Result := 1.0 / (1.0 - 0.99 * Log10(9.0 * EnsureRange(Z, 0.0, 1.0) + 1.0));
|
||||
end;
|
||||
|
||||
procedure TRadioController.ZoomIn;
|
||||
var CurMag, Z: Double; i: Integer;
|
||||
begin
|
||||
CurMag := ZoomZToMag(FZoomFactor);
|
||||
for i := 0 to High(ZOOM_MAG_LADDER) do
|
||||
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
|
||||
begin
|
||||
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||||
ApplyZoom(Z, AnchoredPan(Z));
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRadioController.ZoomOut;
|
||||
var CurMag, Z: Double; i: Integer;
|
||||
begin
|
||||
CurMag := ZoomZToMag(FZoomFactor);
|
||||
for i := High(ZOOM_MAG_LADDER) downto 0 do
|
||||
if ZOOM_MAG_LADDER[i] < CurMag / 1.001 then
|
||||
begin
|
||||
if ZOOM_MAG_LADDER[i] <= 1.0 then ApplyZoom(0.0, 0.5)
|
||||
else
|
||||
begin
|
||||
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||||
ApplyZoom(Z, AnchoredPan(Z));
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
ApplyZoom(0.0, 0.5);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ZoomDefault;
|
||||
begin
|
||||
ApplyZoom(0.0, 0.5);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetSampleRate(Hz: Integer);
|
||||
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
|
||||
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в
|
||||
|
||||
Reference in New Issue
Block a user