mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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,
|
WebServer, WebAdapter,
|
||||||
CATAdapter,
|
CATAdapter,
|
||||||
SpectrumView, SpectrumViewOpengl,
|
SpectrumView, SpectrumViewOpengl,
|
||||||
|
PanZoomBar,
|
||||||
WidebandView,
|
WidebandView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
PlatformUtils,
|
PlatformUtils,
|
||||||
@@ -296,6 +297,12 @@ type
|
|||||||
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
PbRuler: TPaintBox; // полоса частотных меток между спектром и водопадом
|
||||||
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
PanelSplitter: TPanel; // перетаскиваемый разделитель спектр/водопад
|
||||||
PbWaterfall: TControl;
|
PbWaterfall: TControl;
|
||||||
|
// ---- Панель пана/зума под водопадом ----
|
||||||
|
PbPanZoom: TPaintBox; // полоса-ползунок пана зума
|
||||||
|
FPanZoomBar: TPanZoomBar;
|
||||||
|
BtnZoomIn: TFlatButton;
|
||||||
|
BtnZoomDef: TFlatButton;
|
||||||
|
BtnZoomOut: TFlatButton;
|
||||||
|
|
||||||
// ---- Status bar ----
|
// ---- Status bar ----
|
||||||
StatusPanel: TMainStatusBar;
|
StatusPanel: TMainStatusBar;
|
||||||
@@ -437,6 +444,19 @@ type
|
|||||||
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
procedure DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
||||||
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
procedure DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
||||||
procedure DoWidebandClick(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 BtnVfoSwapClick(Sender: TObject);
|
||||||
procedure BtnVfoACopyBClick(Sender: TObject);
|
procedure BtnVfoACopyBClick(Sender: TObject);
|
||||||
procedure BtnVfoBCopyAClick(Sender: TObject);
|
procedure BtnVfoBCopyAClick(Sender: TObject);
|
||||||
@@ -1068,6 +1088,7 @@ begin
|
|||||||
FController.FNetwork.Disconnect;
|
FController.FNetwork.Disconnect;
|
||||||
FreeAndNil(FSpecView);
|
FreeAndNil(FSpecView);
|
||||||
FreeAndNil(FWidebandView);
|
FreeAndNil(FWidebandView);
|
||||||
|
FreeAndNil(FPanZoomBar);
|
||||||
|
|
||||||
// Сохраняем настройки при закрытии
|
// Сохраняем настройки при закрытии
|
||||||
if FController.FDevConnected then
|
if FController.FDevConnected then
|
||||||
@@ -1370,6 +1391,21 @@ begin
|
|||||||
// актуальную ширину спектра (UI-зависимая величина).
|
// актуальную ширину спектра (UI-зависимая величина).
|
||||||
if FController.FWDSPReady then
|
if FController.FWDSPReady then
|
||||||
FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
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;
|
end;
|
||||||
rfFMRpt:
|
rfFMRpt:
|
||||||
begin
|
begin
|
||||||
@@ -2194,8 +2230,25 @@ begin
|
|||||||
end;
|
end;
|
||||||
PbWaterfall.Parent := PanelRight;
|
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
|
// Initial layout
|
||||||
ResizeSpectrumPanels;
|
ResizeSpectrumPanels;
|
||||||
|
UpdatePanZoomBar;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -2241,7 +2294,9 @@ begin
|
|||||||
DeltaY := P.Y - FSplitterDragY0;
|
DeltaY := P.Y - FSplitterDragY0;
|
||||||
|
|
||||||
TopOff := 0;
|
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;
|
if AvailH <= 0 then Exit;
|
||||||
|
|
||||||
NewSH := FSplitterSH0 + DeltaY;
|
NewSH := FSplitterSH0 + DeltaY;
|
||||||
@@ -2279,7 +2334,9 @@ begin
|
|||||||
DeltaY := P.Y - FSplitterDragY0;
|
DeltaY := P.Y - FSplitterDragY0;
|
||||||
|
|
||||||
TopOff := 0;
|
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;
|
if AvailH <= 0 then Exit;
|
||||||
|
|
||||||
NewSH := FSplitterSH0 + DeltaY;
|
NewSH := FSplitterSH0 + DeltaY;
|
||||||
@@ -2483,12 +2540,19 @@ var
|
|||||||
RW, RH, SH, WH, TopOff, WideH, WideSpecH: Integer;
|
RW, RH, SH, WH, TopOff, WideH, WideSpecH: Integer;
|
||||||
AvailH: Integer;
|
AvailH: Integer;
|
||||||
RULER_H: Integer;
|
RULER_H: Integer;
|
||||||
|
PANZOOM_H: Integer;
|
||||||
|
ShowPZ: Boolean;
|
||||||
begin
|
begin
|
||||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||||
if PanelRight = nil then Exit;
|
if PanelRight = nil then Exit;
|
||||||
if Assigned(FSpecView) then SyncSpecViewFreq;
|
if Assigned(FSpecView) then SyncSpecViewFreq;
|
||||||
RW := PanelRight.ClientWidth;
|
RW := PanelRight.ClientWidth;
|
||||||
RH := PanelRight.ClientHeight;
|
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;
|
UpdateWidebandFrequencyView;
|
||||||
WideH := 0;
|
WideH := 0;
|
||||||
WideSpecH := 0;
|
WideSpecH := 0;
|
||||||
@@ -2712,6 +2776,14 @@ begin
|
|||||||
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active);
|
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active);
|
||||||
if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||||
if FStepDropDown <> nil then FStepDropDown.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 BtnChannels <> nil then StyleButton(BtnChannels, False);
|
||||||
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
|
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
|
||||||
StyleButton(BtnNR, BtnNR.Active);
|
StyleButton(BtnNR, BtnNR.Active);
|
||||||
@@ -2995,12 +3067,15 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.SyncSpecViewFreq;
|
procedure TMainForm.SyncSpecViewFreq;
|
||||||
|
var ViewCenter, ViewSpan: Double;
|
||||||
begin
|
begin
|
||||||
|
// Видимое окно с учётом зума (центр+span). Без зума == FCenterFreq/FSpanHz.
|
||||||
|
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||||
FSpecView.VfoA := FController.FVfoA;
|
FSpecView.VfoA := FController.FVfoA;
|
||||||
FSpecView.VfoB := FController.FVfoB;
|
FSpecView.VfoB := FController.FVfoB;
|
||||||
FSpecView.ActiveVfo := FController.FActiveVfo;
|
FSpecView.ActiveVfo := FController.FActiveVfo;
|
||||||
FSpecView.CenterFreq := FController.FCenterFreq;
|
FSpecView.CenterFreq := ViewCenter;
|
||||||
FSpecView.SpanHz := FController.FSpanHz;
|
FSpecView.SpanHz := ViewSpan;
|
||||||
FSpecView.Mode := FController.FMode;
|
FSpecView.Mode := FController.FMode;
|
||||||
FSpecView.FilterBW := FController.FFilterBW;
|
FSpecView.FilterBW := FController.FFilterBW;
|
||||||
if FController.FMode = MODE_FM then
|
if FController.FMode = MODE_FM then
|
||||||
@@ -3017,9 +3092,10 @@ begin
|
|||||||
// Бэндплан QO-100 следует за видом спектра (downlink-домен). SetView сам
|
// Бэндплан QO-100 следует за видом спектра (downlink-домен). SetView сам
|
||||||
// отсекает неизменное → пересборки кэша на каждый вызов нет.
|
// отсекает неизменное → пересборки кэша на каждый вызов нет.
|
||||||
if Assigned(FBandPlanOverlay) then
|
if Assigned(FBandPlanOverlay) then
|
||||||
FBandPlanOverlay.SetView(FController.FCenterFreq, FController.FSpanHz);
|
FBandPlanOverlay.SetView(ViewCenter, ViewSpan);
|
||||||
if UpdateWidebandFrequencyView and FController.FShowWideband and (PbWideband <> nil) then
|
if UpdateWidebandFrequencyView and FController.FShowWideband and (PbWideband <> nil) then
|
||||||
PbWideband.Invalidate;
|
PbWideband.Invalidate;
|
||||||
|
UpdatePanZoomBar; // линейка пана следует за частотой/зумом (no-op если без изменений)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
@@ -4120,13 +4196,15 @@ procedure TMainForm.DoSpectrumClick(PixelX: Integer; PanelWidth: Integer);
|
|||||||
var
|
var
|
||||||
ClickFreq: Int64;
|
ClickFreq: Int64;
|
||||||
StepHz: Int64;
|
StepHz: Int64;
|
||||||
|
ViewCenter, ViewSpan: Double;
|
||||||
begin
|
begin
|
||||||
if PanelWidth <= 0 then Exit;
|
if PanelWidth <= 0 then Exit;
|
||||||
if (FController.FMode = MODE_FM) and FController.FFMStepOn then
|
if (FController.FMode = MODE_FM) and FController.FFMStepOn then
|
||||||
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
|
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
|
||||||
else
|
else
|
||||||
StepHz := 100;
|
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;
|
ClickFreq := ((ClickFreq + StepHz div 2) div StepHz) * StepHz;
|
||||||
if FController.FActiveVfo = 0 then
|
if FController.FActiveVfo = 0 then
|
||||||
ApplyVfoA(ClickFreq)
|
ApplyVfoA(ClickFreq)
|
||||||
@@ -4226,10 +4304,12 @@ procedure TMainForm.DoSpectrumDrag(PixelX: Integer; PanelWidth: Integer);
|
|||||||
var
|
var
|
||||||
dPix: Integer;
|
dPix: Integer;
|
||||||
dFreq: Double;
|
dFreq: Double;
|
||||||
|
ViewCenter, ViewSpan: Double;
|
||||||
begin
|
begin
|
||||||
if PanelWidth <= 0 then Exit;
|
if PanelWidth <= 0 then Exit;
|
||||||
|
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||||
dPix := PixelX - FSpecDragX0;
|
dPix := PixelX - FSpecDragX0;
|
||||||
dFreq := dPix / PanelWidth * FController.FSpanHz;
|
dFreq := dPix / PanelWidth * ViewSpan;
|
||||||
if FController.FCTun then
|
if FController.FCTun then
|
||||||
// CTUN ON: drag двигает окно просмотра (DDC-центр) без смены VFO; shift,
|
// CTUN ON: drag двигает окно просмотра (DDC-центр) без смены VFO; shift,
|
||||||
// сеть и рендер — в контроллере SetCenter / OnControllerState(rfCenterFreq).
|
// сеть и рендер — в контроллере SetCenter / OnControllerState(rfCenterFreq).
|
||||||
@@ -4244,9 +4324,89 @@ begin
|
|||||||
end;
|
end;
|
||||||
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;
|
procedure TMainForm.PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
Shift: TShiftState; X, Y: Integer);
|
Shift: TShiftState; X, Y: Integer);
|
||||||
|
var VC, VS: Double;
|
||||||
begin
|
begin
|
||||||
if Assigned(FVfoOverlay) and
|
if Assigned(FVfoOverlay) and
|
||||||
FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
FVfoOverlay.HandleMouseDown(Button, X, Y) then Exit;
|
||||||
@@ -4261,8 +4421,9 @@ begin
|
|||||||
if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
|
if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
|
||||||
(FBeaconArming or (ssShift in Shift)) then
|
(FBeaconArming or (ssShift in Shift)) then
|
||||||
begin
|
begin
|
||||||
|
FController.GetViewWindow(VC, VS);
|
||||||
FController.BeaconSeedAtHz(
|
FController.BeaconSeedAtHz(
|
||||||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
PixelToFreq(X, PbSpectrum.Width, VC, VS));
|
||||||
FBeaconArming := False;
|
FBeaconArming := False;
|
||||||
UpdateBeaconButton;
|
UpdateBeaconButton;
|
||||||
Exit;
|
Exit;
|
||||||
@@ -4851,13 +5012,15 @@ procedure TMainForm.PositionVfoOverlay;
|
|||||||
var
|
var
|
||||||
VfoX, FilterEndX, OW, NewLeft, NewTop: Integer;
|
VfoX, FilterEndX, OW, NewLeft, NewTop: Integer;
|
||||||
HiHz: Double;
|
HiHz: Double;
|
||||||
|
ViewCenter, ViewSpan: Double;
|
||||||
begin
|
begin
|
||||||
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
if not Assigned(FVfoOverlay) or not FVfoOverlay.Visible then Exit;
|
||||||
OW := FVfoOverlay.Width;
|
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
|
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
|
end
|
||||||
else
|
else
|
||||||
VfoX := PbSpectrum.Width div 2;
|
VfoX := PbSpectrum.Width div 2;
|
||||||
@@ -4868,8 +5031,8 @@ begin
|
|||||||
else
|
else
|
||||||
HiHz := FController.FFilterBW / 2;
|
HiHz := FController.FFilterBW / 2;
|
||||||
end;
|
end;
|
||||||
if (PbSpectrum.Width > 0) and (FController.FSpanHz > 0) then
|
if (PbSpectrum.Width > 0) and (ViewSpan > 0) then
|
||||||
FilterEndX := VfoX + Round(HiHz / FController.FSpanHz * PbSpectrum.Width)
|
FilterEndX := VfoX + Round(HiHz / ViewSpan * PbSpectrum.Width)
|
||||||
else
|
else
|
||||||
FilterEndX := VfoX;
|
FilterEndX := VfoX;
|
||||||
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
FilterEndX := Max(0, Min(PbSpectrum.Width - 1, FilterEndX));
|
||||||
|
|||||||
+425
@@ -0,0 +1,425 @@
|
|||||||
|
unit PanZoomBar;
|
||||||
|
|
||||||
|
{
|
||||||
|
PanZoomBar.pas — нижняя панель пана/зума спектра и водопада.
|
||||||
|
|
||||||
|
Полоса = мини-линейка ПОЛНОГО диапазона (sample rate): цифры частот + деления.
|
||||||
|
Поверх — окно-селектор «стекло» (полупрозрачное, с объёмной фаской), равное
|
||||||
|
видимой полосе. Взаимодействие:
|
||||||
|
• тянешь СЕРЕДИНУ окна → пан (сдвиг видимой полосы);
|
||||||
|
• тянешь ЛЕВЫЙ/ПРАВЫЙ край → зум (меняешь ширину окна, противоположный край
|
||||||
|
зафиксирован), курсор у края ⇔;
|
||||||
|
• клик мимо окна → окно центрируется на курсоре (пан);
|
||||||
|
• двойной клик → сброс к полному диапазону (зум 0).
|
||||||
|
Сквозь стекло видны цифры/деления. Callback OnZoomPan(zoom, pan) отдаёт обе
|
||||||
|
величины (зум 0..1, пан 0..1) — контроллер применяет их напрямую.
|
||||||
|
|
||||||
|
Рисует в чужой TPaintBox (как RulerView) через offscreen-битмап. Полупрозрачность
|
||||||
|
стекла — реальный per-pixel альфа-бленд (TLazIntfImage). Кнопки +/умолч/- — в MainForm.
|
||||||
|
}
|
||||||
|
|
||||||
|
{$IFDEF FPC}
|
||||||
|
{$MODE Delphi}
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
|
||||||
|
LCLIntf, LCLType, IntfGraphics, fpImage, AppTheme;
|
||||||
|
|
||||||
|
type
|
||||||
|
TPanZoomEvent = procedure(AZoom, APan: Double) of object;
|
||||||
|
|
||||||
|
TDragMode = (dmNone, dmBody, dmLeft, dmRight);
|
||||||
|
|
||||||
|
TPanZoomBar = class
|
||||||
|
private
|
||||||
|
FPb: TPaintBox;
|
||||||
|
FBmp: TBitmap;
|
||||||
|
FTheme: TAppTheme;
|
||||||
|
FZoom: Double; // 0..1 (0 = без зума, весь span виден)
|
||||||
|
FPan: Double; // 0..1 (положение окна по span)
|
||||||
|
FCenterFreq: Double; // центр ПОЛНОГО диапазона (DDC), Гц
|
||||||
|
FSpanHz: Double; // полный span (= sample rate), Гц
|
||||||
|
FDragMode: TDragMode;
|
||||||
|
FDragGrab: Double; // body: (курсор_доля − левый_край_доля) в момент захвата
|
||||||
|
FDragLo: Double; // зафиксированный левый край окна (для edge-зума), доля
|
||||||
|
FDragHi: Double; // зафиксированный правый край окна, доля
|
||||||
|
FOnZoomPan: TPanZoomEvent;
|
||||||
|
|
||||||
|
function VisFraction: Double; // доля span, видимая при текущем зуме
|
||||||
|
function ThumbGeom(W: Integer; out L, Wd: Integer): Boolean; // px-геометрия окна
|
||||||
|
procedure CurWindow(out Lo, Hi: Double); // доли краёв окна
|
||||||
|
procedure EmitWindow(NewLo, NewHi: Double); // окно → zoom/pan + callback
|
||||||
|
function EdgePx(Wd: Integer): Integer;
|
||||||
|
procedure DrawRulerInto(W, H: Integer);
|
||||||
|
procedure BlendThumb(L, Wd, W, H: Integer);
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
|
||||||
|
property PaintBox: TPaintBox write FPb;
|
||||||
|
property OnZoomPan: TPanZoomEvent read FOnZoomPan write FOnZoomPan;
|
||||||
|
|
||||||
|
procedure SetState(AZoom, APan: Double);
|
||||||
|
procedure SetFreq(ACenterFreq, ASpanHz: Double);
|
||||||
|
procedure SetTheme(const T: TAppTheme);
|
||||||
|
procedure Paint(Sender: TObject);
|
||||||
|
procedure HandleMouseDown(Button: TMouseButton; X, Y: Integer);
|
||||||
|
procedure HandleMouseMove(X, Y: Integer);
|
||||||
|
procedure HandleMouseUp;
|
||||||
|
procedure HandleDblClick;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
MIN_THUMB_PX = 12; // окно не уже этого на экране — чтобы было за что схватить
|
||||||
|
MIN_VIS = 0.01; // минимальная видимая доля (= максимальный зум ~100x)
|
||||||
|
|
||||||
|
// Линейная интерполяция байт-канала.
|
||||||
|
function MixB(Bg, Fg: Integer; A: Double): Integer; inline;
|
||||||
|
begin
|
||||||
|
Result := Round(Bg * (1.0 - A) + Fg * A);
|
||||||
|
if Result < 0 then Result := 0 else if Result > 255 then Result := 255;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function FreqLabel(Hz: Double): string;
|
||||||
|
begin
|
||||||
|
Result := Format('%.3f', [Hz / 1e6]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Видимая доля → коэффициент зума z (инверсия формулы анализатора).
|
||||||
|
function VisToZoom(Vis: Double): Double;
|
||||||
|
var V: Double;
|
||||||
|
begin
|
||||||
|
V := EnsureRange(Vis, MIN_VIS, 1.0);
|
||||||
|
Result := EnsureRange((Power(10.0, (1.0 - V) / 0.99) - 1.0) / 9.0, 0.0, 1.0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TPanZoomBar.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FBmp := TBitmap.Create;
|
||||||
|
FBmp.PixelFormat := pf32bit;
|
||||||
|
FTheme := DarkTheme;
|
||||||
|
FZoom := 0.0;
|
||||||
|
FPan := 0.5;
|
||||||
|
FCenterFreq := 0.0;
|
||||||
|
FSpanHz := 192000.0;
|
||||||
|
FDragMode := dmNone;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TPanZoomBar.Destroy;
|
||||||
|
begin
|
||||||
|
FBmp.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.SetTheme(const T: TAppTheme);
|
||||||
|
begin
|
||||||
|
FTheme := T;
|
||||||
|
if Assigned(FPb) then FPb.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.SetState(AZoom, APan: Double);
|
||||||
|
var NewZoom, NewPan: Double;
|
||||||
|
begin
|
||||||
|
NewZoom := EnsureRange(AZoom, 0.0, 1.0);
|
||||||
|
NewPan := EnsureRange(APan, 0.0, 1.0);
|
||||||
|
if (Abs(NewZoom - FZoom) < 1e-4) and (Abs(NewPan - FPan) < 1e-4) then Exit;
|
||||||
|
FZoom := NewZoom;
|
||||||
|
FPan := NewPan;
|
||||||
|
if Assigned(FPb) then FPb.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.SetFreq(ACenterFreq, ASpanHz: Double);
|
||||||
|
begin
|
||||||
|
if (Abs(ACenterFreq - FCenterFreq) < 0.5) and (Abs(ASpanHz - FSpanHz) < 1.0) then Exit;
|
||||||
|
FCenterFreq := ACenterFreq;
|
||||||
|
FSpanHz := ASpanHz;
|
||||||
|
if Assigned(FPb) then FPb.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TPanZoomBar.VisFraction: Double;
|
||||||
|
// Та же формула, что и span-clip в WDSPEngine: видимая доля = width/bins.
|
||||||
|
begin
|
||||||
|
Result := EnsureRange(1.0 - 0.99 * Log10(9.0 * FZoom + 1.0), MIN_VIS, 1.0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.CurWindow(out Lo, Hi: Double);
|
||||||
|
var Vis: Double;
|
||||||
|
begin
|
||||||
|
Vis := VisFraction;
|
||||||
|
Lo := EnsureRange(FPan, 0.0, 1.0) * (1.0 - Vis);
|
||||||
|
Hi := Lo + Vis;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TPanZoomBar.EdgePx(Wd: Integer): Integer;
|
||||||
|
// Зона захвата края — узкая, но не съедает всё узкое окно (оставляем тело).
|
||||||
|
begin
|
||||||
|
Result := Max(3, Min(8, Wd div 3));
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TPanZoomBar.ThumbGeom(W: Integer; out L, Wd: Integer): Boolean;
|
||||||
|
var Lo, Hi: Double;
|
||||||
|
begin
|
||||||
|
Result := W > 0;
|
||||||
|
if not Result then begin L := 0; Wd := 0; Exit; end;
|
||||||
|
CurWindow(Lo, Hi);
|
||||||
|
Wd := Max(MIN_THUMB_PX, Round((Hi - Lo) * W));
|
||||||
|
if Wd > W then Wd := W;
|
||||||
|
L := Round(Lo * W);
|
||||||
|
if L < 0 then L := 0;
|
||||||
|
if L + Wd > W then L := W - Wd;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.EmitWindow(NewLo, NewHi: Double);
|
||||||
|
var Vis, Z, P: Double;
|
||||||
|
begin
|
||||||
|
if NewLo < 0.0 then NewLo := 0.0;
|
||||||
|
if NewHi > 1.0 then NewHi := 1.0;
|
||||||
|
if NewHi - NewLo < MIN_VIS then NewHi := NewLo + MIN_VIS;
|
||||||
|
if NewHi > 1.0 then begin NewHi := 1.0; NewLo := NewHi - MIN_VIS; end;
|
||||||
|
Vis := NewHi - NewLo;
|
||||||
|
Z := VisToZoom(Vis);
|
||||||
|
if (1.0 - Vis) > 1e-6 then P := NewLo / (1.0 - Vis) else P := 0.5;
|
||||||
|
P := EnsureRange(P, 0.0, 1.0);
|
||||||
|
// Локально обновляем сразу — драг плавный даже до round-trip контроллера.
|
||||||
|
FZoom := Z; FPan := P;
|
||||||
|
if Assigned(FPb) then FPb.Invalidate;
|
||||||
|
if Assigned(FOnZoomPan) then FOnZoomPan(Z, P);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.DrawRulerInto(W, H: Integer);
|
||||||
|
// Цифры частот + деления на всю ширину (= полный диапазон).
|
||||||
|
var
|
||||||
|
C: TCanvas;
|
||||||
|
i, X, TickMaj, TickMin, LabelY, NDiv, LabelEvery, TW: Integer;
|
||||||
|
FreqStart, FreqHz, PixPerDiv: Double;
|
||||||
|
Lbl: string;
|
||||||
|
begin
|
||||||
|
C := FBmp.Canvas;
|
||||||
|
C.Brush.Style := bsSolid;
|
||||||
|
C.Brush.Color := FTheme.SliderBG;
|
||||||
|
C.FillRect(Rect(0, 0, W, H));
|
||||||
|
|
||||||
|
C.Font.Name := 'Courier New';
|
||||||
|
C.Font.Style := [];
|
||||||
|
C.Font.Height := -Max(7, Round(H * 0.40));
|
||||||
|
C.Brush.Style := bsClear;
|
||||||
|
|
||||||
|
TickMaj := Max(3, Round(H * 0.28));
|
||||||
|
TickMin := Max(2, Round(H * 0.16));
|
||||||
|
LabelY := TickMaj + ((H - TickMaj) - C.TextHeight('0')) div 2;
|
||||||
|
if LabelY < TickMaj then LabelY := TickMaj;
|
||||||
|
|
||||||
|
NDiv := 8;
|
||||||
|
if FSpanHz <= 0 then Exit;
|
||||||
|
FreqStart := FCenterFreq - FSpanHz / 2;
|
||||||
|
PixPerDiv := W / NDiv;
|
||||||
|
if PixPerDiv >= (C.TextWidth('000.000') + 8) then LabelEvery := 1
|
||||||
|
else if PixPerDiv * 2 >= (C.TextWidth('000.000') + 8) then LabelEvery := 2
|
||||||
|
else LabelEvery := 4;
|
||||||
|
|
||||||
|
for i := 0 to NDiv do
|
||||||
|
begin
|
||||||
|
X := Round(i * PixPerDiv);
|
||||||
|
if X >= W then X := W - 1;
|
||||||
|
C.Pen.Color := FTheme.RulerBorder;
|
||||||
|
if (i mod LabelEvery) = 0 then
|
||||||
|
begin
|
||||||
|
C.MoveTo(X, 0); C.LineTo(X, TickMaj);
|
||||||
|
FreqHz := FreqStart + i * FSpanHz / NDiv;
|
||||||
|
Lbl := FreqLabel(FreqHz);
|
||||||
|
TW := C.TextWidth(Lbl);
|
||||||
|
C.Font.Color := FTheme.RulerText;
|
||||||
|
C.TextOut(EnsureRange(X - TW div 2, 0, W - TW), LabelY, Lbl);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
C.MoveTo(X, 0); C.LineTo(X, TickMin);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
C.Brush.Style := bsSolid;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.BlendThumb(L, Wd, W, H: Integer);
|
||||||
|
// Полупрозрачное «стекло» с объёмной фаской поверх линейки.
|
||||||
|
var
|
||||||
|
Intf: TLazIntfImage;
|
||||||
|
x, y, top, bot: Integer;
|
||||||
|
c: TFPColor;
|
||||||
|
tR, tG, tB, bgR, bgG, bgB, nR, nG, nB: Integer;
|
||||||
|
A, RowT, Gloss: Double;
|
||||||
|
Active: Boolean;
|
||||||
|
begin
|
||||||
|
if (Wd <= 0) or (W <= 0) or (H <= 2) then Exit;
|
||||||
|
Active := FDragMode <> dmNone;
|
||||||
|
if Active then
|
||||||
|
begin
|
||||||
|
tR := GetRValue(ColorToRGB(FTheme.SliderThumbDrag));
|
||||||
|
tG := GetGValue(ColorToRGB(FTheme.SliderThumbDrag));
|
||||||
|
tB := GetBValue(ColorToRGB(FTheme.SliderThumbDrag));
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
tR := GetRValue(ColorToRGB(FTheme.SliderThumbNorm));
|
||||||
|
tG := GetGValue(ColorToRGB(FTheme.SliderThumbNorm));
|
||||||
|
tB := GetBValue(ColorToRGB(FTheme.SliderThumbNorm));
|
||||||
|
end;
|
||||||
|
|
||||||
|
top := 1;
|
||||||
|
bot := H - 2;
|
||||||
|
Intf := FBmp.CreateIntfImage;
|
||||||
|
try
|
||||||
|
for y := top to bot do
|
||||||
|
begin
|
||||||
|
if (bot - top) > 0 then RowT := (y - top) / (bot - top) else RowT := 0.0;
|
||||||
|
Gloss := 1.18 - 0.42 * RowT; // объём: ярче вверху, темнее внизу
|
||||||
|
A := 0.42 + 0.10 * (1.0 - RowT);
|
||||||
|
for x := L to L + Wd - 1 do
|
||||||
|
begin
|
||||||
|
if (x < 0) or (x >= W) then Continue;
|
||||||
|
c := Intf.Colors[x, y];
|
||||||
|
bgR := c.red shr 8; bgG := c.green shr 8; bgB := c.blue shr 8;
|
||||||
|
nR := MixB(bgR, EnsureRange(Round(tR * Gloss), 0, 255), A);
|
||||||
|
nG := MixB(bgG, EnsureRange(Round(tG * Gloss), 0, 255), A);
|
||||||
|
nB := MixB(bgB, EnsureRange(Round(tB * Gloss), 0, 255), A);
|
||||||
|
c.red := nR * 257; c.green := nG * 257; c.blue := nB * 257; c.alpha := $FFFF;
|
||||||
|
Intf.Colors[x, y] := c;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
FBmp.LoadFromIntfImage(Intf);
|
||||||
|
finally
|
||||||
|
Intf.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Объёмная фаска + маркеры краёв (две вертикальные риски — «ручки» ресайза).
|
||||||
|
with FBmp.Canvas do
|
||||||
|
begin
|
||||||
|
Pen.Width := 1; Brush.Style := bsClear;
|
||||||
|
Pen.Color := FTheme.SliderThumbBdr;
|
||||||
|
Rectangle(L, top, L + Wd, bot + 1);
|
||||||
|
Pen.Color := RGBToColor(EnsureRange(tR + 70, 0, 255),
|
||||||
|
EnsureRange(tG + 70, 0, 255),
|
||||||
|
EnsureRange(tB + 70, 0, 255));
|
||||||
|
MoveTo(L + 1, top + 1); LineTo(L + Wd - 1, top + 1);
|
||||||
|
MoveTo(L + 1, top + 1); LineTo(L + 1, bot);
|
||||||
|
Pen.Color := RGBToColor(EnsureRange(tR - 60, 0, 255),
|
||||||
|
EnsureRange(tG - 60, 0, 255),
|
||||||
|
EnsureRange(tB - 60, 0, 255));
|
||||||
|
MoveTo(L + 1, bot); LineTo(L + Wd - 1, bot);
|
||||||
|
MoveTo(L + Wd - 1, top + 1); LineTo(L + Wd - 1, bot + 1);
|
||||||
|
// «ручки» захвата по краям (видны, когда окно достаточно широкое)
|
||||||
|
if Wd >= 3 * MIN_THUMB_PX then
|
||||||
|
begin
|
||||||
|
Pen.Color := FTheme.SliderThumbBdr;
|
||||||
|
MoveTo(L + 3, top + 2); LineTo(L + 3, bot - 1);
|
||||||
|
MoveTo(L + Wd - 4, top + 2); LineTo(L + Wd - 4, bot - 1);
|
||||||
|
end;
|
||||||
|
Brush.Style := bsSolid;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.Paint(Sender: TObject);
|
||||||
|
var W, H, L, Wd: Integer;
|
||||||
|
begin
|
||||||
|
if FPb = nil then Exit;
|
||||||
|
W := FPb.Width; H := FPb.Height;
|
||||||
|
if (W <= 0) or (H <= 0) then Exit;
|
||||||
|
if (FBmp.Width <> W) or (FBmp.Height <> H) then FBmp.SetSize(W, H);
|
||||||
|
|
||||||
|
DrawRulerInto(W, H);
|
||||||
|
if ThumbGeom(W, L, Wd) then BlendThumb(L, Wd, W, H);
|
||||||
|
|
||||||
|
FBmp.Canvas.Brush.Style := bsClear;
|
||||||
|
FBmp.Canvas.Pen.Color := FTheme.RulerBorder;
|
||||||
|
FBmp.Canvas.Rectangle(0, 0, W, H);
|
||||||
|
FBmp.Canvas.Brush.Style := bsSolid;
|
||||||
|
|
||||||
|
FPb.Canvas.Draw(0, 0, FBmp);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.HandleMouseDown(Button: TMouseButton; X, Y: Integer);
|
||||||
|
var W, L, Wd, Edge: Integer; Lo, Hi, MouseFrac: Double;
|
||||||
|
begin
|
||||||
|
if (FPb = nil) or (Button <> mbLeft) then Exit;
|
||||||
|
W := FPb.Width;
|
||||||
|
if not ThumbGeom(W, L, Wd) then Exit;
|
||||||
|
CurWindow(Lo, Hi);
|
||||||
|
FDragLo := Lo; FDragHi := Hi;
|
||||||
|
Edge := EdgePx(Wd);
|
||||||
|
MouseFrac := X / W;
|
||||||
|
|
||||||
|
if (X >= L) and (X < L + Edge) then
|
||||||
|
FDragMode := dmLeft
|
||||||
|
else if (X > L + Wd - Edge) and (X <= L + Wd) then
|
||||||
|
FDragMode := dmRight
|
||||||
|
else if (X >= L) and (X <= L + Wd) then
|
||||||
|
begin
|
||||||
|
FDragMode := dmBody;
|
||||||
|
FDragGrab := MouseFrac - Lo; // тянем тело за точку захвата
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// Клик мимо окна → центрируем окно на курсоре, дальше тянем телом.
|
||||||
|
FDragMode := dmBody;
|
||||||
|
FDragGrab := (Hi - Lo) / 2.0;
|
||||||
|
EmitWindow(MouseFrac - FDragGrab, MouseFrac - FDragGrab + (Hi - Lo));
|
||||||
|
end;
|
||||||
|
if Assigned(FPb) then FPb.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.HandleMouseMove(X, Y: Integer);
|
||||||
|
var W, L, Wd, Edge: Integer; MouseFrac, Width: Double;
|
||||||
|
begin
|
||||||
|
if FPb = nil then Exit;
|
||||||
|
W := FPb.Width;
|
||||||
|
if W <= 0 then Exit;
|
||||||
|
MouseFrac := X / W;
|
||||||
|
|
||||||
|
if FDragMode = dmNone then
|
||||||
|
begin
|
||||||
|
// Hover: курсор-подсказка над краями (зум) / телом (пан).
|
||||||
|
if ThumbGeom(W, L, Wd) then
|
||||||
|
begin
|
||||||
|
Edge := EdgePx(Wd);
|
||||||
|
if ((X >= L) and (X < L + Edge)) or ((X > L + Wd - Edge) and (X <= L + Wd)) then
|
||||||
|
FPb.Cursor := crSizeWE
|
||||||
|
else if (X >= L) and (X <= L + Wd) then
|
||||||
|
FPb.Cursor := crSizeAll
|
||||||
|
else
|
||||||
|
FPb.Cursor := crDefault;
|
||||||
|
end;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
case FDragMode of
|
||||||
|
dmLeft: EmitWindow(Min(MouseFrac, FDragHi - MIN_VIS), FDragHi);
|
||||||
|
dmRight: EmitWindow(FDragLo, Max(MouseFrac, FDragLo + MIN_VIS));
|
||||||
|
dmBody:
|
||||||
|
begin
|
||||||
|
Width := FDragHi - FDragLo; // ширина фиксирована → зум не меняется
|
||||||
|
EmitWindow(EnsureRange(MouseFrac - FDragGrab, 0.0, 1.0 - Width),
|
||||||
|
EnsureRange(MouseFrac - FDragGrab, 0.0, 1.0 - Width) + Width);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.HandleMouseUp;
|
||||||
|
begin
|
||||||
|
if FDragMode = dmNone then Exit;
|
||||||
|
FDragMode := dmNone;
|
||||||
|
if Assigned(FPb) then FPb.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPanZoomBar.HandleDblClick;
|
||||||
|
begin
|
||||||
|
// Сброс к полному диапазону.
|
||||||
|
FDragMode := dmNone;
|
||||||
|
EmitWindow(0.0, 1.0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+140
-1
@@ -66,7 +66,7 @@ type
|
|||||||
rfRxGain, // Pluto: hw-gain (auto/manual dB)
|
rfRxGain, // Pluto: hw-gain (auto/manual dB)
|
||||||
rfVolume, rfMute, rfDrive, rfAtten,
|
rfVolume, rfMute, rfDrive, rfAtten,
|
||||||
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
|
rfBand, rfXvtr, rfBandRestore, // rfBandRestore: явная смена диапазона (сброс Wf-avg в UI)
|
||||||
rfCenterFreq, rfSpan, rfSampleRate,
|
rfCenterFreq, rfSpan, rfSampleRate, rfZoom,
|
||||||
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
|
rfCTun, rfDuplex, rfRxMuteOnTx, // rfRxMuteOnTx: QO-100 self-monitor toggle
|
||||||
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
rfRunning, rfConnected, rfTransmitting, rfTuning,
|
||||||
rfNR, rfNB, rfSNB, rfANF,
|
rfNR, rfNB, rfSNB, rfANF,
|
||||||
@@ -138,6 +138,7 @@ type
|
|||||||
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
|
||||||
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
|
||||||
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
function ActiveVfoHz: Double; // частота активного VFO (A или B)
|
||||||
|
function AnchoredPan(AZoom: Double): Double; // пан, центрирующий окно на маркере VFO
|
||||||
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
procedure ApplyTuneCore(VfoHz: Double); // CTUN shift/center/scroll для актив. VFO
|
||||||
public
|
public
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -181,6 +182,8 @@ type
|
|||||||
FCenterFreq: Double;
|
FCenterFreq: Double;
|
||||||
FSpanHz: Double;
|
FSpanHz: Double;
|
||||||
FSampleRate: Integer;
|
FSampleRate: Integer;
|
||||||
|
FZoomFactor: Double; // зум спектра/водопада 0.05..1.0 (1.0 = без зума)
|
||||||
|
FPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
||||||
FCurrentBand: Integer;
|
FCurrentBand: Integer;
|
||||||
FCurrentXvtr: Integer;
|
FCurrentXvtr: Integer;
|
||||||
|
|
||||||
@@ -480,6 +483,18 @@ type
|
|||||||
procedure SetSpan(Hz: Integer);
|
procedure SetSpan(Hz: Integer);
|
||||||
procedure SetSampleRate(Hz: Integer);
|
procedure SetSampleRate(Hz: Integer);
|
||||||
|
|
||||||
|
// ---- Zoom/pan спектра и водопада ----
|
||||||
|
// GetViewWindow — единая истина «видимого окна» (центр+span в Гц) для всех
|
||||||
|
// оверлеев и маппинга мыши: при зуме отдаёт окно из WDSP-анализатора, иначе
|
||||||
|
// базовые FCenterFreq/FSpanHz.
|
||||||
|
procedure GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||||||
|
procedure ApplyZoom(AZoom, APan: Double); // задать зум+пан, сохранить в band, Changed(rfZoom)
|
||||||
|
procedure ZoomIn; // шаг увеличения (якорь — маркер активного VFO)
|
||||||
|
procedure ZoomOut; // шаг уменьшения
|
||||||
|
procedure ZoomDefault; // сброс в 1.0 (весь span)
|
||||||
|
function ZoomFactor: Double;
|
||||||
|
function PanSlider: Double;
|
||||||
|
|
||||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||||
// QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
|
// QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
|
||||||
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
|
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
|
||||||
@@ -610,6 +625,7 @@ begin
|
|||||||
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
|
||||||
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
|
||||||
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
|
||||||
|
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
|
||||||
FCurrentBand := 5; FCurrentXvtr := -1;
|
FCurrentBand := 5; FCurrentXvtr := -1;
|
||||||
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
|
||||||
FPAMaxPower := 100.0;
|
FPAMaxPower := 100.0;
|
||||||
@@ -1452,6 +1468,8 @@ begin
|
|||||||
Result.CTun := FCTun;
|
Result.CTun := FCTun;
|
||||||
Result.CenterHz := FCenterFreq; // DDC-центр → восстановим offset CTun при загрузке
|
Result.CenterHz := FCenterFreq; // DDC-центр → восстановим offset CTun при загрузке
|
||||||
Result.SpanHz := FSpanHz;
|
Result.SpanHz := FSpanHz;
|
||||||
|
Result.ZoomFactor := FZoomFactor;
|
||||||
|
Result.PanSlider := FPanSlider;
|
||||||
Result.FMSQOn := FFMSQOn;
|
Result.FMSQOn := FFMSQOn;
|
||||||
Result.FMSQLevel := FFMSQLevel;
|
Result.FMSQLevel := FFMSQLevel;
|
||||||
Result.CTCSSOn := FFMCTCSSOn;
|
Result.CTCSSOn := FFMCTCSSOn;
|
||||||
@@ -1667,6 +1685,12 @@ begin
|
|||||||
|
|
||||||
ApplyBandDSP(B);
|
ApplyBandDSP(B);
|
||||||
|
|
||||||
|
// --- Zoom/pan диапазона: восстанавливаем и переармируем анализатор ---
|
||||||
|
FZoomFactor := EnsureRange(B.ZoomFactor, 0.0, 1.0);
|
||||||
|
FPanSlider := EnsureRange(B.PanSlider, 0.0, 1.0);
|
||||||
|
if Assigned(FDSPEngine) then FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||||||
|
Changed(rfZoom);
|
||||||
|
|
||||||
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
|
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
|
||||||
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
|
// При CTun восстанавливаем сохранённый DDC-центр, чтобы VFO остался на своём
|
||||||
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
|
// месте в полосе (а не прыгал в центр). Берём только если offset влезает в
|
||||||
@@ -2327,6 +2351,121 @@ end;
|
|||||||
procedure TRadioController.SetSpan(Hz: Integer);
|
procedure TRadioController.SetSpan(Hz: Integer);
|
||||||
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
|
begin FSpanHz := Hz; Changed(rfSpan); { TODO wiring } end;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// Zoom / pan спектра и водопада
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function TRadioController.ZoomFactor: Double;
|
||||||
|
begin Result := FZoomFactor; end;
|
||||||
|
|
||||||
|
function TRadioController.PanSlider: Double;
|
||||||
|
begin Result := FPanSlider; end;
|
||||||
|
|
||||||
|
procedure TRadioController.GetViewWindow(out ACenter: Double; out ASpan: Double);
|
||||||
|
// Видимое окно (центр+span, Гц) с учётом зума. Формула совпадает с расчётом
|
||||||
|
// span-clip в WDSPEngine.ApplyRXAnalyzerSettings (bw=FSpanHz, clp=0), поэтому
|
||||||
|
// оверлеи точно ложатся на пиксели. Без зума → базовые центр/span.
|
||||||
|
var
|
||||||
|
FVis, OneMinusFVis, VisSpan, LowEdgeOff, CenterOff: Double;
|
||||||
|
begin
|
||||||
|
if (FZoomFactor <= 0.0) or (FSpanHz <= 0.0) then
|
||||||
|
begin
|
||||||
|
ACenter := FCenterFreq;
|
||||||
|
ASpan := FSpanHz;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(FZoomFactor, 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;
|
||||||
|
CenterOff := LowEdgeOff + VisSpan / 2.0;
|
||||||
|
ACenter := FCenterFreq + CenterOff;
|
||||||
|
ASpan := VisSpan;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.AnchoredPan(AZoom: Double): Double;
|
||||||
|
// Положение пана, при котором видимое окно центрировано на маркере активного
|
||||||
|
// VFO. Клампится в [0..1], чтобы окно не вылезало за span (у краёв маркер
|
||||||
|
// перестаёт быть строго по центру — окно упирается в край).
|
||||||
|
var OneMinusFVis, U: Double;
|
||||||
|
begin
|
||||||
|
if (AZoom <= 0.0) or (FSpanHz <= 0.0) then Exit(0.5);
|
||||||
|
OneMinusFVis := 0.99 * Log10(9.0 * EnsureRange(AZoom, 0.0, 1.0) + 1.0);
|
||||||
|
if OneMinusFVis <= 1e-9 then Exit(0.5);
|
||||||
|
U := (ActiveVfoHz - FCenterFreq) / FSpanHz; // нормализованный сдвиг маркера
|
||||||
|
Result := EnsureRange(0.5 + U / OneMinusFVis, 0.0, 1.0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ApplyZoom(AZoom, APan: Double);
|
||||||
|
begin
|
||||||
|
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
|
||||||
|
FPanSlider := EnsureRange(APan, 0.0, 1.0);
|
||||||
|
if Assigned(FDSPEngine) then
|
||||||
|
FDSPEngine.SetZoomPan(FZoomFactor, FPanSlider);
|
||||||
|
if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
|
||||||
|
begin
|
||||||
|
FBandCache[FCurrentBand].ZoomFactor := FZoomFactor;
|
||||||
|
FBandCache[FCurrentBand].PanSlider := FPanSlider;
|
||||||
|
end;
|
||||||
|
Changed(rfZoom);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Лестница кратностей увеличения: мелкий шаг в начале (1.0→1.2→1.45…), крупнее
|
||||||
|
// к концу. Кнопки [+]/[-] шагают по ней; ZoomFactor (0..1) хранит соответствующий
|
||||||
|
// z. Перевод кратность↔z — инверсия формулы анализатора (см. ApplyRXAnalyzerSettings).
|
||||||
|
const
|
||||||
|
ZOOM_MAG_LADDER: array[0..16] of Double =
|
||||||
|
(1.0, 1.1, 1.15, 1.2, 1.45, 1.75, 2.1, 2.5, 3.0, 3.7, 4.7, 6.2, 8.5, 12.0, 20.0, 40.0, 100.0);
|
||||||
|
|
||||||
|
function ZoomMagToZ(M: Double): Double;
|
||||||
|
begin
|
||||||
|
if M <= 1.0 then Exit(0.0);
|
||||||
|
// M = 1/(1-0.99*log10(9z+1)) ⇒ z = (10^((1-1/M)/0.99) - 1)/9
|
||||||
|
Result := EnsureRange((Power(10.0, (1.0 - 1.0 / M) / 0.99) - 1.0) / 9.0, 0.0, 1.0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function ZoomZToMag(Z: Double): Double;
|
||||||
|
begin
|
||||||
|
Result := 1.0 / (1.0 - 0.99 * Log10(9.0 * EnsureRange(Z, 0.0, 1.0) + 1.0));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ZoomIn;
|
||||||
|
var CurMag, Z: Double; i: Integer;
|
||||||
|
begin
|
||||||
|
CurMag := ZoomZToMag(FZoomFactor);
|
||||||
|
for i := 0 to High(ZOOM_MAG_LADDER) do
|
||||||
|
if ZOOM_MAG_LADDER[i] > CurMag * 1.001 then
|
||||||
|
begin
|
||||||
|
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||||||
|
ApplyZoom(Z, AnchoredPan(Z));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ZoomOut;
|
||||||
|
var CurMag, Z: Double; i: Integer;
|
||||||
|
begin
|
||||||
|
CurMag := ZoomZToMag(FZoomFactor);
|
||||||
|
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 ApplyZoom(0.0, 0.5)
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Z := ZoomMagToZ(ZOOM_MAG_LADDER[i]);
|
||||||
|
ApplyZoom(Z, AnchoredPan(Z));
|
||||||
|
end;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
ApplyZoom(0.0, 0.5);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ZoomDefault;
|
||||||
|
begin
|
||||||
|
ApplyZoom(0.0, 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 в
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ type
|
|||||||
FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz)
|
FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz)
|
||||||
FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS
|
FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS
|
||||||
FMRptOffsetHz: Double; // Hz, 0 = use band default
|
FMRptOffsetHz: Double; // Hz, 0 = use band default
|
||||||
|
ZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
|
||||||
|
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
|
||||||
@@ -407,6 +409,8 @@ begin
|
|||||||
B.FMStepIdx := 3; // 25 kHz
|
B.FMStepIdx := 3; // 25 kHz
|
||||||
B.FMRptDir := 0;
|
B.FMRptDir := 0;
|
||||||
B.FMRptOffsetHz := 0.0;
|
B.FMRptOffsetHz := 0.0;
|
||||||
|
B.ZoomFactor := 0.0;
|
||||||
|
B.PanSlider := 0.5;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
|
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
|
||||||
@@ -823,6 +827,7 @@ begin
|
|||||||
JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx);
|
JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx);
|
||||||
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
|
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
|
||||||
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
|
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
|
||||||
|
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
|
||||||
@@ -852,6 +857,8 @@ begin
|
|||||||
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
|
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
|
||||||
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
|
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
|
||||||
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
|
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
|
||||||
|
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
|
||||||
|
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
|
procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);
|
||||||
|
|||||||
+55
-2
@@ -204,6 +204,16 @@ type
|
|||||||
FWfAvgMode: Integer;
|
FWfAvgMode: Integer;
|
||||||
FWfAvgTimeMS: Double;
|
FWfAvgTimeMS: Double;
|
||||||
|
|
||||||
|
// Zoom/pan анализатора (span-clip в SetAnalyzer). Конвенция WDSP/Thetis:
|
||||||
|
// FZoomFactor 0.0..1.0, где 0.0 = весь span (без зума), 1.0 = максимум.
|
||||||
|
// FPanSlider 0..1 (положение окна по span). FViewLowHz/FViewHighHz —
|
||||||
|
// границы видимого окна в Гц относительно DDC-центра (low отрицательный),
|
||||||
|
// пересчитываются в ApplyRXAnalyzerSettings.
|
||||||
|
FZoomFactor: Double;
|
||||||
|
FPanSlider: Double;
|
||||||
|
FViewLowHz: Double;
|
||||||
|
FViewHighHz: Double;
|
||||||
|
|
||||||
FOnAudio: TOnAudioReady;
|
FOnAudio: TOnAudioReady;
|
||||||
FOnSpectrum: TOnSpectrumReady;
|
FOnSpectrum: TOnSpectrumReady;
|
||||||
FOnWaterfall: TOnWaterfallReady;
|
FOnWaterfall: TOnWaterfallReady;
|
||||||
@@ -431,6 +441,14 @@ type
|
|||||||
// S-meter
|
// S-meter
|
||||||
function GetSMeterDBm: Double;
|
function GetSMeterDBm: Double;
|
||||||
|
|
||||||
|
// Zoom/pan: задать коэффициент зума и положение пана и переармировать
|
||||||
|
// RX-анализатор. View* — границы видимого окна (Гц отн. DDC-центра).
|
||||||
|
procedure SetZoomPan(AZoom, APan: Double);
|
||||||
|
property ZoomFactor: Double read FZoomFactor;
|
||||||
|
property PanSlider: Double read FPanSlider;
|
||||||
|
property ViewLowHz: Double read FViewLowHz;
|
||||||
|
property ViewHighHz: Double read FViewHighHz;
|
||||||
|
|
||||||
property Initialized: Boolean read FInitialized;
|
property Initialized: Boolean read FInitialized;
|
||||||
property SampleRate: Integer read FSampleRate;
|
property SampleRate: Integer read FSampleRate;
|
||||||
property TXSampleRate: Integer read FTXSampleRate;
|
property TXSampleRate: Integer read FTXSampleRate;
|
||||||
@@ -768,6 +786,10 @@ begin
|
|||||||
FWfDetector := 0; // Peak
|
FWfDetector := 0; // Peak
|
||||||
FWfAvgMode := 3; // Log Recursive
|
FWfAvgMode := 3; // Log Recursive
|
||||||
FWfAvgTimeMS := 120.0;
|
FWfAvgTimeMS := 120.0;
|
||||||
|
FZoomFactor := 0.0; // 0.0 = весь span, без зума
|
||||||
|
FPanSlider := 0.5; // окно по центру
|
||||||
|
FViewLowHz := -FSampleRate / 2.0;
|
||||||
|
FViewHighHz := FSampleRate / 2.0;
|
||||||
|
|
||||||
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
SetLength(FRXIn, FBufSize * 2); // in_size пар @ FSampleRate (4096*2)
|
||||||
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
SetLength(FRXOut, FAudioBufSize * 2); // out_size пар @ FAudioRate (1024*2)
|
||||||
@@ -906,7 +928,11 @@ procedure TWDSPEngine.ApplyRXAnalyzerSettings;
|
|||||||
var
|
var
|
||||||
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
AnalyzerFFT, AvgCount, WfAvgCount, MaxW, Overlap, DisplayPixels: Integer;
|
||||||
Backmult, WfBackmult: Double;
|
Backmult, WfBackmult: Double;
|
||||||
const KEEP_TIME_SEC = 0.10;
|
Bins, SpanClipL, SpanClipH, ZWidth: Integer;
|
||||||
|
BinWidth, BW, ZoomSlider: Double;
|
||||||
|
const
|
||||||
|
KEEP_TIME_SEC = 0.10;
|
||||||
|
ZOOM_LIMIT = 100.0; // максимальная кратность зума
|
||||||
begin
|
begin
|
||||||
AnalyzerFFT := CalcDisplayFFTSize;
|
AnalyzerFFT := CalcDisplayFFTSize;
|
||||||
FDispPos := 0;
|
FDispPos := 0;
|
||||||
@@ -919,8 +945,25 @@ begin
|
|||||||
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
AverageTimeToParams(FSpecAvgMode, FSpecAvgTimeMS, AvgCount, Backmult);
|
||||||
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
AverageTimeToParams(FWfAvgMode, FWfAvgTimeMS, WfAvgCount, WfBackmult);
|
||||||
|
|
||||||
|
// Zoom/pan: считаем span-clip для SetAnalyzer (fscLin/fscHin).
|
||||||
|
// clp=0 — оставляем полный span FFT, чтобы при зуме 0.0 видимая полоса была
|
||||||
|
// ровно == FSampleRate (остальной код это предполагает). Зум делает только
|
||||||
|
// span-clip: FZoomFactor=0.0 → ZWidth=Bins → SpanClipL=SpanClipH=0.
|
||||||
|
BinWidth := FSampleRate / AnalyzerFFT;
|
||||||
|
Bins := AnalyzerFFT;
|
||||||
|
BW := Bins * BinWidth;
|
||||||
|
ZoomSlider := Log10(9.0 * EnsureRange(FZoomFactor, 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(FPanSlider, 0.0, 1.0) * (Bins - ZWidth));
|
||||||
|
SpanClipH := Bins - ZWidth - SpanClipL;
|
||||||
|
// Видимое окно в Гц относительно DDC-центра (low отрицательный). Поправка
|
||||||
|
// BinWidth/2 — у комплексного FFT на одну отрицательную ячейку больше.
|
||||||
|
FViewLowHz := -(0.5 * BW - SpanClipL * BinWidth + BinWidth / 2.0);
|
||||||
|
FViewHighHz := (0.5 * BW - SpanClipH * BinWidth - BinWidth / 2.0);
|
||||||
|
|
||||||
SetAnalyzer(RX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
SetAnalyzer(RX_DISP_ID, 2, 1, 1, @FFlp[0], AnalyzerFFT, DISPLAY_BLOCK_SIZE,
|
||||||
FWindowType, 14.0, Overlap, 0, 0.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(RX_DISP_ID, 0, FSpecDetector);
|
SetDisplayDetectorMode(RX_DISP_ID, 0, FSpecDetector);
|
||||||
SetDisplayAverageMode (RX_DISP_ID, 0, FSpecAvgMode);
|
SetDisplayAverageMode (RX_DISP_ID, 0, FSpecAvgMode);
|
||||||
@@ -978,6 +1021,16 @@ begin
|
|||||||
ApplyTXAnalyzerSettings;
|
ApplyTXAnalyzerSettings;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.SetZoomPan(AZoom, APan: Double);
|
||||||
|
// Задаёт зум/пан и переармирует RX-анализатор (SetAnalyzer внутри WDSP
|
||||||
|
// защищён собственной критической секцией, дисплей-поток не мешает).
|
||||||
|
begin
|
||||||
|
FZoomFactor := EnsureRange(AZoom, 0.0, 1.0);
|
||||||
|
FPanSlider := EnsureRange(APan, 0.0, 1.0);
|
||||||
|
if FAnalyzerOpen then
|
||||||
|
ApplyRXAnalyzerSettings;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.CloseAnalyzer;
|
procedure TWDSPEngine.CloseAnalyzer;
|
||||||
begin
|
begin
|
||||||
if not FAnalyzerOpen then Exit;
|
if not FAnalyzerOpen then Exit;
|
||||||
|
|||||||
+12
-7
@@ -369,13 +369,14 @@ procedure TWebAdapter.OnBeaconSeed(Frac: Double);
|
|||||||
begin FSyncFreq := Frac; FController.Invoke(@SyncBeaconSeed); end;
|
begin FSyncFreq := Frac; FController.Invoke(@SyncBeaconSeed); end;
|
||||||
|
|
||||||
procedure TWebAdapter.SyncBeaconSeed;
|
procedure TWebAdapter.SyncBeaconSeed;
|
||||||
var Hz: Double;
|
var Hz, VC, VS: Double;
|
||||||
begin
|
begin
|
||||||
// Frac (доля 0..1 по ширине спектра) → абс. display-Hz от ЖИВЫХ центра/спана
|
// Frac (доля 0..1 по ширине спектра) → абс. display-Hz от ЖИВОГО видимого окна
|
||||||
// контроллера (зеркалит десктоп PixelToFreq(X,W,FCenterFreq,FSpanHz)). Так
|
// контроллера (зеркалит десктоп PixelToFreq с учётом зума). Так наведение
|
||||||
// наведение точно, даже когда LO ретюнится локом и клиентский center отстаёт.
|
// точно, даже когда LO ретюнится локом и клиентский center отстаёт.
|
||||||
if (FSyncFreq < 0) or (FSyncFreq > 1) then Exit;
|
if (FSyncFreq < 0) or (FSyncFreq > 1) then Exit;
|
||||||
Hz := FController.FCenterFreq + (FSyncFreq - 0.5) * FController.FSpanHz;
|
FController.GetViewWindow(VC, VS);
|
||||||
|
Hz := VC + (FSyncFreq - 0.5) * VS;
|
||||||
FController.BeaconSeedAtHz(Hz);
|
FController.BeaconSeedAtHz(Hz);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -466,7 +467,11 @@ procedure TWebAdapter.PushState;
|
|||||||
var
|
var
|
||||||
StatusText, BoardText, IPText, SupplyText: string;
|
StatusText, BoardText, IPText, SupplyText: string;
|
||||||
PLLText, RXText, TXText, SeqText: string;
|
PLLText, RXText, TXText, SeqText: string;
|
||||||
|
ViewCenter, ViewSpan: Double;
|
||||||
begin
|
begin
|
||||||
|
// Видимое окно (центр+span) с учётом зума: пиксели спектра уже зумлены
|
||||||
|
// анализатором, web-метки/маппинг должны соответствовать.
|
||||||
|
FController.GetViewWindow(ViewCenter, ViewSpan);
|
||||||
if FController.FNetwork.Connected then
|
if FController.FNetwork.Connected then
|
||||||
begin
|
begin
|
||||||
if FController.FRunning then StatusText := 'Running'
|
if FController.FRunning then StatusText := 'Running'
|
||||||
@@ -558,13 +563,13 @@ begin
|
|||||||
FController.FWaterfallBuf,
|
FController.FWaterfallBuf,
|
||||||
FController.FLastSMeter,
|
FController.FLastSMeter,
|
||||||
FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop,
|
FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop,
|
||||||
FController.FSpanHz, FController.FVolume,
|
ViewSpan, FController.FVolume,
|
||||||
FController.FWfAGCEnabled, FController.FWfNFEnabled,
|
FController.FWfAGCEnabled, FController.FWfNFEnabled,
|
||||||
FController.FCurrentBand,
|
FController.FCurrentBand,
|
||||||
FController.FRunning and FController.FNetwork.Connected,
|
FController.FRunning and FController.FNetwork.Connected,
|
||||||
FController.FRunning, FController.FMuted, FController.FCTun,
|
FController.FRunning, FController.FMuted, FController.FCTun,
|
||||||
FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF,
|
FController.FNRMode, FController.FNBMode, FController.FSNB, FController.FANF,
|
||||||
FController.FCenterFreq, FController.FFilter,
|
ViewCenter, FController.FFilter,
|
||||||
FController.FVfoB, FController.FActiveVfo,
|
FController.FVfoB, FController.FActiveVfo,
|
||||||
FController.FTransmitting, FController.FDrivePercent,
|
FController.FTransmitting, FController.FDrivePercent,
|
||||||
FController.FAtten, FController.FTuning, FController.FDisplayDuplex,
|
FController.FAtten, FController.FTuning, FController.FDisplayDuplex,
|
||||||
|
|||||||
Reference in New Issue
Block a user