diff --git a/MainForm.pas b/MainForm.pas index 49def34..220df1b 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -481,7 +481,8 @@ type procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer); procedure DoWidebandClick(PixelX: Integer; PanelWidth: Integer); // ---- Пан/зум (мышь полосы/сплиттера — внутри TPanafallPanel) ---- - procedure OnPanZoomWindow(AZoom, APan: Double); + function PanOfZoomButton(Sender: TObject): TPanafallPanel; + procedure OnPanZoomWindow(Sender: TObject; AZoom, APan: Double); procedure BtnZoomInClick(Sender: TObject); procedure BtnZoomDefClick(Sender: TObject); procedure BtnZoomOutClick(Sender: TObject); @@ -590,6 +591,8 @@ type procedure AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double); procedure PanClickTune(P: TPanafallPanel; X, W: Integer); // клик по фону пана N procedure PanViewWindow(P: TPanafallPanel; out VC, VS: Double); + procedure SyncPanZoomUI(P: TPanafallPanel); // вьюха+зум-бар ← контроллер + procedure RefreshPanZoom(P: TPanafallPanel); // + флаги/инвалидции procedure SyncPanViews; // центр/span вьюх панов procedure MarkPanDirty(P: TPanafallPanel); // репейнт пана N // Мышь панов N>0 (у пана 0 семантика ниже, PbSpectrum*). @@ -2706,6 +2709,9 @@ begin FPans[i].SetFlagsTheme(T); if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border; 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; if FPanZoomBar <> nil then @@ -4338,25 +4344,74 @@ begin end; end; -procedure TMainForm.OnPanZoomWindow(AZoom, APan: Double); +procedure TMainForm.OnPanZoomWindow(Sender: TObject; AZoom, APan: Double); +var i: Integer; 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). FController.ApplyZoom(AZoom, APan); end; -procedure TMainForm.BtnZoomInClick(Sender: TObject); +function TMainForm.PanOfZoomButton(Sender: TObject): TPanafallPanel; +// Зум-кнопки создаются MakeFlatBtn с Owner = контейнер (PanelRight/окно), а не +// панель — PanFromSender их НЕ находит (клик уходил в зум главного пана). +// Матчим по идентичности контролов. +var i: Integer; 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; procedure TMainForm.BtnZoomDefClick(Sender: TObject); +var P: TPanafallPanel; 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; procedure TMainForm.BtnZoomOutClick(Sender: TObject); +var P: TPanafallPanel; 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; // Спектр @@ -5298,13 +5353,37 @@ begin end; procedure TMainForm.PanViewWindow(P: TPanafallPanel; out VC, VS: Double); -// Видимое окно доп. пана: центр DDC ± rate/2 (пан-зума пока нет). +// Видимое окно доп. пана (с учётом его пан-зума). begin - VC := FController.PanDDCFreq(P.PanId); - VS := FController.PanDDCRateKHz(P.PanId) * 1000.0; + FController.GetPanViewWindow(P.PanId, VC, VS); if VS <= 0 then VS := 384000; 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); // Слайс в доп. пане (режим/полоса копируются с главного, звуковуха default). var Lo, Hi, Id: Integer; @@ -5624,7 +5703,10 @@ begin P := TPanafallPanel.Create(nil, FUseOpenGLSpectrum); P.PanId := PanId; 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.OnSliceClosed := OnPanSliceClosed; P.OnFlagsInvalidate := OnVfoOverlayInvalidate; @@ -5643,6 +5725,9 @@ begin P.WaterfallMouseMove := PanNSpectrumMouseMove; P.WaterfallMouseUp := PanNSpectrumMouseUp; 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 (пан-персональные — позже). P.View.WfAGCEnabled := FController.FWfAGCEnabled; @@ -5753,9 +5838,8 @@ begin begin P := FPans[i]; if P = nil then Continue; - P.View.CenterFreq := FController.PanDDCFreq(i); - P.View.SpanHz := FController.PanDDCRateKHz(i) * 1000.0; - P.View.WDSPReady := FController.FWDSPReady; + SyncPanZoomUI(P); // центр/span вьюхи (с пан-зумом) + зум-бар + P.View.WDSPReady := FController.FWDSPReady; end; // Шапки следуют за частотами (Caption-сеттер — no-op при том же тексте). if PanCount > 1 then UpdatePanHeaders; @@ -5891,7 +5975,7 @@ begin NewHz := Max(FController.BackendCaps.MinFreqHz, Min(FController.BackendCaps.MaxFreqHz, NewHz)); FController.SetPanDDCFreq(P.PanId, NewHz); - P.View.CenterFreq := NewHz; + SyncPanZoomUI(P); // окно вьюхи с учётом зума + freq зум-бара P.LayoutFlags; UpdatePanHeaders; MarkPanDirty(P); diff --git a/PanZoomBar.pas b/PanZoomBar.pas index 425fc8b..a47ebfa 100644 --- a/PanZoomBar.pas +++ b/PanZoomBar.pas @@ -29,7 +29,7 @@ uses LCLIntf, LCLType, IntfGraphics, fpImage, AppTheme, PlatformUtils; type - TPanZoomEvent = procedure(AZoom, APan: Double) of object; + TPanZoomEvent = procedure(Sender: TObject; AZoom, APan: Double) of object; TDragMode = (dmNone, dmBody, dmLeft, dmRight); @@ -196,7 +196,7 @@ begin // Локально обновляем сразу — драг плавный даже до round-trip контроллера. FZoom := Z; FPan := P; if Assigned(FPb) then FPb.Invalidate; - if Assigned(FOnZoomPan) then FOnZoomPan(Z, P); + if Assigned(FOnZoomPan) then FOnZoomPan(Self, Z, P); end; procedure TPanZoomBar.DrawRulerInto(W, H: Integer); diff --git a/PanafallPanel.pas b/PanafallPanel.pas index 8dd563c..7853391 100644 --- a/PanafallPanel.pas +++ b/PanafallPanel.pas @@ -512,13 +512,8 @@ procedure TPanafallPanel.ViewWindow(out VC, VS: Double); begin VC := 0; VS := 0; if FController = nil then Exit; - if FPanId = 0 then - FController.GetViewWindow(VC, VS) - else - begin - VC := FController.PanDDCFreq(FPanId); - VS := FController.PanDDCRateKHz(FPanId) * 1000.0; - end; + // Единая точка видимого окна: PanId=0 → главный зум, паны N → свой пан-зум. + FController.GetPanViewWindow(FPanId, VC, VS); end; procedure TPanafallPanel.PositionHeaderChildren(RW, H: Integer); diff --git a/RadioController.pas b/RadioController.pas index 12e78c8..7dc88ae 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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 в diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 831b54f..39d2276 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -235,6 +235,8 @@ type DispBuf: array of Double; // feed анализатора (DISPLAY_BLOCK_SIZE пар) DispPos: Integer; DispOpen: Boolean; // XCreateAnalyzer(PAN_DISP_BASE+PanId) жив + ZoomFactor: Double; // зум пана 0..1 (0 = весь span), как FZoomFactor + PanSlider: Double; // положение окна 0..1 end; { TWDSPEngine } @@ -646,6 +648,9 @@ type // Zoom/pan: задать коэффициент зума и положение пана и переармировать // RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра). procedure SetZoomPan(AZoom, APan: Double); + // Зум/пан доп. пана (span-clip его анализатора). Видимое окно считает + // контроллер той же формулой (GetPanViewWindow). + procedure SetPanZoomPan(PanId: Integer; AZoom, APan: Double); property ZoomFactor: Double read FZoomFactor; property PanSlider: Double read FPanSlider; property ViewLowHz: Double read FViewLowHz; @@ -1383,14 +1388,17 @@ end; procedure TWDSPEngine.ApplyPanAnalyzerSettings(PanId: Integer); // Параметризованная копия ApplyRXAnalyzerSettings для analyzer'а доп. пана: -// rate пана, БЕЗ span-clip/zoom (пан-зум — этап 3.3+), FFT/детектор/усреднение -// пока общие с главным. Вызывать под FSliceLock. +// rate пана + СВОЙ span-clip/zoom (ZoomFactor/PanSlider записи пана); +// FFT/детектор/усреднение пока общие с главным. Вызывать под FSliceLock. var AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels, Disp: Integer; Backmult, WfBackmult: Double; EffFPS: Double; + Bins, SpanClipL, SpanClipH, ZWidth: Integer; + ZoomSlider: Double; const KEEP_TIME_SEC = 0.10; + ZOOM_LIMIT = 100.0; // как у главного (ApplyRXAnalyzerSettings) begin if not FPans[PanId].DispOpen then Exit; Disp := PAN_DISP_BASE + PanId; @@ -1405,8 +1413,17 @@ begin AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult); 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, - FWindowType, 14.0, Overlap, 0, 0, 0, DisplayPixels, + FWindowType, 14.0, Overlap, 0, SpanClipL, SpanClipH, DisplayPixels, 1, 0, 0.0, 0.0, MaxW); SetDisplayDetectorMode(Disp, 0, FSpecDetector); SetDisplayAverageMode (Disp, 0, FSpecAvgMode); @@ -2292,6 +2309,8 @@ begin FPans[PanId].AccPos := 0; FPans[PanId].DispPos := 0; FPans[PanId].DispOpen := False; + FPans[PanId].ZoomFactor := 0.0; // без зума + FPans[PanId].PanSlider := 0.5; FPans[PanId].Active := True; AllocSliceBuffers; // FSliceIn/Out под новый макс. блок if FAnalyzerOpen then CreatePanAnalyzer(PanId); @@ -2361,6 +2380,27 @@ begin 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; begin Result := (PanId >= 1) and (PanId < MAX_PANS) and FPans[PanId].Active; diff --git a/doc/SLICES_PLAN.md b/doc/SLICES_PLAN.md index eac8195..1269638 100644 --- a/doc/SLICES_PLAN.md +++ b/doc/SLICES_PLAN.md @@ -545,6 +545,34 @@ RATEPROBE удалены.** Наблюдение юзера: смена rate Г паузы >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.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).