mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(spectrum): zoom + pan для спектра и водопада (WDSP span-clip)
Зум через нативный span-clip WDSP (fscLin/fscHin в SetAnalyzer): clp=0, видимое окно сужается клипом, при зуме 0 полоса == sample rate (старое поведение сохранено). Конвенция как в Thetis: ZoomFactor 0=без зума..1=макс. - WDSPEngine: SetZoomPan + расчёт span-clip, ViewLowHz/ViewHighHz. - RadioController: GetViewWindow (единая истина видимого окна для оверлеев и мыши), ApplyZoom, ZoomIn/Out по лестнице кратностей, ZoomDefault. Якорь зума кнопками — на маркере активного VFO. rfZoom. - Per-band персист зума/пана (TBandSettings + Save/Load/Restore). - MainForm: маршрутизация оверлеев/кликов через GetViewWindow (покрывает CPU и OpenGL), резерв строки PANZOOM_H в layout + сплиттере, rfZoom-рендер. - WebAdapter: web-спектр/наведение на view-окно (делит те же WDSP-пиксели). - PanZoomBar (новый): нижняя полоса = линейка полного диапазона (частоты + деления), поверх окно-селектор «стекло» (полупрозрачное, объёмная фаска). Середина=пан, края=зум, клик мимо=центрировать, двойной клик=сброс. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+175
-12
@@ -33,6 +33,7 @@ uses
|
||||
WebServer, WebAdapter,
|
||||
CATAdapter,
|
||||
SpectrumView, SpectrumViewOpengl,
|
||||
PanZoomBar,
|
||||
WidebandView,
|
||||
StatusBar,
|
||||
PlatformUtils,
|
||||
@@ -296,6 +297,12 @@ type
|
||||
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
||||
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
||||
PbWaterfall: TControl;
|
||||
// ---- Панель пана/зума под водопадом ----
|
||||
PbPanZoom: TPaintBox; // полоса-ползунок пана зума
|
||||
FPanZoomBar: TPanZoomBar;
|
||||
BtnZoomIn: TFlatButton;
|
||||
BtnZoomDef: TFlatButton;
|
||||
BtnZoomOut: TFlatButton;
|
||||
|
||||
// ---- Status bar ----
|
||||
StatusPanel: TMainStatusBar;
|
||||
@@ -437,6 +444,19 @@ type
|
||||
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||||
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||||
procedure DoWidebandClick(PixelX: Integer; PanelWidth: Integer);
|
||||
// ---- Пан/зум ----
|
||||
procedure PbPanZoomMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PbPanZoomMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
procedure PbPanZoomMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PbPanZoomDblClick(Sender: TObject);
|
||||
procedure OnPanZoomWindow(AZoom, APan: Double);
|
||||
procedure BtnZoomInClick(Sender: TObject);
|
||||
procedure BtnZoomDefClick(Sender: TObject);
|
||||
procedure BtnZoomOutClick(Sender: TObject);
|
||||
procedure PositionPanZoomBar(BottomY, RW, H: Integer; AVisible: Boolean);
|
||||
procedure UpdatePanZoomBar;
|
||||
procedure BtnVfoSwapClick(Sender: TObject);
|
||||
procedure BtnVfoACopyBClick(Sender: TObject);
|
||||
procedure BtnVfoBCopyAClick(Sender: TObject);
|
||||
@@ -1068,6 +1088,7 @@ begin
|
||||
FController.FNetwork.Disconnect;
|
||||
FreeAndNil(FSpecView);
|
||||
FreeAndNil(FWidebandView);
|
||||
FreeAndNil(FPanZoomBar);
|
||||
|
||||
// Сохраняем настройки при закрытии
|
||||
if FController.FDevConnected then
|
||||
@@ -1370,6 +1391,21 @@ begin
|
||||
// актуальную ширину спектра (UI-зависимая величина).
|
||||
if FController.FWDSPReady then
|
||||
FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||||
UpdatePanZoomBar;
|
||||
end;
|
||||
rfZoom:
|
||||
begin
|
||||
// Зум/пан изменились: окно просмотра, шаг линейки и полоса фильтра
|
||||
// следуют за span → пересинк + полная перерисовка спектра и водопада.
|
||||
FSpecView.InvalidateRulerCache;
|
||||
SyncSpecViewFreq;
|
||||
UpdatePanZoomBar;
|
||||
PositionVfoOverlay;
|
||||
FSpecView.DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
FSpecView.DrawWaterfall;
|
||||
if PbWaterfall <> nil then PbWaterfall.Invalidate;
|
||||
if PbRuler <> nil then PbRuler.Invalidate;
|
||||
end;
|
||||
rfFMRpt:
|
||||
begin
|
||||
@@ -2194,8 +2230,25 @@ begin
|
||||
end;
|
||||
PbWaterfall.Parent := PanelRight;
|
||||
|
||||
// ---- Панель пана/зума под водопадом ----
|
||||
// Кнопки создаём с дефолтными цветами; финальная тема — в ApplyDarkTheme.
|
||||
FPanZoomBar := TPanZoomBar.Create;
|
||||
FPanZoomBar.OnZoomPan := OnPanZoomWindow;
|
||||
PbPanZoom := TPaintBox.Create(Self);
|
||||
PbPanZoom.Parent := PanelRight;
|
||||
PbPanZoom.OnPaint := FPanZoomBar.Paint;
|
||||
PbPanZoom.OnMouseDown := PbPanZoomMouseDown;
|
||||
PbPanZoom.OnMouseMove := PbPanZoomMouseMove;
|
||||
PbPanZoom.OnMouseUp := PbPanZoomMouseUp;
|
||||
PbPanZoom.OnDblClick := PbPanZoomDblClick;
|
||||
FPanZoomBar.PaintBox := PbPanZoom;
|
||||
BtnZoomOut := MakeFlatBtn(PanelRight, '−', 0, 0, 10, 10, BtnZoomOutClick);
|
||||
BtnZoomDef := MakeFlatBtn(PanelRight, '⌂', 0, 0, 10, 10, BtnZoomDefClick);
|
||||
BtnZoomIn := MakeFlatBtn(PanelRight, '+', 0, 0, 10, 10, BtnZoomInClick);
|
||||
|
||||
// Initial layout
|
||||
ResizeSpectrumPanels;
|
||||
UpdatePanZoomBar;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
@@ -2241,7 +2294,9 @@ begin
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
@@ -2279,7 +2334,9 @@ begin
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
@@ -2483,12 +2540,19 @@ var
|
||||
RW, RH, SH, WH, TopOff, WideH, WideSpecH: Integer;
|
||||
AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
PANZOOM_H: Integer;
|
||||
ShowPZ: Boolean;
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if PanelRight = nil then Exit;
|
||||
if Assigned(FSpecView) then SyncSpecViewFreq;
|
||||
RW := PanelRight.ClientWidth;
|
||||
RH := PanelRight.ClientHeight;
|
||||
// Резервируем низ под панель пана/зума (видна, если виден спектр или водопад).
|
||||
PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
ShowPZ := FController.FShowSpectrum or FController.FShowWaterfall;
|
||||
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||||
if ShowPZ then RH := RH - PANZOOM_H;
|
||||
UpdateWidebandFrequencyView;
|
||||
WideH := 0;
|
||||
WideSpecH := 0;
|
||||
@@ -2712,6 +2776,14 @@ begin
|
||||
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active);
|
||||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||
if FStepDropDown <> nil then FStepDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||
if BtnZoomIn <> nil then StyleButton(BtnZoomIn, False);
|
||||
if BtnZoomDef <> nil then StyleButton(BtnZoomDef, False);
|
||||
if BtnZoomOut <> nil then StyleButton(BtnZoomOut, False);
|
||||
if FPanZoomBar <> nil then
|
||||
begin
|
||||
FPanZoomBar.SetTheme(T);
|
||||
if PbPanZoom <> nil then PbPanZoom.Color := T.SliderBG;
|
||||
end;
|
||||
if BtnChannels <> nil then StyleButton(BtnChannels, False);
|
||||
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||
StyleButton(BtnNR, BtnNR.Active);
|
||||
@@ -2995,12 +3067,15 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.SyncSpecViewFreq;
|
||||
var ViewCenter, ViewSpan: Double;
|
||||
begin
|
||||
// Видимое окно с учётом зума (центр+span). Без зума == FCenterFreq/FSpanHz.
|
||||
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||
FSpecView.VfoA := FController.FVfoA;
|
||||
FSpecView.VfoB := FController.FVfoB;
|
||||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||||
FSpecView.CenterFreq := FController.FCenterFreq;
|
||||
FSpecView.SpanHz := FController.FSpanHz;
|
||||
FSpecView.CenterFreq := ViewCenter;
|
||||
FSpecView.SpanHz := ViewSpan;
|
||||
FSpecView.Mode := FController.FMode;
|
||||
FSpecView.FilterBW := FController.FFilterBW;
|
||||
if FController.FMode = MODE_FM then
|
||||
@@ -3017,9 +3092,10 @@ begin
|
||||
// Бэндплан QO-100 следует за видом спектра (downlink-домен). SetView сам
|
||||
// отсекает неизменное → пересборки кэша на каждый вызов нет.
|
||||
if Assigned(FBandPlanOverlay) then
|
||||
FBandPlanOverlay.SetView(FController.FCenterFreq, FController.FSpanHz);
|
||||
FBandPlanOverlay.SetView(ViewCenter, ViewSpan);
|
||||
if UpdateWidebandFrequencyView and FController.FShowWideband and (PbWideband <> nil) then
|
||||
PbWideband.Invalidate;
|
||||
UpdatePanZoomBar; // линейка пана следует за частотой/зумом (no-op если без изменений)
|
||||
end;
|
||||
|
||||
|
||||
@@ -4120,13 +4196,15 @@ procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||||
var
|
||||
ClickFreq: Int64;
|
||||
StepHz: Int64;
|
||||
ViewCenter, ViewSpan: Double;
|
||||
begin
|
||||
if PanelWidth <= 0 then Exit;
|
||||
if (FController.FMode = MODE_FM) and FController.FFMStepOn then
|
||||
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
|
||||
else
|
||||
StepHz := 100;
|
||||
ClickFreq := Round(FController.FCenterFreq + (PixelX / PanelWidth - 0.5) * FController.FSpanHz);
|
||||
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||
ClickFreq := Round(ViewCenter + (PixelX / PanelWidth - 0.5) * ViewSpan);
|
||||
ClickFreq := ((ClickFreq + StepHz div 2) div StepHz) * StepHz;
|
||||
if FController.FActiveVfo = 0 then
|
||||
ApplyVfoA(ClickFreq)
|
||||
@@ -4226,10 +4304,12 @@ procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||||
var
|
||||
dPix: Integer;
|
||||
dFreq: Double;
|
||||
ViewCenter, ViewSpan: Double;
|
||||
begin
|
||||
if PanelWidth <= 0 then Exit;
|
||||
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||
dPix := PixelX - FSpecDragX0;
|
||||
dFreq := dPix / PanelWidth * FController.FSpanHz;
|
||||
dFreq := dPix / PanelWidth * ViewSpan;
|
||||
if FController.FCTun then
|
||||
// CTUN ON: drag двигает окно просмотра (DDC-центр) без смены VFO; shift,
|
||||
// сеть и рендер — в контроллере SetCenter / OnControllerState(rfCenterFreq).
|
||||
@@ -4244,9 +4324,89 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Панель пана / зума
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
procedure TMainForm.PositionPanZoomBar(BottomY, RW, H: Integer; AVisible: Boolean);
|
||||
var BtnW, Gap, StripW, X: Integer;
|
||||
begin
|
||||
if (PbPanZoom = nil) or (BtnZoomIn = nil) then Exit;
|
||||
PbPanZoom.Visible := AVisible;
|
||||
BtnZoomIn.Visible := AVisible;
|
||||
BtnZoomDef.Visible := AVisible;
|
||||
BtnZoomOut.Visible := AVisible;
|
||||
if not AVisible then Exit;
|
||||
Gap := MulDiv(2, Screen.PixelsPerInch, 96);
|
||||
BtnW := H; // квадратные кнопки в высоту строки
|
||||
StripW := RW - 3 * BtnW - 4 * Gap;
|
||||
if StripW < 20 then StripW := 20;
|
||||
PbPanZoom.SetBounds(0, BottomY, StripW, H);
|
||||
X := StripW + Gap;
|
||||
BtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||
BtnZoomDef.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||
BtnZoomIn.SetBounds(X, BottomY, BtnW, H);
|
||||
PbPanZoom.Invalidate;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdatePanZoomBar;
|
||||
begin
|
||||
if FPanZoomBar <> nil then
|
||||
begin
|
||||
// Полоса — линейка ПОЛНОГО диапазона (базовые центр/span), стекло-ползунок
|
||||
// показывает видимое окно (zoom/pan).
|
||||
FPanZoomBar.SetFreq(FController.FCenterFreq, FController.FSpanHz);
|
||||
FPanZoomBar.SetState(FController.ZoomFactor, FController.PanSlider);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.OnPanZoomWindow(AZoom, APan: Double);
|
||||
begin
|
||||
// Окно-селектор задаёт и зум, и пан напрямую (без якоря на VFO).
|
||||
FController.ApplyZoom(AZoom, APan);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PbPanZoomDblClick(Sender: TObject);
|
||||
begin
|
||||
if FPanZoomBar <> nil then FPanZoomBar.HandleDblClick;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PbPanZoomMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FPanZoomBar <> nil then FPanZoomBar.HandleMouseDown(Button, X, Y);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PbPanZoomMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FPanZoomBar <> nil then FPanZoomBar.HandleMouseMove(X, Y);
|
||||
end;
|
||||
|
||||
procedure TMainForm.PbPanZoomMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FPanZoomBar <> nil then FPanZoomBar.HandleMouseUp;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnZoomInClick(Sender: TObject);
|
||||
begin
|
||||
FController.ZoomIn;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnZoomDefClick(Sender: TObject);
|
||||
begin
|
||||
FController.ZoomDefault;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnZoomOutClick(Sender: TObject);
|
||||
begin
|
||||
FController.ZoomOut;
|
||||
end;
|
||||
|
||||
// Спектр
|
||||
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var VC, VS: Double;
|
||||
begin
|
||||
if Assigned(FVfoOverlay) and
|
||||
FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||||
@@ -4261,8 +4421,9 @@ begin
|
||||
if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
|
||||
(FBeaconArming or (ssShift in Shift)) then
|
||||
begin
|
||||
FController.GetViewWindow(VC, VS);
|
||||
FController.BeaconSeedAtHz(
|
||||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
||||
PixelToFreq(X, PbSpectrum.Width, VC, VS));
|
||||
FBeaconArming := False;
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
@@ -4851,13 +5012,15 @@ procedure TMainForm.PositionVfoOverlay;
|
||||
var
|
||||
VfoX, FilterEndX, OW, NewLeft, NewTop: Integer;
|
||||
HiHz: Double;
|
||||
ViewCenter, ViewSpan: Double;
|
||||
begin
|
||||
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
||||
OW := FVfoOverlay.Width;
|
||||
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||
|
||||
if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then
|
||||
if (PbSpectrum.Width > 0) and (ViewSpan > 0) then
|
||||
begin
|
||||
VfoX := Round((FController.FVfoA - FController.FCenterFreq + FController.FSpanHz / 2) / FController.FSpanHz * PbSpectrum.Width)
|
||||
VfoX := Round((FController.FVfoA - ViewCenter + ViewSpan / 2) / ViewSpan * PbSpectrum.Width)
|
||||
end
|
||||
else
|
||||
VfoX := PbSpectrum.Width div 2;
|
||||
@@ -4868,8 +5031,8 @@ begin
|
||||
else
|
||||
HiHz := FController.FFilterBW / 2;
|
||||
end;
|
||||
if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then
|
||||
FilterEndX := VfoX + Round(HiHz / FController.FSpanHz * PbSpectrum.Width)
|
||||
if (PbSpectrum.Width > 0) and (ViewSpan > 0) then
|
||||
FilterEndX := VfoX + Round(HiHz / ViewSpan * PbSpectrum.Width)
|
||||
else
|
||||
FilterEndX := VfoX;
|
||||
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
||||
|
||||
Reference in New Issue
Block a user