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
+43 -3
View File
@@ -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;