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 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);