feat(slices): пан-зум панов N — свой бар/кнопки, span-clip анализатора пана

Движок: TPanChan.ZoomFactor/PanSlider, span-clip в ApplyPanAnalyzerSettings
(математика главного, ZOOM_LIMIT 100), SetPanZoomPan с no-op guard (SetAnalyzer
сбрасывает FFT-историю). Зум переживает смену rate/ADC пана.
Контроллер: формула окна вынесена в ViewWindowFor (главный GetViewWindow тоже
через неё), GetPanViewWindow (PanId=0 делегирует), PanZoomIn/Out по лестнице
кратностей с якорем на центре окна, PanZoomDefault, геттеры.
UI: TPanZoomEvent получил Sender; у панов N включён ряд зума, бар
диспетчеризуется по объекту, кнопки −/⌂/+ общие через PanOfZoomButton (матч по
идентичности — MakeFlatBtn создаёт кнопки с Owner=контейнер, PanFromSender их
не видит); SyncPanZoomUI/RefreshPanZoom, вся мышь панов зум-корректна через
GetPanViewWindow. Темизация баров панов N.
Смоук panzoomtest ALL PASS (пик тона совпадает с формулой окна ±0.6%),
подтверждено на экране.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:35:13 +03:00
co-authored by Claude Fable 5
parent 209e490dfa
commit 64edf9e289
6 changed files with 312 additions and 36 deletions
+139 -10
View File
@@ -167,6 +167,7 @@ type
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
procedure PanZoomStepTo(PanId: Integer; NewZ: Double); // зум с якорем на центре окна
function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq)
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
@@ -353,6 +354,8 @@ type
FPanFreqHz: array[1..MAX_PANS-1] of Double;
FPanRateKHz: array[1..MAX_PANS-1] of Word;
FPanADC: array[1..MAX_PANS-1] of Byte; // ADC-источник пана (3.4)
FPanZoomF: array[1..MAX_PANS-1] of Double; // зум пана 0..1 (0 = весь span)
FPanPanSl: array[1..MAX_PANS-1] of Double; // положение окна пана 0..1
FMainADCSrc: Byte; // ADC главного DDC (пан 0)
FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
@@ -581,6 +584,15 @@ type
// N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1].
function PanADC(PanId: Integer): Byte;
function SetPanADC(PanId: Integer; ADC: Byte): Boolean;
// ---- Зум/пан доп. панов (PanId=0 делегирует главному зуму) ----
// Видимое окно пана — та же формула, что span-clip его анализатора.
procedure GetPanViewWindow(PanId: Integer; out ACenter, ASpan: Double);
procedure SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
procedure PanZoomIn(PanId: Integer); // шаг по лестнице, якорь = центр окна
procedure PanZoomOut(PanId: Integer);
procedure PanZoomDefault(PanId: Integer);
function PanZoomFactor(PanId: Integer): Double;
function PanPanSlider(PanId: Integer): Double;
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
@@ -1189,6 +1201,8 @@ begin
FPanFreqHz[i] := 0;
FPanRateKHz[i] := 0;
FPanADC[i] := 0;
FPanZoomF[i] := 0.0;
FPanPanSl[i] := 0.5;
end;
for i := 0 to MAX_DDCS - 1 do
FDDCPanMap[i] := -1;
@@ -1235,6 +1249,8 @@ begin
FPanFreqHz[PanId] := FreqHz;
FPanRateKHz[PanId] := RateKHz;
FPanADC[PanId] := ADCSource;
FPanZoomF[PanId] := 0.0;
FPanPanSl[PanId] := 0.5;
FDDCPanMap[ddc] := PanId;
FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource);
Result := True;
@@ -1282,6 +1298,8 @@ begin
FPanFreqHz[PanId] := 0;
FPanRateKHz[PanId] := 0;
FPanADC[PanId] := 0;
FPanZoomF[PanId] := 0.0;
FPanPanSl[PanId] := 0.5;
for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (FSlices[i].PanId = PanId) then
RemoveSlice(FSlices[i].Id);
@@ -3041,29 +3059,35 @@ begin Result := FZoomFactor; end;
function TRadioController.PanSlider: Double;
begin Result := FPanSlider; end;
procedure TRadioController.GetViewWindow(out ACenter: Double; out ASpan: Double);
procedure ViewWindowFor(BaseC, BaseS, AZoom, APan: Double;
out ACenter, ASpan: Double);
// Видимое окно (центр+span, Гц) с учётом зума. Формула совпадает с расчётом
// span-clip в WDSPEngine.ApplyRXAnalyzerSettings (bw=FSpanHz, clp=0), поэтому
// оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
// span-clip в WDSPEngine.Apply{RX,Pan}AnalyzerSettings (bw=BaseS, clp=0),
// поэтому оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
var
FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double;
begin
if (FZoomFactor <= 0.0) or (FSpanHz <= 0.0) then
if (AZoom <= 0.0) or (BaseS <= 0.0) then
begin
ACenter := FCenterFreq;
ASpan := FSpanHz;
ACenter := BaseC;
ASpan := BaseS;
Exit;
end;
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(FZoomFactor, 0.0, 1.0) + 1.0);
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 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;
VisSpan := FVis * BaseS;
LowEdgeOff := -BaseS / 2.0 + EnsureRange(APan, 0.0, 1.0) * OneMinusFVis * BaseS;
CenterOff := LowEdgeOff + VisSpan / 2.0;
ACenter := FCenterFreq + CenterOff;
ACenter := BaseC + CenterOff;
ASpan := VisSpan;
end;
procedure TRadioController.GetViewWindow(out ACenter: Double; out ASpan: Double);
begin
ViewWindowFor(FCenterFreq, FSpanHz, FZoomFactor, FPanSlider, ACenter, ASpan);
end;
function TRadioController.AnchoredPan(AZoom: Double): Double;
// Положение пана, при котором видимое окно центрировано на маркере активного
// VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер
@@ -3156,6 +3180,111 @@ begin
ApplyZoom(0.0, 0.5);
end;
{ ---- Зум/пан доп. панов (PanId=0 делегирует главному) ---- }
procedure TRadioController.GetPanViewWindow(PanId: Integer;
out ACenter, ASpan: Double);
begin
if PanId = 0 then
begin
GetViewWindow(ACenter, ASpan);
Exit;
end;
ACenter := 0; ASpan := 0;
if not PanDDCActive(PanId) then Exit;
ViewWindowFor(FPanFreqHz[PanId], FPanRateKHz[PanId] * 1000.0,
FPanZoomF[PanId], FPanPanSl[PanId], ACenter, ASpan);
end;
procedure TRadioController.SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
// Зум пана — состояние сессии пана (не персистится с бандом, в отличие от
// главного ApplyZoom): пан живёт до STOP.
begin
if PanId = 0 then
begin
ApplyZoom(AZoom, APan);
Exit;
end;
if not PanDDCActive(PanId) then Exit;
FPanZoomF[PanId] := EnsureRange(AZoom, 0.0, 1.0);
FPanPanSl[PanId] := EnsureRange(APan, 0.0, 1.0);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetPanZoomPan(PanId, FPanZoomF[PanId], FPanPanSl[PanId]);
end;
procedure TRadioController.PanZoomStepTo(PanId: Integer; NewZ: Double);
// Ставит новый зум, удерживая ЦЕНТР текущего видимого окна (у пана нет VFO-якоря
// главного AnchoredPan; фиксация центра — естественный аналог).
var
C, S, BaseC, BaseS, U, OneMinus, FVis, NewPan: Double;
begin
if not PanDDCActive(PanId) then Exit;
GetPanViewWindow(PanId, C, S);
BaseC := FPanFreqHz[PanId];
BaseS := FPanRateKHz[PanId] * 1000.0;
if BaseS <= 0 then Exit;
NewZ := EnsureRange(NewZ, 0.0, 1.0);
// Инверсия ViewWindowFor: U = CenterOff/BaseS = 0.5 + Pan·OneMinus + FVis/2.
U := (C - BaseC) / BaseS;
OneMinus := 0.99 * Log10(9.0 * NewZ + 1.0);
if OneMinus <= 1e-9 then
NewPan := 0.5
else
begin
FVis := 1.0 - OneMinus;
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0;
NewPan := EnsureRange((U + 0.5 - FVis / 2.0) / OneMinus, 0.0, 1.0);
end;
SetPanZoomPan(PanId, NewZ, NewPan);
end;
procedure TRadioController.PanZoomIn(PanId: Integer);
var CurMag: Double; i: Integer;
begin
if not PanDDCActive(PanId) then Exit;
CurMag := ZoomZToMag(FPanZoomF[PanId]);
for i := 0 to High(ZOOM_MAG_LADDER) do
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
begin
PanZoomStepTo(PanId, ZoomMagToZ(ZOOM_MAG_LADDER[i]));
Exit;
end;
end;
procedure TRadioController.PanZoomOut(PanId: Integer);
var CurMag: Double; i: Integer;
begin
if not PanDDCActive(PanId) then Exit;
CurMag := ZoomZToMag(FPanZoomF[PanId]);
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 SetPanZoomPan(PanId, 0.0, 0.5)
else PanZoomStepTo(PanId, ZoomMagToZ(ZOOM_MAG_LADDER[i]));
Exit;
end;
SetPanZoomPan(PanId, 0.0, 0.5);
end;
procedure TRadioController.PanZoomDefault(PanId: Integer);
begin
SetPanZoomPan(PanId, 0.0, 0.5);
end;
function TRadioController.PanZoomFactor(PanId: Integer): Double;
begin
if PanId = 0 then Result := FZoomFactor
else if PanDDCActive(PanId) then Result := FPanZoomF[PanId]
else Result := 0.0;
end;
function TRadioController.PanPanSlider(PanId: Integer): Double;
begin
if PanId = 0 then Result := FPanSlider
else if PanDDCActive(PanId) then Result := FPanPanSl[PanId]
else Result := 0.5;
end;
procedure TRadioController.SetSampleRate(Hz: Integer);
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в