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
+98 -14
View File
@@ -481,7 +481,8 @@ type
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer); procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
procedure DoWidebandClick(PixelX: Integer; PanelWidth: Integer); procedure DoWidebandClick(PixelX: Integer; PanelWidth: Integer);
// ---- Пан/зум (мышь полосы/сплиттера — внутри TPanafallPanel) ---- // ---- Пан/зум (мышь полосы/сплиттера — внутри TPanafallPanel) ----
procedure OnPanZoomWindow(AZoom, APan: Double); function PanOfZoomButton(Sender: TObject): TPanafallPanel;
procedure OnPanZoomWindow(Sender: TObject; AZoom, APan: Double);
procedure BtnZoomInClick(Sender: TObject); procedure BtnZoomInClick(Sender: TObject);
procedure BtnZoomDefClick(Sender: TObject); procedure BtnZoomDefClick(Sender: TObject);
procedure BtnZoomOutClick(Sender: TObject); procedure BtnZoomOutClick(Sender: TObject);
@@ -590,6 +591,8 @@ type
procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double); procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N
procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double); procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
procedure SyncPanZoomUI(P: TPanafallPanel); // вьюха+зум-бар ← контроллер
procedure RefreshPanZoom(P: TPanafallPanel); // + флаги/инвалидции
procedure SyncPanViews; // центр/span вьюх панов procedure SyncPanViews; // центр/span вьюх панов
procedure MarkPanDirty(P: TPanafallPanel); // репейнт пана N procedure MarkPanDirty(P: TPanafallPanel); // репейнт пана N
// Мышь панов N>0 (у пана 0 семантика ниже, PbSpectrum*). // Мышь панов N>0 (у пана 0 семантика ниже, PbSpectrum*).
@@ -2706,6 +2709,9 @@ begin
FPans[i].SetFlagsTheme(T); FPans[i].SetFlagsTheme(T);
if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border; if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border;
if FPanFloatForm[i] <> nil then FPanFloatForm[i].Color := T.BG; if FPanFloatForm[i] <> nil then FPanFloatForm[i].Color := T.BG;
// Зум-бар пана N (пан 0 — через алиас FPanZoomBar ниже).
if FPans[i].ZoomBar <> nil then FPans[i].ZoomBar.SetTheme(T);
if FPans[i].PbPanZoom <> nil then FPans[i].PbPanZoom.Color := T.SliderBG;
end; end;
end; end;
if FPanZoomBar <> nil then if FPanZoomBar <> nil then
@@ -4338,25 +4344,74 @@ begin
end; end;
end; end;
procedure TMainForm.OnPanZoomWindow(AZoom, APan: Double); procedure TMainForm.OnPanZoomWindow(Sender: TObject; AZoom, APan: Double);
var i: Integer;
begin begin
// Чей бар? Паны N — свой зум (пан-локальный, без band-персиста);
// иначе пан 0 — контроллерный зум (rfZoom обновит UI).
for i := 1 to MAX_PANS - 1 do
if (FPans[i] <> nil) and (TObject(FPans[i].ZoomBar) = Sender) then
begin
FController.SetPanZoomPan(i, AZoom, APan);
RefreshPanZoom(FPans[i]);
Exit;
end;
// Окно-селектор задаёт и зум, и пан напрямую (без якоря на VFO). // Окно-селектор задаёт и зум, и пан напрямую (без якоря на VFO).
FController.ApplyZoom(AZoom, APan); FController.ApplyZoom(AZoom, APan);
end; end;
procedure TMainForm.BtnZoomInClick(Sender: TObject); function TMainForm.PanOfZoomButton(Sender: TObject): TPanafallPanel;
// Зум-кнопки создаются MakeFlatBtn с Owner = контейнер (PanelRight/окно), а не
// панель — PanFromSender их НЕ находит (клик уходил в зум главного пана).
// Матчим по идентичности контролов.
var i: Integer;
begin begin
FController.ZoomIn; Result := nil;
for i := 0 to MAX_PANS - 1 do
if FPans[i] <> nil then
if (Sender = TObject(FPans[i].BtnZoomIn)) or
(Sender = TObject(FPans[i].BtnZoomDef)) or
(Sender = TObject(FPans[i].BtnZoomOut)) then
Exit(FPans[i]);
end;
procedure TMainForm.BtnZoomInClick(Sender: TObject);
var P: TPanafallPanel;
begin
P := PanOfZoomButton(Sender);
if (P <> nil) and (P.PanId > 0) then
begin
FController.PanZoomIn(P.PanId);
RefreshPanZoom(P);
end
else
FController.ZoomIn;
end; end;
procedure TMainForm.BtnZoomDefClick(Sender: TObject); procedure TMainForm.BtnZoomDefClick(Sender: TObject);
var P: TPanafallPanel;
begin begin
FController.ZoomDefault; P := PanOfZoomButton(Sender);
if (P <> nil) and (P.PanId > 0) then
begin
FController.PanZoomDefault(P.PanId);
RefreshPanZoom(P);
end
else
FController.ZoomDefault;
end; end;
procedure TMainForm.BtnZoomOutClick(Sender: TObject); procedure TMainForm.BtnZoomOutClick(Sender: TObject);
var P: TPanafallPanel;
begin begin
FController.ZoomOut; P := PanOfZoomButton(Sender);
if (P <> nil) and (P.PanId > 0) then
begin
FController.PanZoomOut(P.PanId);
RefreshPanZoom(P);
end
else
FController.ZoomOut;
end; end;
// Спектр // Спектр
@@ -5298,13 +5353,37 @@ begin
end; end;
procedure TMainForm.PanViewWindow(P: TPanafallPanel; out VC, VS: Double); procedure TMainForm.PanViewWindow(P: TPanafallPanel; out VC, VS: Double);
// Видимое окно доп. пана: центр DDC ± rate/2 (пан-зума пока нет). // Видимое окно доп. пана (с учётом его пан-зума).
begin begin
VC := FController.PanDDCFreq(P.PanId); FController.GetPanViewWindow(P.PanId, VC, VS);
VS := FController.PanDDCRateKHz(P.PanId) * 1000.0;
if VS <= 0 then VS := 384000; if VS <= 0 then VS := 384000;
end; end;
procedure TMainForm.SyncPanZoomUI(P: TPanafallPanel);
// Вьюха и зум-бар пана ← контроллер (после смены зума/центра/rate пана).
var VC, VS: Double;
begin
if (P = nil) or (P.PanId = 0) then Exit;
FController.GetPanViewWindow(P.PanId, VC, VS);
P.View.CenterFreq := VC;
P.View.SpanHz := VS;
P.ZoomBar.SetFreq(FController.PanDDCFreq(P.PanId),
FController.PanDDCRateKHz(P.PanId) * 1000.0);
P.ZoomBar.SetState(FController.PanZoomFactor(P.PanId),
FController.PanPanSlider(P.PanId));
if P.PbPanZoom <> nil then P.PbPanZoom.Invalidate;
end;
procedure TMainForm.RefreshPanZoom(P: TPanafallPanel);
// Полное обновление пана после смены зума: окно вьюхи, бар, флаги, спектр.
begin
if P = nil then Exit;
SyncPanZoomUI(P);
P.LayoutFlags;
MarkPanDirty(P);
if P.PbWaterfall <> nil then P.PbWaterfall.Invalidate;
end;
procedure TMainForm.AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double); procedure TMainForm.AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
// Слайс в доп. пане (режим/полоса копируются с главного, звуковуха default). // Слайс в доп. пане (режим/полоса копируются с главного, звуковуха default).
var Lo, Hi, Id: Integer; var Lo, Hi, Id: Integer;
@@ -5624,7 +5703,10 @@ begin
P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum); P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
P.PanId := PanId; P.PanId := PanId;
P.Controller := FController; P.Controller := FController;
P.ShowZoomRow := False; // пан-зум доп. панов — этап 3.3+ P.ShowZoomRow := True; // пан-зум пана: свой бар/кнопки (диспетчер по Sender)
P.ZoomInClick := BtnZoomInClick; // до Build — кнопки создаются в Build
P.ZoomDefClick := BtnZoomDefClick;
P.ZoomOutClick := BtnZoomOutClick;
P.OnWireFlag := WireOverlayEvents; P.OnWireFlag := WireOverlayEvents;
P.OnSliceClosed := OnPanSliceClosed; P.OnSliceClosed := OnPanSliceClosed;
P.OnFlagsInvalidate := OnVfoOverlayInvalidate; P.OnFlagsInvalidate := OnVfoOverlayInvalidate;
@@ -5643,6 +5725,9 @@ begin
P.WaterfallMouseMove := PanNSpectrumMouseMove; P.WaterfallMouseMove := PanNSpectrumMouseMove;
P.WaterfallMouseUp := PanNSpectrumMouseUp; P.WaterfallMouseUp := PanNSpectrumMouseUp;
P.Build(PanelRight, Max(1, FController.FSpecMSAA)); P.Build(PanelRight, Max(1, FController.FSpecMSAA));
P.ZoomBar.OnZoomPan := OnPanZoomWindow; // drag окна-селектора
P.ZoomBar.SetFreq(Freq, FController.PanDDCRateKHz(PanId) * 1000.0);
P.ZoomBar.SetState(0.0, 0.5);
// Параметры отображения — с главного view (пан-персональные — позже). // Параметры отображения — с главного view (пан-персональные — позже).
P.View.WfAGCEnabled := FController.FWfAGCEnabled; P.View.WfAGCEnabled := FController.FWfAGCEnabled;
@@ -5753,9 +5838,8 @@ begin
begin begin
P := FPans[i]; P := FPans[i];
if P = nil then Continue; if P = nil then Continue;
P.View.CenterFreq := FController.PanDDCFreq(i); SyncPanZoomUI(P); // центр/span вьюхи (с пан-зумом) + зум-бар
P.View.SpanHz := FController.PanDDCRateKHz(i) * 1000.0; P.View.WDSPReady := FController.FWDSPReady;
P.View.WDSPReady := FController.FWDSPReady;
end; end;
// Шапки следуют за частотами (Caption-сеттер — no-op при том же тексте). // Шапки следуют за частотами (Caption-сеттер — no-op при том же тексте).
if PanCount > 1 then UpdatePanHeaders; if PanCount > 1 then UpdatePanHeaders;
@@ -5891,7 +5975,7 @@ begin
NewHz := Max(FController.BackendCaps.MinFreqHz, NewHz := Max(FController.BackendCaps.MinFreqHz,
Min(FController.BackendCaps.MaxFreqHz, NewHz)); Min(FController.BackendCaps.MaxFreqHz, NewHz));
FController.SetPanDDCFreq(P.PanId, NewHz); FController.SetPanDDCFreq(P.PanId, NewHz);
P.View.CenterFreq := NewHz; SyncPanZoomUI(P); // окно вьюхи с учётом зума + freq зум-бара
P.LayoutFlags; P.LayoutFlags;
UpdatePanHeaders; UpdatePanHeaders;
MarkPanDirty(P); MarkPanDirty(P);
+2 -2
View File
@@ -29,7 +29,7 @@ uses
LCLIntf, LCLType, IntfGraphics, fpImage, AppTheme, PlatformUtils; LCLIntf, LCLType, IntfGraphics, fpImage, AppTheme, PlatformUtils;
type type
TPanZoomEvent = procedure(AZoom, APan: Double) of object; TPanZoomEvent = procedure(Sender: TObject; AZoom, APan: Double) of object;
TDragMode = (dmNone, dmBody, dmLeft, dmRight); TDragMode = (dmNone, dmBody, dmLeft, dmRight);
@@ -196,7 +196,7 @@ begin
// Локально обновляем сразу — драг плавный даже до round-trip контроллера. // Локально обновляем сразу — драг плавный даже до round-trip контроллера.
FZoom := Z; FPan := P; FZoom := Z; FPan := P;
if Assigned(FPb) then FPb.Invalidate; if Assigned(FPb) then FPb.Invalidate;
if Assigned(FOnZoomPan) then FOnZoomPan(Z, P); if Assigned(FOnZoomPan) then FOnZoomPan(Self, Z, P);
end; end;
procedure TPanZoomBar.DrawRulerInto(W, H: Integer); procedure TPanZoomBar.DrawRulerInto(W, H: Integer);
+2 -7
View File
@@ -512,13 +512,8 @@ procedure TPanafallPanel.ViewWindow(out VC, VS: Double);
begin begin
VC := 0; VS := 0; VC := 0; VS := 0;
if FController = nil then Exit; if FController = nil then Exit;
if FPanId = 0 then // Единая точка видимого окна: PanId=0 → главный зум, паны N → свой пан-зум.
FController.GetViewWindow(VC, VS) FController.GetPanViewWindow(FPanId, VC, VS);
else
begin
VC := FController.PanDDCFreq(FPanId);
VS := FController.PanDDCRateKHz(FPanId) * 1000.0;
end;
end; end;
procedure TPanafallPanel.PositionHeaderChildren(RW, H: Integer); procedure TPanafallPanel.PositionHeaderChildren(RW, H: Integer);
+139 -10
View File
@@ -167,6 +167,7 @@ type
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) --- // --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
procedure PanZoomStepTo(PanId: Integer; NewZ: Double); // зум с якорем на центре окна
function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq) function PanCenterHz(PanId: Integer): Double; // центр пана (0 = FCenterFreq)
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг) procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
@@ -353,6 +354,8 @@ type
FPanFreqHz: array[1..MAX_PANS-1] of Double; FPanFreqHz: array[1..MAX_PANS-1] of Double;
FPanRateKHz: array[1..MAX_PANS-1] of Word; FPanRateKHz: array[1..MAX_PANS-1] of Word;
FPanADC: array[1..MAX_PANS-1] of Byte; // ADC-источник пана (3.4) 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) FMainADCSrc: Byte; // ADC главного DDC (пан 0)
FDDCPanMap: array[0..MAX_DDCS-1] of Integer; FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
FLastDDCSeq: LongWord; // последний seq (из сетевого потока) FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
@@ -581,6 +584,15 @@ type
// N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1]. // N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1].
function PanADC(PanId: Integer): Byte; function PanADC(PanId: Integer): Byte;
function SetPanADC(PanId: Integer; ADC: Byte): Boolean; 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 на // Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain. // 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
@@ -1189,6 +1201,8 @@ begin
FPanFreqHz[i] := 0; FPanFreqHz[i] := 0;
FPanRateKHz[i] := 0; FPanRateKHz[i] := 0;
FPanADC[i] := 0; FPanADC[i] := 0;
FPanZoomF[i] := 0.0;
FPanPanSl[i] := 0.5;
end; end;
for i := 0 to MAX_DDCS - 1 do for i := 0 to MAX_DDCS - 1 do
FDDCPanMap[i] := -1; FDDCPanMap[i] := -1;
@@ -1235,6 +1249,8 @@ begin
FPanFreqHz[PanId] := FreqHz; FPanFreqHz[PanId] := FreqHz;
FPanRateKHz[PanId] := RateKHz; FPanRateKHz[PanId] := RateKHz;
FPanADC[PanId] := ADCSource; FPanADC[PanId] := ADCSource;
FPanZoomF[PanId] := 0.0;
FPanPanSl[PanId] := 0.5;
FDDCPanMap[ddc] := PanId; FDDCPanMap[ddc] := PanId;
FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource); FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource);
Result := True; Result := True;
@@ -1282,6 +1298,8 @@ begin
FPanFreqHz[PanId] := 0; FPanFreqHz[PanId] := 0;
FPanRateKHz[PanId] := 0; FPanRateKHz[PanId] := 0;
FPanADC[PanId] := 0; FPanADC[PanId] := 0;
FPanZoomF[PanId] := 0.0;
FPanPanSl[PanId] := 0.5;
for i := 0 to MAX_SLICES - 1 do for i := 0 to MAX_SLICES - 1 do
if FSlices[i].Active and (FSlices[i].PanId = PanId) then if FSlices[i].Active and (FSlices[i].PanId = PanId) then
RemoveSlice(FSlices[i].Id); RemoveSlice(FSlices[i].Id);
@@ -3041,29 +3059,35 @@ begin Result := FZoomFactor; end;
function TRadioController.PanSlider: Double; function TRadioController.PanSlider: Double;
begin Result := FPanSlider; end; 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, Гц) с учётом зума. Формула совпадает с расчётом
// span-clip в WDSPEngine.ApplyRXAnalyzerSettings (bw=FSpanHz, clp=0), поэтому // span-clip в WDSPEngine.Apply{RX,Pan}AnalyzerSettings (bw=BaseS, clp=0),
// оверлеи точно ложатся на пиксели. Без зума → базовые центр/span. // поэтому оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
var var
FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double; FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double;
begin begin
if (FZoomFactor <= 0.0) or (FSpanHz <= 0.0) then if (AZoom <= 0.0) or (BaseS <= 0.0) then
begin begin
ACenter := FCenterFreq; ACenter := BaseC;
ASpan := FSpanHz; ASpan := BaseS;
Exit; Exit;
end; 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 после зума FVis := 1.0 - OneMinusFVis; // доля span после зума
if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0; // ZOOM_LIMIT if FVis < 1.0 / 100.0 then FVis := 1.0 / 100.0; // ZOOM_LIMIT
VisSpan := FVis * FSpanHz; VisSpan := FVis * BaseS;
LowEdgeOff := -FSpanHz / 2.0 + EnsureRange(FPanSlider, 0.0, 1.0) * OneMinusFVis * FSpanHz; LowEdgeOff := -BaseS / 2.0 + EnsureRange(APan, 0.0, 1.0) * OneMinusFVis * BaseS;
CenterOff := LowEdgeOff + VisSpan / 2.0; CenterOff := LowEdgeOff + VisSpan / 2.0;
ACenter := FCenterFreq + CenterOff; ACenter := BaseC + CenterOff;
ASpan := VisSpan; ASpan := VisSpan;
end; 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; function TRadioController.AnchoredPan(AZoom: Double): Double;
// Положение пана, при котором видимое окно центрировано на маркере активного // Положение пана, при котором видимое окно центрировано на маркере активного
// VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер // VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер
@@ -3156,6 +3180,111 @@ begin
ApplyZoom(0.0, 0.5); ApplyZoom(0.0, 0.5);
end; 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); procedure TRadioController.SetSampleRate(Hz: Integer);
// Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate. // Смена частоты дискретизации DDC. В этом приложении span ≡ sample rate.
// Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в // Несёт: пересоздание WDSP-канала (Close+FlushQueue+Open), новый rate в
+43 -3
View File
@@ -235,6 +235,8 @@ type
DispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар) DispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар)
DispPos: Integer; DispPos: Integer;
DispOpen: Boolean; // XCreateAnalyzer(PAN_DISP_BASE+PanId) жив DispOpen: Boolean; // XCreateAnalyzer(PAN_DISP_BASE+PanId) жив
ZoomFactor: Double; // зум пана 0..1 (0 = весь span), как FZoomFactor
PanSlider: Double; // положение окна 0..1
end; end;
{ TWDSPEngine } { TWDSPEngine }
@@ -646,6 +648,9 @@ type
// Zoom/pan: задать коэффициент зума и положение пана и переармировать // Zoom/pan: задать коэффициент зума и положение пана и переармировать
// RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра). // RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра).
procedure SetZoomPan(AZoom, APan: Double); procedure SetZoomPan(AZoom, APan: Double);
// Зум/пан доп. пана (span-clip его анализатора). Видимое окно считает
// контроллер той же формулой (GetPanViewWindow).
procedure SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
property ZoomFactor: Double read FZoomFactor; property ZoomFactor: Double read FZoomFactor;
property PanSlider: Double read FPanSlider; property PanSlider: Double read FPanSlider;
property ViewLowHz: Double read FViewLowHz; property ViewLowHz: Double read FViewLowHz;
@@ -1383,14 +1388,17 @@ end;
procedure TWDSPEngine.ApplyPanAnalyzerSettings(PanId: Integer); procedure TWDSPEngine.ApplyPanAnalyzerSettings(PanId: Integer);
// Параметризованная копия ApplyRXAnalyzerSettings для analyzer'а доп. пана: // Параметризованная копия ApplyRXAnalyzerSettings для analyzer'а доп. пана:
// rate пана, БЕЗ span-clip/zoom (пан-зум — этап 3.3+), FFT/детектор/усреднение // rate пана + СВОЙ span-clip/zoom (ZoomFactor/PanSlider записи пана);
// пока общие с главным. Вызывать под FSliceLock. // FFT/детектор/усреднение пока общие с главным. Вызывать под FSliceLock.
var var
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer; AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer;
Backmult, WfBackmult: Double; Backmult, WfBackmult: Double;
EffFPS: Double; EffFPS: Double;
Bins, SpanClipL, SpanClipH, ZWidth: Integer;
ZoomSlider: Double;
const const
KEEP_TIME_SEC = 0.10; KEEP_TIME_SEC = 0.10;
ZOOM_LIMIT = 100.0; // как у главного (ApplyRXAnalyzerSettings)
begin begin
if not FPans[PanId].DispOpen then Exit; if not FPans[PanId].DispOpen then Exit;
Disp := PAN_DISP_BASE + PanId; Disp := PAN_DISP_BASE + PanId;
@@ -1405,8 +1413,17 @@ begin
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult); AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult); AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
// Span-clip зума пана — та же математика, что у главного (clp=0, зум только
// клипует края FFT). Контроллер считает видимое окно той же формулой.
Bins := AnalyzerFFT;
ZoomSlider := Log10(9.0 * EnsureRange(FPans[PanId].ZoomFactor, 0.0, 1.0) + 1.0);
ZWidth := Round(Bins * (1.0 - (1.0 - 1.0 / ZOOM_LIMIT) * ZoomSlider));
ZWidth := EnsureRange(ZWidth, 1, Bins);
SpanClipL := Floor(EnsureRange(FPans[PanId].PanSlider, 0.0, 1.0) * (Bins - ZWidth));
SpanClipH := Bins - ZWidth - SpanClipL;
SetAnalyzer(Disp, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE, SetAnalyzer(Disp, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
FWindowType, 14.0, Overlap, 0, 0, 0, DisplayPixels, FWindowType, 14.0, Overlap, 0, SpanClipL, SpanClipH, DisplayPixels,
1, 0, 0.0, 0.0, MaxW); 1, 0, 0.0, 0.0, MaxW);
SetDisplayDetectorMode(Disp, 0, FSpecDetector); SetDisplayDetectorMode(Disp, 0, FSpecDetector);
SetDisplayAverageMode (Disp, 0, FSpecAvgMode); SetDisplayAverageMode (Disp, 0, FSpecAvgMode);
@@ -2292,6 +2309,8 @@ begin
FPans[PanId].AccPos := 0; FPans[PanId].AccPos := 0;
FPans[PanId].DispPos := 0; FPans[PanId].DispPos := 0;
FPans[PanId].DispOpen := False; FPans[PanId].DispOpen := False;
FPans[PanId].ZoomFactor := 0.0; // без зума
FPans[PanId].PanSlider := 0.5;
FPans[PanId].Active := True; FPans[PanId].Active := True;
AllocSliceBuffers; // FSliceIn/Out под новый макс. блок AllocSliceBuffers; // FSliceIn/Out под новый макс. блок
if FAnalyzerOpen then CreatePanAnalyzer(PanId); if FAnalyzerOpen then CreatePanAnalyzer(PanId);
@@ -2361,6 +2380,27 @@ begin
end; end;
end; end;
procedure TWDSPEngine.SetPanZoomPan(PanId: Integer; AZoom, APan: Double);
// Переармирует анализатор пана под новый span-clip. No-op guard обязателен:
// SetAnalyzer сбрасывает FFT-историю (урок SetZoomPan главного, dae0c19).
var NZ, NP: Double;
begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
NZ := EnsureRange(AZoom, 0.0, 1.0);
NP := EnsureRange(APan, 0.0, 1.0);
FSliceLock.Enter;
try
if not FPans[PanId].Active then Exit;
if (Abs(NZ - FPans[PanId].ZoomFactor) < 1E-9) and
(Abs(NP - FPans[PanId].PanSlider) < 1E-9) then Exit;
FPans[PanId].ZoomFactor := NZ;
FPans[PanId].PanSlider := NP;
ApplyPanAnalyzerSettings(PanId); // внутри гейт по DispOpen
finally
FSliceLock.Leave;
end;
end;
function TWDSPEngine.PanActive(PanId: Integer): Boolean; function TWDSPEngine.PanActive(PanId: Integer): Boolean;
begin begin
Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active; Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active;
+28
View File
@@ -545,6 +545,34 @@ RATEPROBE удалены.** Наблюдение юзера: смена rate Г
паузы >41мс, main-аудио/слайс пана 0/слайс пана 1 живы и слышат тон после паузы >41мс, main-аудио/слайс пана 0/слайс пана 1 живы и слышат тон после
смены. Оба бинарника собираются. смены. Оба бинарника собираются.
**СТАТУС «пан-зум панов N» (2026-07-12): ГОТОВО — смоук panzoomtest ALL PASS,
юзер подтвердил на экране, коммит cb6bdfa.**
- Движок: `TPanChan.ZoomFactor/PanSlider`, span-clip в
`ApplyPanAnalyzerSettings` (та же математика, что у главного; ZOOM_LIMIT 100),
`SetPanZoomPan` (no-op guard — SetAnalyzer сбрасывает FFT-историю, урок
dae0c19). Зум переживает смену rate пана (SetPanRate пересоздаёт анализатор
из записи пана).
- Контроллер: `FPanZoomF/FPanPanSl` (сессия пана, сброс в Add/RemovePanDDC —
band-персиста у панов нет), формула окна вынесена в юнит-хелпер
`ViewWindowFor` (GetViewWindow главного теперь тоже через него),
`GetPanViewWindow(0)` делегирует главному; `PanZoomIn/Out` по той же
лестнице ZOOM_MAG_LADDER, якорь = ЦЕНТР текущего окна (`PanZoomStepTo`,
у панов нет VFO-якоря главного), `PanZoomDefault`, геттеры.
- UI: `TPanZoomEvent` получил Sender (PanZoomBar), `OnPanZoomWindow`
диспетчеризует бар→пан; кнопки −/⌂/+ общие (`PanFromSender`); у панов N
включён ShowZoomRow, бар проводится в CreatePanadapter; единые
`SyncPanZoomUI/RefreshPanZoom` (вьюха+бар ← контроллер) используются в
SyncPanViews и drag-ретюне; `TPanafallPanel.ViewWindow` и `PanViewWindow`
`GetPanViewWindow` ⇒ вся мышь/флаги панов автоматически зум-корректны.
Темизация баров панов N в ApplyDarkTheme.
- Смоук panzoomtest.pas: пик тона +50к на пане 384к совпадает с формулой окна
при зуме 0 / 0.5+пан 0.7 / сбросе (±0.6%).
- ⚠Фикс по первому экранному тесту: кнопки −/⌂/+ панов N рулили ГЛАВНЫМ зумом —
MakeFlatBtn создаёт кнопки с Owner=контейнер (не панель), PanFromSender их
не находит. Диспетчер кнопок = PanOfZoomButton (матч по идентичности
BtnZoomIn/Def/Out всех панов). Урок: PanFromSender работает только для
контролов с Owner=панель (TLabel шапки — да, MakeFlatBtn-кнопки — нет).
### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе ### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе
Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён). Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).