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:
2026-06-30 21:18:24 +03:00
co-authored by Claude Opus 4.8
parent 0e550dd9f9
commit 371b2858bd
6 changed files with 814 additions and 22 deletions
+7
View File
@@ -67,6 +67,8 @@ type
FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz)
FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS
FMRptOffsetHz: Double; // Hz, 0 = use band default
ZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
end;
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
@@ -407,6 +409,8 @@ begin
B.FMStepIdx := 3; // 25 kHz
B.FMRptDir := 0;
B.FMRptOffsetHz := 0.0;
B.ZoomFactor := 0.0;
B.PanSlider := 0.5;
end;
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,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
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;
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.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
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;
procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean);