mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
refactor(slices): этап 3.0A — панадаптер выделен в TPanafallPanel
PanafallPanel.pas: view (CPU/GL), контролы стека (спектр/линейка/сплиттер/ водопад/пан-зум/кнопки зума), геометрия Layout, drag сплиттера и мышь пан/зум-полосы. MainForm работает через алиасы (FSpecView/PbSpectrum/… = FPan.*), размеры областей — FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight. Семантика мыши/флаги/тик пока в MainForm (фаза B). GL-аудит per-instance пройден (глобальных GL-ресурсов нет). План: doc/SLICES_PLAN.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+71
-393
@@ -33,7 +33,7 @@ uses
|
||||
WebServer, WebAdapter,
|
||||
CATAdapter,
|
||||
SpectrumView, SpectrumViewOpengl,
|
||||
PanZoomBar,
|
||||
PanZoomBar, PanafallPanel,
|
||||
WidebandView,
|
||||
StatusBar,
|
||||
PlatformUtils,
|
||||
@@ -156,12 +156,14 @@ type
|
||||
// живут в TRadioController.
|
||||
|
||||
// ---- Spectrum / Waterfall view ----
|
||||
// Панадаптер (спектр+линейка+сплиттер+водопад+пан/зум) — composite-объект
|
||||
// (Этап 3.0 мультислайсов). FSpecView и Pb*-поля ниже — алиасы его членов
|
||||
// (исторические имена; постепенная миграция на FPan.*). Размеры областей —
|
||||
// FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight.
|
||||
FPan: TPanafallPanel;
|
||||
FSpecView: TSpectrumView;
|
||||
FWidebandView: TWidebandView;
|
||||
FUseOpenGLSpectrum: Boolean;
|
||||
FSpectrumWidth: Integer; // актуальная ширина для FDSPEngine и resize-детектора
|
||||
FSpectrumHeight: Integer;
|
||||
FWaterfallHeight:Integer;
|
||||
FDisplayFPS: Integer;
|
||||
FWaterfallDirty: Boolean;
|
||||
FWidebandDirty: Boolean;
|
||||
@@ -170,11 +172,6 @@ type
|
||||
// Снимок последнего кадра спектра/водопада для web переехал в TRadioController
|
||||
// (FSpectrumBuf/FWaterfallBuf) — нужен headless-демону.
|
||||
FAgcLineCounter: Integer;
|
||||
// ---- Splitter ----
|
||||
FSplitterDrag: Boolean;
|
||||
FSplitterDragY0: Integer;
|
||||
FSplitterSH0: Integer;
|
||||
FSplitterRatio: Double;
|
||||
// --- Settings ---
|
||||
FWebServer: TWebServer;
|
||||
FWebAdapter: TWebAdapter; // мост web-сервер ↔ контроллер (вынесенные WebOnXxx)
|
||||
@@ -301,6 +298,7 @@ type
|
||||
|
||||
// ---- Right panel ----
|
||||
PanelRight: TPanel;
|
||||
// Контролы панадаптера — алиасы членов FPan (владелец FPan; см. выше).
|
||||
PbSpectrum: TControl;
|
||||
PbWideband: TControl;
|
||||
PbWidebandRuler: TPaintBox;
|
||||
@@ -454,18 +452,11 @@ 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);
|
||||
// ---- Пан/зум (мышь полосы/сплиттера — внутри TPanafallPanel) ----
|
||||
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);
|
||||
@@ -577,13 +568,6 @@ type
|
||||
procedure SpectrumTimerTick(Sender: TObject);
|
||||
procedure RightPanelResize(Sender: TObject);
|
||||
procedure AfterShowTick(Sender: TObject);
|
||||
// Splitter handlers
|
||||
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
|
||||
public
|
||||
// Методы для SettingsForm — немедленное применение настроек
|
||||
@@ -1008,16 +992,15 @@ begin
|
||||
FController.FSpectrumFill := True;
|
||||
FDisplayFPS := 60;
|
||||
FFreqMhzDigits := 3;
|
||||
FSplitterRatio := 0.40;
|
||||
FSplitterDrag := False;
|
||||
|
||||
// OpenGLContextEx (QOpenGLWidget-бэкенд) работает и в нативном Wayland,
|
||||
// старая X11-заглушка больше не нужна
|
||||
FUseOpenGLSpectrum := HasOpenGLSpectrumSwitch;
|
||||
if FUseOpenGLSpectrum then
|
||||
FSpecView := TSpectrumViewOpenGL.Create
|
||||
else
|
||||
FSpecView := TSpectrumView.Create;
|
||||
// Панадаптер: владеет view/зум-баром/контролами спектра; контролы создаст
|
||||
// FPan.Build в BuildUI. Owner=nil — освобождаем сами в FormDestroy.
|
||||
FPan := TPanafallPanel.Create(nil, FUseOpenGLSpectrum);
|
||||
FSpecView := FPan.View; // алиас (историческое имя)
|
||||
FPanZoomBar := FPan.ZoomBar; // алиас
|
||||
FSpecView.VfoOverlay := FVfoOverlay;
|
||||
FSpecView.VfoA := FController.FVfoA;
|
||||
FSpecView.VfoB := FController.FVfoB;
|
||||
@@ -1114,9 +1097,16 @@ begin
|
||||
if FController.FNetwork.Running then
|
||||
FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0);
|
||||
FController.FNetwork.Disconnect;
|
||||
FreeAndNil(FSpecView);
|
||||
// Панадаптер освобождает view, зум-бар и свои контролы; алиасы обнуляем.
|
||||
FSpecView := nil;
|
||||
FPanZoomBar := nil;
|
||||
PbSpectrum := nil;
|
||||
PbRuler := nil;
|
||||
PanelSplitter := nil;
|
||||
PbWaterfall := nil;
|
||||
PbPanZoom := nil;
|
||||
FreeAndNil(FPan);
|
||||
FreeAndNil(FWidebandView);
|
||||
FreeAndNil(FPanZoomBar);
|
||||
|
||||
// Сохраняем настройки при закрытии
|
||||
if FController.FDevConnected then
|
||||
@@ -1416,7 +1406,7 @@ begin
|
||||
// ChangeSampleRate пересоздал WDSP-канал — пересчёт AGC-линий под
|
||||
// актуальную ширину спектра (UI-зависимая величина).
|
||||
if FController.FWDSPReady then
|
||||
FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||||
FController.FDSPEngine.UpdateAGCLines(FPan.SpectrumWidth);
|
||||
UpdatePanZoomBar;
|
||||
end;
|
||||
rfZoom:
|
||||
@@ -2215,190 +2205,42 @@ begin
|
||||
PbWidebandRuler.Cursor := crDefault;
|
||||
PbWidebandRuler.Visible := False;
|
||||
|
||||
if FUseOpenGLSpectrum then
|
||||
begin
|
||||
PbSpectrum := TOpenGLControl.Create(Self);
|
||||
TOpenGLControl(PbSpectrum).AutoResizeViewport := False;
|
||||
TOpenGLControl(PbSpectrum).DoubleBuffered := True;
|
||||
TOpenGLControl(PbSpectrum).MultiSampling := Max(1, FController.FSpecMSAA);
|
||||
TOpenGLControl(PbSpectrum).OnPaint := FSpecView.PaintSpectrum;
|
||||
TOpenGLControl(PbSpectrum).OnMouseDown := PbSpectrumMouseDown;
|
||||
TOpenGLControl(PbSpectrum).OnDblClick := PbSpectrumDblClick;
|
||||
TOpenGLControl(PbSpectrum).OnMouseMove := PbSpectrumMouseMove;
|
||||
TOpenGLControl(PbSpectrum).OnMouseUp := PbSpectrumMouseUp;
|
||||
TOpenGLControl(PbSpectrum).OnMouseLeave := PbSpectrumMouseLeave;
|
||||
TSpectrumViewOpenGL(FSpecView).AttachControl(TOpenGLControl(PbSpectrum));
|
||||
end
|
||||
else
|
||||
begin
|
||||
PbSpectrum := TPaintBox.Create(Self);
|
||||
TPaintBox(PbSpectrum).OnPaint := FSpecView.PaintSpectrum;
|
||||
TPaintBox(PbSpectrum).OnMouseDown := PbSpectrumMouseDown;
|
||||
TPaintBox(PbSpectrum).OnDblClick := PbSpectrumDblClick;
|
||||
TPaintBox(PbSpectrum).OnMouseMove := PbSpectrumMouseMove;
|
||||
TPaintBox(PbSpectrum).OnMouseUp := PbSpectrumMouseUp;
|
||||
TPaintBox(PbSpectrum).OnMouseLeave := PbSpectrumMouseLeave;
|
||||
end;
|
||||
PbSpectrum.Parent := PanelRight;
|
||||
|
||||
PbRuler := TPaintBox.Create(Self);
|
||||
PbRuler.Parent := PanelRight;
|
||||
PbRuler.OnPaint := FSpecView.PaintRuler;
|
||||
PbRuler.Cursor := crDefault;
|
||||
FSpecView.PbRuler := PbRuler;
|
||||
|
||||
// ---- Splitter между спектром+линейкой и водопадом ----
|
||||
PanelSplitter := TPanel.Create(Self);
|
||||
PanelSplitter.Parent := PanelRight;
|
||||
PanelSplitter.BevelOuter := bvNone;
|
||||
PanelSplitter.Color := TColor($00303030);
|
||||
PanelSplitter.Cursor := crVSplit;
|
||||
PanelSplitter.Height := 5;
|
||||
PanelSplitter.OnMouseDown := SplitterMouseDown;
|
||||
PanelSplitter.OnMouseMove := SplitterMouseMove;
|
||||
PanelSplitter.OnMouseUp := SplitterMouseUp;
|
||||
|
||||
if FUseOpenGLSpectrum then
|
||||
begin
|
||||
PbWaterfall := TOpenGLControl.Create(Self);
|
||||
TOpenGLControl(PbWaterfall).AutoResizeViewport := False;
|
||||
TOpenGLControl(PbWaterfall).DoubleBuffered := True;
|
||||
TOpenGLControl(PbWaterfall).OnPaint := FSpecView.PaintWaterfall;
|
||||
TOpenGLControl(PbWaterfall).OnMouseDown := PbWaterfallMouseDown;
|
||||
TOpenGLControl(PbWaterfall).OnMouseMove := PbWaterfallMouseMove;
|
||||
TOpenGLControl(PbWaterfall).OnMouseUp := PbWaterfallMouseUp;
|
||||
TSpectrumViewOpenGL(FSpecView).AttachWaterfallControl(TOpenGLControl(PbWaterfall));
|
||||
end
|
||||
else
|
||||
begin
|
||||
PbWaterfall := TPaintBox.Create(Self);
|
||||
TPaintBox(PbWaterfall).OnPaint := FSpecView.PaintWaterfall;
|
||||
TPaintBox(PbWaterfall).OnMouseDown := PbWaterfallMouseDown;
|
||||
TPaintBox(PbWaterfall).OnMouseMove := PbWaterfallMouseMove;
|
||||
TPaintBox(PbWaterfall).OnMouseUp := PbWaterfallMouseUp;
|
||||
end;
|
||||
PbWaterfall.Parent := PanelRight;
|
||||
|
||||
// ---- Панель пана/зума под водопадом ----
|
||||
// Кнопки создаём с дефолтными цветами; финальная тема — в ApplyDarkTheme.
|
||||
FPanZoomBar := TPanZoomBar.Create;
|
||||
// ---- Панадаптер: контролы спектра/линейки/сплиттера/водопада/пан-зума ----
|
||||
// Создаёт и раскладывает TPanafallPanel; семантика мыши остаётся здесь —
|
||||
// обработчики подвешиваются ДО Build.
|
||||
FPan.SpectrumMouseDown := PbSpectrumMouseDown;
|
||||
FPan.SpectrumMouseMove := PbSpectrumMouseMove;
|
||||
FPan.SpectrumMouseUp := PbSpectrumMouseUp;
|
||||
FPan.SpectrumMouseLeave := PbSpectrumMouseLeave;
|
||||
FPan.SpectrumDblClick := PbSpectrumDblClick;
|
||||
FPan.WaterfallMouseDown := PbWaterfallMouseDown;
|
||||
FPan.WaterfallMouseMove := PbWaterfallMouseMove;
|
||||
FPan.WaterfallMouseUp := PbWaterfallMouseUp;
|
||||
FPan.ZoomInClick := BtnZoomInClick;
|
||||
FPan.ZoomDefClick := BtnZoomDefClick;
|
||||
FPan.ZoomOutClick := BtnZoomOutClick;
|
||||
FPan.OnSplitterMoved := RightPanelResize;
|
||||
FPan.Build(PanelRight, Max(1, FController.FSpecMSAA));
|
||||
// Алиасы контролов (исторические имена; постепенная миграция на FPan.*)
|
||||
PbSpectrum := FPan.PbSpectrum;
|
||||
PbRuler := FPan.PbRuler;
|
||||
PanelSplitter := FPan.Splitter;
|
||||
PbWaterfall := FPan.PbWaterfall;
|
||||
PbPanZoom := FPan.PbPanZoom;
|
||||
BtnZoomIn := FPan.BtnZoomIn;
|
||||
BtnZoomDef := FPan.BtnZoomDef;
|
||||
BtnZoomOut := FPan.BtnZoomOut;
|
||||
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;
|
||||
|
||||
// ===========================================================================
|
||||
// Splitter — перетаскивание границы спектр/водопад
|
||||
// Обновление размеров происходит только при отпускании мыши (MouseUp),
|
||||
// чтобы исключить фризы во время перетаскивания.
|
||||
// Во время перетаскивания рисуем только призрак-линию на PanelSplitter.
|
||||
// ===========================================================================
|
||||
|
||||
procedure TMainForm.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
P: TPoint;
|
||||
begin
|
||||
if Button <> mbLeft then Exit;
|
||||
FSplitterDrag := True;
|
||||
FSplitterSH0 := FSpectrumHeight;
|
||||
// Y в координатах PanelRight
|
||||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||||
FSplitterDragY0 := P.Y;
|
||||
PanelSplitter.Color := TColor($005050A0); // подсветка при захвате
|
||||
{$IFDEF WINDOWS}
|
||||
SetCapture(PanelSplitter.Handle);
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
procedure TMainForm.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
P: TPoint;
|
||||
DeltaY: Integer;
|
||||
NewSH: Integer;
|
||||
TopOff, AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
MIN_SH = 60;
|
||||
MIN_WH = 40;
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if not FSplitterDrag then Exit;
|
||||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||||
|
||||
// Только двигаем сплиттер визуально — без пересчёта битмапов
|
||||
PanelSplitter.Top := TopOff + NewSH + RULER_H;
|
||||
PbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H;
|
||||
PbWaterfall.Height := AvailH - NewSH;
|
||||
end;
|
||||
|
||||
procedure TMainForm.SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
P: TPoint;
|
||||
DeltaY: Integer;
|
||||
NewSH: Integer;
|
||||
TopOff, AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
MIN_SH = 60;
|
||||
MIN_WH = 40;
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if not FSplitterDrag then Exit;
|
||||
FSplitterDrag := False;
|
||||
{$IFDEF WINDOWS}
|
||||
ReleaseCapture;
|
||||
{$ENDIF}
|
||||
PanelSplitter.Color := TColor($00303030); // обычный цвет
|
||||
|
||||
P := PanelRight.ScreenToClient(PanelSplitter.ClientToScreen(Point(X, Y)));
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := PanelRight.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||||
|
||||
// Сохраняем новое соотношение и делаем полный пересчёт с перерисовкой
|
||||
FSplitterRatio := NewSH / AvailH;
|
||||
ResizeSpectrumPanels;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Resize handler (normal method instead of anonymous procedure)
|
||||
// Сплиттер спектр/водопад живёт в TPanafallPanel; по OnSplitterMoved панель
|
||||
// зовёт RightPanelResize → полный пересчёт.
|
||||
// ===========================================================================
|
||||
|
||||
procedure TMainForm.RightPanelResize(Sender: TObject);
|
||||
@@ -2581,27 +2423,22 @@ end;
|
||||
|
||||
procedure TMainForm.ResizeSpectrumPanels;
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
WB_H = 118;
|
||||
MIN_SH = 60; // минимальная высота спектра
|
||||
MIN_WH = 40; // минимальная высота водопада
|
||||
WB_H = 118;
|
||||
var
|
||||
RW, RH, SH, WH, TopOff, WideH, WideSpecH: Integer;
|
||||
AvailH: Integer;
|
||||
RW, RH, WideH, WideSpecH: 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;
|
||||
// Резервируем низ под панель пана/зума (видна, если виден спектр или водопад).
|
||||
// Низ занят панелью пана/зума (позиционирует FPan.Layout той же формулой) —
|
||||
// wideband-блок делит оставшуюся высоту.
|
||||
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;
|
||||
if FController.FShowSpectrum or FController.FShowWaterfall then
|
||||
RH := RH - PANZOOM_H;
|
||||
UpdateWidebandFrequencyView;
|
||||
WideH := 0;
|
||||
WideSpecH := 0;
|
||||
@@ -2609,7 +2446,6 @@ begin
|
||||
WideH := Min(MulDiv(WB_H, Screen.PixelsPerInch, 96), Max(40, RH div 2));
|
||||
if WideH > 0 then
|
||||
WideSpecH := Max(1, WideH - RULER_H);
|
||||
TopOff := WideH;
|
||||
if PbWideband <> nil then
|
||||
begin
|
||||
PbWideband.SetBounds(0, 0, RW, WideSpecH);
|
||||
@@ -2633,124 +2469,9 @@ begin
|
||||
// S-метр позиционируется отдельно в ResizeSMeter
|
||||
ResizeSMeter;
|
||||
|
||||
// ---- Оба скрыты ----
|
||||
if (not FController.FShowSpectrum) and (not FController.FShowWaterfall) then
|
||||
begin
|
||||
PbSpectrum.Visible := False;
|
||||
PbRuler.Visible := False;
|
||||
PanelSplitter.Visible := False;
|
||||
PbWaterfall.Visible := False;
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := 0;
|
||||
FWaterfallHeight := 0;
|
||||
PositionSampleRateOverlay;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// ---- Только спектр (водопад скрыт) ----
|
||||
if FController.FShowSpectrum and (not FController.FShowWaterfall) then
|
||||
begin
|
||||
AvailH := RH - TopOff - RULER_H;
|
||||
if AvailH < MIN_SH then AvailH := MIN_SH;
|
||||
SH := AvailH;
|
||||
WH := 0;
|
||||
PbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||||
PbSpectrum.Visible := True;
|
||||
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||||
PbRuler.Visible := True;
|
||||
PanelSplitter.Visible := False;
|
||||
PbWaterfall.Visible := False;
|
||||
PositionSampleRateOverlay;
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := SH;
|
||||
FWaterfallHeight := 0;
|
||||
if RW > 0 then
|
||||
begin
|
||||
FSpecView.SetSpectrumBitmapSize(RW, SH);
|
||||
FSpecView.SetWaterfallBitmapSize(1, 1);
|
||||
FSpecView.SetRulerSize(RW, RULER_H);
|
||||
FSpecView.DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
PbRuler.Invalidate;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
|
||||
if (not FController.FShowSpectrum) and FController.FShowWaterfall then
|
||||
begin
|
||||
AvailH := RH - TopOff - RULER_H;
|
||||
if AvailH < MIN_WH then AvailH := MIN_WH;
|
||||
SH := 0;
|
||||
WH := AvailH;
|
||||
PbSpectrum.Visible := False;
|
||||
PanelSplitter.Visible := False;
|
||||
PbWaterfall.SetBounds(0, TopOff, RW, WH);
|
||||
PbWaterfall.Visible := True;
|
||||
PbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
|
||||
PbRuler.Visible := True;
|
||||
PositionSampleRateOverlay;
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := 0;
|
||||
FWaterfallHeight := WH;
|
||||
if RW > 0 then
|
||||
begin
|
||||
FSpecView.SetSpectrumBitmapSize(1, 1);
|
||||
FSpecView.SetWaterfallBitmapSize(RW, WH);
|
||||
FSpecView.SetRulerSize(RW, RULER_H);
|
||||
FSpecView.DrawWaterfall;
|
||||
PbWaterfall.Invalidate;
|
||||
PbRuler.Invalidate;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// ---- Оба видимы: стандартный режим со сплиттером ----
|
||||
PbSpectrum.Visible := True;
|
||||
PbRuler.Visible := True;
|
||||
PanelSplitter.Visible := True;
|
||||
PbWaterfall.Visible := True;
|
||||
|
||||
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
||||
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
||||
|
||||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||||
if FSplitterRatio < MIN_SH / AvailH then
|
||||
FSplitterRatio := MIN_SH / AvailH;
|
||||
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
|
||||
FSplitterRatio := 1.0 - MIN_WH / AvailH;
|
||||
|
||||
SH := Round(AvailH * FSplitterRatio);
|
||||
WH := AvailH - SH;
|
||||
if WH < MIN_WH then WH := MIN_WH;
|
||||
|
||||
// Спектр
|
||||
PbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||||
// Линейка частот — всегда прижата к низу спектра
|
||||
PbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||||
// Сплиттер — между линейкой и водопадом
|
||||
PanelSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
|
||||
// Водопад — под сплиттером
|
||||
PbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
|
||||
|
||||
// Стек спектр/линейка/сплиттер/водопад/пан-зум раскладывает панадаптер.
|
||||
FPan.Layout(WideH, FController.FShowSpectrum, FController.FShowWaterfall);
|
||||
PositionSampleRateOverlay;
|
||||
|
||||
// Обновляем переменные размеров
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := SH;
|
||||
FWaterfallHeight := WH;
|
||||
|
||||
// Пересоздаём bitmap точно под новый размер
|
||||
if RW > 0 then
|
||||
begin
|
||||
if SH > 0 then FSpecView.SetSpectrumBitmapSize(RW, SH);
|
||||
if WH > 0 then FSpecView.SetWaterfallBitmapSize(RW, WH);
|
||||
FSpecView.SetRulerSize(RW, RULER_H);
|
||||
FSpecView.DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
if PbRuler <> nil then PbRuler.Invalidate;
|
||||
PbWaterfall.Invalidate;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
@@ -3234,17 +2955,17 @@ var
|
||||
begin
|
||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
|
||||
// PbSpectrum.Height может быть ненулевым при FPan.SpectrumHeight=0,
|
||||
// что вызвало бы сброс буфера водопада на каждом тике.
|
||||
if (FSpectrumWidth = 0) or
|
||||
(FController.FShowWideband and (PbWideband.Width <> FSpectrumWidth)) or
|
||||
(FController.FShowSpectrum and ((PbSpectrum.Width <> FSpectrumWidth) or
|
||||
(PbSpectrum.Height <> FSpectrumHeight))) or
|
||||
if (FPan.SpectrumWidth = 0) or
|
||||
(FController.FShowWideband and (PbWideband.Width <> FPan.SpectrumWidth)) or
|
||||
(FController.FShowSpectrum and ((PbSpectrum.Width <> FPan.SpectrumWidth) or
|
||||
(PbSpectrum.Height <> FPan.SpectrumHeight))) or
|
||||
(FController.FShowWaterfall and not FController.FShowSpectrum and
|
||||
((PbWaterfall.Width <> FSpectrumWidth) or
|
||||
(PbWaterfall.Height <> FWaterfallHeight))) then
|
||||
((PbWaterfall.Width <> FPan.SpectrumWidth) or
|
||||
(PbWaterfall.Height <> FPan.WaterfallHeight))) then
|
||||
ResizeSpectrumPanels;
|
||||
if FSpectrumWidth <= 0 then Exit;
|
||||
if FPan.SpectrumWidth <= 0 then Exit;
|
||||
SyncSpecViewFreq;
|
||||
|
||||
// Проверка таймаута — трансивер не отвечает
|
||||
@@ -3290,11 +3011,11 @@ begin
|
||||
if FController.FWDSPReady then
|
||||
begin
|
||||
FController.FLastSMeter := FController.FDSPEngine.GetSMeterDBm;
|
||||
FController.FDSPEngine.SetSpectrumWidth(FSpectrumWidth);
|
||||
FController.FDSPEngine.SetSpectrumWidth(FPan.SpectrumWidth);
|
||||
Inc(FAgcLineCounter);
|
||||
if FAgcLineCounter >= 6 then
|
||||
begin
|
||||
FController.FDSPEngine.UpdateAGCLines(FSpectrumWidth);
|
||||
FController.FDSPEngine.UpdateAGCLines(FPan.SpectrumWidth);
|
||||
FSpecView.AGCThresh := FController.FDSPEngine.AGCThresh;
|
||||
FSpecView.AGCHangLevel := FController.FDSPEngine.AGCHangLevel;
|
||||
FAgcLineCounter := 0;
|
||||
@@ -3350,9 +3071,9 @@ begin
|
||||
if not FController.FNetwork.Connected then
|
||||
begin
|
||||
// При первом старте показываем демо-спектр
|
||||
if FSpecView.SpectrumBitmapWidth <> FSpectrumWidth then
|
||||
if FSpecView.SpectrumBitmapWidth <> FPan.SpectrumWidth then
|
||||
begin
|
||||
FSpecView.SetSpectrumBitmapSize(FSpectrumWidth, FSpectrumHeight);
|
||||
FSpecView.SetSpectrumBitmapSize(FPan.SpectrumWidth, FPan.SpectrumHeight);
|
||||
FSpecView.ResetSpectrumBuf;
|
||||
FSpecView.DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
@@ -3891,7 +3612,7 @@ begin
|
||||
// вся оркестрация в контроллере (ConnectAndRun); рендер device/band/run-
|
||||
// виджетов идёт через события (rfDevice/rfBandRestore/rfXvtr/rfActiveVfo/
|
||||
// rfRunning). Здесь — только UI-extras (статус-строки/AGCTop/resize).
|
||||
if not FController.ConnectAndRun(Dev, FSpectrumWidth) then
|
||||
if not FController.ConnectAndRun(Dev, FPan.SpectrumWidth) then
|
||||
begin
|
||||
ShowMessage('Failed to connect to ' + Dev.IPAddress +
|
||||
IfThen(FController.FNetwork.LastError <> '', ': ' + FController.FNetwork.LastError, ''));
|
||||
@@ -4437,27 +4158,6 @@ 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
|
||||
@@ -4475,28 +4175,6 @@ begin
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
unit PanafallPanel;
|
||||
|
||||
{
|
||||
TPanafallPanel — панадаптер (спектр + линейка частот + сплиттер + водопад +
|
||||
полоса пана/зума с кнопками) как единый composite-объект.
|
||||
|
||||
Этап 3.0 плана мультислайсов (doc/SLICES_PLAN.md): вынос панадаптера из
|
||||
MainForm без изменения поведения. Панель владеет:
|
||||
• конструированием контролов (Build) и SpectrumView (CPU или OpenGL);
|
||||
• геометрией стека спектр/линейка/сплиттер/водопад/пан-зум (Layout);
|
||||
• механикой сплиттера (drag с призраком, пересчёт по MouseUp);
|
||||
• прокидкой мыши пан/зум-полосы в TPanZoomBar.
|
||||
Семантика мыши спектра/водопада (тюн, слайсы, маркер, оверлеи) остаётся у
|
||||
хозяина — он подвешивает свои обработчики через публичные поля Spectrum*/
|
||||
Waterfall*/Zoom*Click ДО вызова Build.
|
||||
|
||||
Хозяин по-прежнему сам синхронизирует состояние view (частоты, палитры,
|
||||
wf-настройки) и решает, когда звать Layout (resize, show/hide, сплиттер).
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
{$MODE Delphi}
|
||||
{$ENDIF}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, Controls, ExtCtrls, Graphics, Forms,
|
||||
LCLIntf, LCLType,
|
||||
OpenGLContextEx,
|
||||
SpectrumView, SpectrumViewOpengl, PanZoomBar, FlatButton;
|
||||
|
||||
type
|
||||
TPanafallPanel = class(TComponent)
|
||||
private
|
||||
FParent: TWinControl;
|
||||
FUseOpenGL: Boolean;
|
||||
FView: TSpectrumView;
|
||||
FPbSpectrum: TControl;
|
||||
FPbRuler: TPaintBox;
|
||||
FSplitter: TPanel;
|
||||
FPbWaterfall: TControl;
|
||||
FPbPanZoom: TPaintBox;
|
||||
FZoomBar: TPanZoomBar;
|
||||
FBtnZoomIn: TFlatButton;
|
||||
FBtnZoomDef: TFlatButton;
|
||||
FBtnZoomOut: TFlatButton;
|
||||
|
||||
// Актуальные размеры областей после Layout (для DSP-движка и
|
||||
// resize-детектора хозяина).
|
||||
FSpectrumWidth: Integer;
|
||||
FSpectrumHeight: Integer;
|
||||
FWaterfallHeight: Integer;
|
||||
FShowSpectrum: Boolean;
|
||||
FShowWaterfall: Boolean;
|
||||
FSplitterRatio: Double;
|
||||
|
||||
// Сплиттер-драг: обновление размеров только при отпускании мыши (MouseUp),
|
||||
// чтобы исключить фризы; во время перетаскивания двигаем контролы визуально.
|
||||
FSplitterDrag: Boolean;
|
||||
FSplitterDragY0: Integer;
|
||||
FSplitterSH0: Integer;
|
||||
|
||||
FOnSplitterMoved: TNotifyEvent;
|
||||
|
||||
procedure SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
procedure SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanZoomMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanZoomMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanZoomMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure PanZoomDblClick(Sender: TObject);
|
||||
procedure PositionPanZoomBar(BottomY, RW, H: Integer; AVisible: Boolean);
|
||||
public
|
||||
// Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина —
|
||||
// задать ДО вызова Build (Build подвешивает их на создаваемые контролы).
|
||||
SpectrumMouseDown: TMouseEvent;
|
||||
SpectrumMouseMove: TMouseMoveEvent;
|
||||
SpectrumMouseUp: TMouseEvent;
|
||||
SpectrumMouseLeave: TNotifyEvent;
|
||||
SpectrumDblClick: TNotifyEvent;
|
||||
WaterfallMouseDown: TMouseEvent;
|
||||
WaterfallMouseMove: TMouseMoveEvent;
|
||||
WaterfallMouseUp: TMouseEvent;
|
||||
ZoomInClick: TNotifyEvent;
|
||||
ZoomDefClick: TNotifyEvent;
|
||||
ZoomOutClick: TNotifyEvent;
|
||||
|
||||
constructor Create(AOwner: TComponent; AUseOpenGL: Boolean); reintroduce;
|
||||
destructor Destroy; override;
|
||||
|
||||
// Создаёт контролы в AParent и подвешивает обработчики (см. поля выше).
|
||||
procedure Build(AParent: TWinControl; AMSAA: Integer);
|
||||
// Полная раскладка стека. ATopOffset — высота wideband-блока хозяина
|
||||
// над спектром. Show-флаги приходят от контроллера (FShowSpectrum/
|
||||
// FShowWaterfall).
|
||||
procedure Layout(ATopOffset: Integer; AShowSpectrum, AShowWaterfall: Boolean);
|
||||
|
||||
property View: TSpectrumView read FView;
|
||||
property PbSpectrum: TControl read FPbSpectrum;
|
||||
property PbRuler: TPaintBox read FPbRuler;
|
||||
property Splitter: TPanel read FSplitter;
|
||||
property PbWaterfall: TControl read FPbWaterfall;
|
||||
property PbPanZoom: TPaintBox read FPbPanZoom;
|
||||
property ZoomBar: TPanZoomBar read FZoomBar;
|
||||
property BtnZoomIn: TFlatButton read FBtnZoomIn;
|
||||
property BtnZoomDef: TFlatButton read FBtnZoomDef;
|
||||
property BtnZoomOut: TFlatButton read FBtnZoomOut;
|
||||
|
||||
property SpectrumWidth: Integer read FSpectrumWidth;
|
||||
property SpectrumHeight: Integer read FSpectrumHeight;
|
||||
property WaterfallHeight: Integer read FWaterfallHeight;
|
||||
property SplitterRatio: Double read FSplitterRatio write FSplitterRatio;
|
||||
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
|
||||
// полный пересчёт (у него wideband/S-метр/оверлеи).
|
||||
property OnSplitterMoved: TNotifyEvent read FOnSplitterMoved write FOnSplitterMoved;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
constructor TPanafallPanel.Create(AOwner: TComponent; AUseOpenGL: Boolean);
|
||||
begin
|
||||
inherited Create(AOwner);
|
||||
FUseOpenGL := AUseOpenGL;
|
||||
if FUseOpenGL then
|
||||
FView := TSpectrumViewOpenGL.Create
|
||||
else
|
||||
FView := TSpectrumView.Create;
|
||||
FZoomBar := TPanZoomBar.Create;
|
||||
FSplitterRatio := 0.40;
|
||||
FSplitterDrag := False;
|
||||
end;
|
||||
|
||||
destructor TPanafallPanel.Destroy;
|
||||
begin
|
||||
// Порядок как в прежнем FormDestroy: view/зум-бар первыми, контролы
|
||||
// (Owner=Self) освободит inherited.
|
||||
FreeAndNil(FZoomBar);
|
||||
FreeAndNil(FView);
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.Build(AParent: TWinControl; AMSAA: Integer);
|
||||
begin
|
||||
FParent := AParent;
|
||||
|
||||
if FUseOpenGL then
|
||||
begin
|
||||
FPbSpectrum := TOpenGLControl.Create(Self);
|
||||
TOpenGLControl(FPbSpectrum).AutoResizeViewport := False;
|
||||
TOpenGLControl(FPbSpectrum).DoubleBuffered := True;
|
||||
TOpenGLControl(FPbSpectrum).MultiSampling := Max(1, AMSAA);
|
||||
TOpenGLControl(FPbSpectrum).OnPaint := FView.PaintSpectrum;
|
||||
TOpenGLControl(FPbSpectrum).OnMouseDown := SpectrumMouseDown;
|
||||
TOpenGLControl(FPbSpectrum).OnDblClick := SpectrumDblClick;
|
||||
TOpenGLControl(FPbSpectrum).OnMouseMove := SpectrumMouseMove;
|
||||
TOpenGLControl(FPbSpectrum).OnMouseUp := SpectrumMouseUp;
|
||||
TOpenGLControl(FPbSpectrum).OnMouseLeave := SpectrumMouseLeave;
|
||||
TSpectrumViewOpenGL(FView).AttachControl(TOpenGLControl(FPbSpectrum));
|
||||
end
|
||||
else
|
||||
begin
|
||||
FPbSpectrum := TPaintBox.Create(Self);
|
||||
TPaintBox(FPbSpectrum).OnPaint := FView.PaintSpectrum;
|
||||
TPaintBox(FPbSpectrum).OnMouseDown := SpectrumMouseDown;
|
||||
TPaintBox(FPbSpectrum).OnDblClick := SpectrumDblClick;
|
||||
TPaintBox(FPbSpectrum).OnMouseMove := SpectrumMouseMove;
|
||||
TPaintBox(FPbSpectrum).OnMouseUp := SpectrumMouseUp;
|
||||
TPaintBox(FPbSpectrum).OnMouseLeave := SpectrumMouseLeave;
|
||||
end;
|
||||
FPbSpectrum.Parent := AParent;
|
||||
|
||||
FPbRuler := TPaintBox.Create(Self);
|
||||
FPbRuler.Parent := AParent;
|
||||
FPbRuler.OnPaint := FView.PaintRuler;
|
||||
FPbRuler.Cursor := crDefault;
|
||||
FView.PbRuler := FPbRuler;
|
||||
|
||||
// ---- Сплиттер между спектром+линейкой и водопадом ----
|
||||
FSplitter := TPanel.Create(Self);
|
||||
FSplitter.Parent := AParent;
|
||||
FSplitter.BevelOuter := bvNone;
|
||||
FSplitter.Color := TColor($00303030);
|
||||
FSplitter.Cursor := crVSplit;
|
||||
FSplitter.Height := 5;
|
||||
FSplitter.OnMouseDown := SplitterMouseDown;
|
||||
FSplitter.OnMouseMove := SplitterMouseMove;
|
||||
FSplitter.OnMouseUp := SplitterMouseUp;
|
||||
|
||||
if FUseOpenGL then
|
||||
begin
|
||||
FPbWaterfall := TOpenGLControl.Create(Self);
|
||||
TOpenGLControl(FPbWaterfall).AutoResizeViewport := False;
|
||||
TOpenGLControl(FPbWaterfall).DoubleBuffered := True;
|
||||
TOpenGLControl(FPbWaterfall).OnPaint := FView.PaintWaterfall;
|
||||
TOpenGLControl(FPbWaterfall).OnMouseDown := WaterfallMouseDown;
|
||||
TOpenGLControl(FPbWaterfall).OnMouseMove := WaterfallMouseMove;
|
||||
TOpenGLControl(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
|
||||
TSpectrumViewOpenGL(FView).AttachWaterfallControl(TOpenGLControl(FPbWaterfall));
|
||||
end
|
||||
else
|
||||
begin
|
||||
FPbWaterfall := TPaintBox.Create(Self);
|
||||
TPaintBox(FPbWaterfall).OnPaint := FView.PaintWaterfall;
|
||||
TPaintBox(FPbWaterfall).OnMouseDown := WaterfallMouseDown;
|
||||
TPaintBox(FPbWaterfall).OnMouseMove := WaterfallMouseMove;
|
||||
TPaintBox(FPbWaterfall).OnMouseUp := WaterfallMouseUp;
|
||||
end;
|
||||
FPbWaterfall.Parent := AParent;
|
||||
|
||||
// ---- Панель пана/зума под водопадом ----
|
||||
// Кнопки создаём с дефолтными цветами; финальная тема — у хозяина
|
||||
// (ApplyDarkTheme → StyleButton/SetTheme через алиасы).
|
||||
FPbPanZoom := TPaintBox.Create(Self);
|
||||
FPbPanZoom.Parent := AParent;
|
||||
FPbPanZoom.OnPaint := FZoomBar.Paint;
|
||||
FPbPanZoom.OnMouseDown := PanZoomMouseDown;
|
||||
FPbPanZoom.OnMouseMove := PanZoomMouseMove;
|
||||
FPbPanZoom.OnMouseUp := PanZoomMouseUp;
|
||||
FPbPanZoom.OnDblClick := PanZoomDblClick;
|
||||
FZoomBar.PaintBox := FPbPanZoom;
|
||||
FBtnZoomOut := MakeFlatBtn(AParent, '−', 0, 0, 10, 10, ZoomOutClick);
|
||||
FBtnZoomDef := MakeFlatBtn(AParent, '⌂', 0, 0, 10, 10, ZoomDefClick);
|
||||
FBtnZoomIn := MakeFlatBtn(AParent, '+', 0, 0, 10, 10, ZoomInClick);
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer;
|
||||
AVisible: Boolean);
|
||||
var BtnW, Gap, StripW, X: Integer;
|
||||
begin
|
||||
if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit;
|
||||
FPbPanZoom.Visible := AVisible;
|
||||
FBtnZoomIn.Visible := AVisible;
|
||||
FBtnZoomDef.Visible := AVisible;
|
||||
FBtnZoomOut.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;
|
||||
FPbPanZoom.SetBounds(0, BottomY, StripW, H);
|
||||
X := StripW + Gap;
|
||||
FBtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||
FBtnZoomDef.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
|
||||
FBtnZoomIn.SetBounds(X, BottomY, BtnW, H);
|
||||
FPbPanZoom.Invalidate;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.Layout(ATopOffset: Integer;
|
||||
AShowSpectrum, AShowWaterfall: Boolean);
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
MIN_SH = 60; // минимальная высота спектра
|
||||
MIN_WH = 40; // минимальная высота водопада
|
||||
var
|
||||
RW, RH, SH, WH, TopOff: Integer;
|
||||
AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
PANZOOM_H: Integer;
|
||||
ShowPZ: Boolean;
|
||||
begin
|
||||
if (FParent = nil) or (FPbSpectrum = nil) then Exit;
|
||||
FShowSpectrum := AShowSpectrum;
|
||||
FShowWaterfall := AShowWaterfall;
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
RW := FParent.ClientWidth;
|
||||
RH := FParent.ClientHeight;
|
||||
// Резервируем низ под панель пана/зума (видна, если виден спектр или водопад).
|
||||
PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
ShowPZ := AShowSpectrum or AShowWaterfall;
|
||||
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
|
||||
if ShowPZ then RH := RH - PANZOOM_H;
|
||||
TopOff := ATopOffset;
|
||||
|
||||
// ---- Оба скрыты ----
|
||||
if (not AShowSpectrum) and (not AShowWaterfall) then
|
||||
begin
|
||||
FPbSpectrum.Visible := False;
|
||||
FPbRuler.Visible := False;
|
||||
FSplitter.Visible := False;
|
||||
FPbWaterfall.Visible := False;
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := 0;
|
||||
FWaterfallHeight := 0;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// ---- Только спектр (водопад скрыт) ----
|
||||
if AShowSpectrum and (not AShowWaterfall) then
|
||||
begin
|
||||
AvailH := RH - TopOff - RULER_H;
|
||||
if AvailH < MIN_SH then AvailH := MIN_SH;
|
||||
SH := AvailH;
|
||||
FPbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||||
FPbSpectrum.Visible := True;
|
||||
FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||||
FPbRuler.Visible := True;
|
||||
FSplitter.Visible := False;
|
||||
FPbWaterfall.Visible := False;
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := SH;
|
||||
FWaterfallHeight := 0;
|
||||
if RW > 0 then
|
||||
begin
|
||||
FView.SetSpectrumBitmapSize(RW, SH);
|
||||
FView.SetWaterfallBitmapSize(1, 1);
|
||||
FView.SetRulerSize(RW, RULER_H);
|
||||
FView.DrawSpectrum;
|
||||
FPbSpectrum.Invalidate;
|
||||
FPbRuler.Invalidate;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// ---- Только водопад (спектр скрыт): линейка под водопадом ----
|
||||
if (not AShowSpectrum) and AShowWaterfall then
|
||||
begin
|
||||
AvailH := RH - TopOff - RULER_H;
|
||||
if AvailH < MIN_WH then AvailH := MIN_WH;
|
||||
WH := AvailH;
|
||||
FPbSpectrum.Visible := False;
|
||||
FSplitter.Visible := False;
|
||||
FPbWaterfall.SetBounds(0, TopOff, RW, WH);
|
||||
FPbWaterfall.Visible := True;
|
||||
FPbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
|
||||
FPbRuler.Visible := True;
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := 0;
|
||||
FWaterfallHeight := WH;
|
||||
if RW > 0 then
|
||||
begin
|
||||
FView.SetSpectrumBitmapSize(1, 1);
|
||||
FView.SetWaterfallBitmapSize(RW, WH);
|
||||
FView.SetRulerSize(RW, RULER_H);
|
||||
FView.DrawWaterfall;
|
||||
FPbWaterfall.Invalidate;
|
||||
FPbRuler.Invalidate;
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// ---- Оба видимы: стандартный режим со сплиттером ----
|
||||
FPbSpectrum.Visible := True;
|
||||
FPbRuler.Visible := True;
|
||||
FSplitter.Visible := True;
|
||||
FPbWaterfall.Visible := True;
|
||||
|
||||
AvailH := RH - TopOff - RULER_H - SPLITTER_H;
|
||||
if AvailH < (MIN_SH + MIN_WH) then Exit;
|
||||
|
||||
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
|
||||
if FSplitterRatio < MIN_SH / AvailH then
|
||||
FSplitterRatio := MIN_SH / AvailH;
|
||||
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
|
||||
FSplitterRatio := 1.0 - MIN_WH / AvailH;
|
||||
|
||||
SH := Round(AvailH * FSplitterRatio);
|
||||
WH := AvailH - SH;
|
||||
if WH < MIN_WH then WH := MIN_WH;
|
||||
|
||||
// Спектр
|
||||
FPbSpectrum.SetBounds(0, TopOff, RW, SH);
|
||||
// Линейка частот — всегда прижата к низу спектра
|
||||
FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
|
||||
// Сплиттер — между линейкой и водопадом
|
||||
FSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
|
||||
// Водопад — под сплиттером
|
||||
FPbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
|
||||
|
||||
// Обновляем переменные размеров
|
||||
FSpectrumWidth := RW;
|
||||
FSpectrumHeight := SH;
|
||||
FWaterfallHeight := WH;
|
||||
|
||||
// Пересоздаём bitmap точно под новый размер
|
||||
if RW > 0 then
|
||||
begin
|
||||
if SH > 0 then FView.SetSpectrumBitmapSize(RW, SH);
|
||||
if WH > 0 then FView.SetWaterfallBitmapSize(RW, WH);
|
||||
FView.SetRulerSize(RW, RULER_H);
|
||||
FView.DrawSpectrum;
|
||||
FPbSpectrum.Invalidate;
|
||||
if FPbRuler <> nil then FPbRuler.Invalidate;
|
||||
FPbWaterfall.Invalidate;
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Splitter — перетаскивание границы спектр/водопад
|
||||
// Обновление размеров происходит только при отпускании мыши (MouseUp),
|
||||
// чтобы исключить фризы во время перетаскивания.
|
||||
// Во время перетаскивания рисуем только призрак-линию на сплиттере.
|
||||
// ===========================================================================
|
||||
|
||||
procedure TPanafallPanel.SplitterMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
P: TPoint;
|
||||
begin
|
||||
if Button <> mbLeft then Exit;
|
||||
FSplitterDrag := True;
|
||||
FSplitterSH0 := FSpectrumHeight;
|
||||
// Y в координатах родителя (области панадаптера)
|
||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||
FSplitterDragY0 := P.Y;
|
||||
FSplitter.Color := TColor($005050A0); // подсветка при захвате
|
||||
{$IFDEF WINDOWS}
|
||||
SetCapture(FSplitter.Handle);
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.SplitterMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
P: TPoint;
|
||||
DeltaY: Integer;
|
||||
NewSH: Integer;
|
||||
TopOff, AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
MIN_SH = 60;
|
||||
MIN_WH = 40;
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if not FSplitterDrag then Exit;
|
||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := FParent.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||||
|
||||
// Только двигаем сплиттер визуально — без пересчёта битмапов
|
||||
FSplitter.Top := TopOff + NewSH + RULER_H;
|
||||
FPbWaterfall.Top := TopOff + NewSH + RULER_H + SPLITTER_H;
|
||||
FPbWaterfall.Height := AvailH - NewSH;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.SplitterMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
P: TPoint;
|
||||
DeltaY: Integer;
|
||||
NewSH: Integer;
|
||||
TopOff, AvailH: Integer;
|
||||
RULER_H: Integer;
|
||||
const
|
||||
SPLITTER_H = 5;
|
||||
MIN_SH = 60;
|
||||
MIN_WH = 40;
|
||||
begin
|
||||
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
|
||||
if not FSplitterDrag then Exit;
|
||||
FSplitterDrag := False;
|
||||
{$IFDEF WINDOWS}
|
||||
ReleaseCapture;
|
||||
{$ENDIF}
|
||||
FSplitter.Color := TColor($00303030); // обычный цвет
|
||||
|
||||
P := FParent.ScreenToClient(FSplitter.ClientToScreen(Point(X, Y)));
|
||||
DeltaY := P.Y - FSplitterDragY0;
|
||||
|
||||
TopOff := 0;
|
||||
// Низ зарезервирован под панель пана/зума (она видна, пока виден водопад).
|
||||
AvailH := FParent.ClientHeight - TopOff - RULER_H - SPLITTER_H
|
||||
- MulDiv(22, Screen.PixelsPerInch, 96);
|
||||
if AvailH <= 0 then Exit;
|
||||
|
||||
NewSH := FSplitterSH0 + DeltaY;
|
||||
if NewSH < MIN_SH then NewSH := MIN_SH;
|
||||
if NewSH > AvailH - MIN_WH then NewSH := AvailH - MIN_WH;
|
||||
|
||||
// Сохраняем новое соотношение; полный пересчёт с перерисовкой — у хозяина
|
||||
// (ему виднее: wideband, S-метр, оверлеи).
|
||||
FSplitterRatio := NewSH / AvailH;
|
||||
if Assigned(FOnSplitterMoved) then FOnSplitterMoved(Self);
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
// Пан/зум-полоса — прокидка мыши в TPanZoomBar
|
||||
// ===========================================================================
|
||||
|
||||
procedure TPanafallPanel.PanZoomMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FZoomBar <> nil then FZoomBar.HandleMouseDown(Button, X, Y);
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.PanZoomMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
begin
|
||||
if FZoomBar <> nil then FZoomBar.HandleMouseMove(X, Y);
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.PanZoomMouseUp(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
begin
|
||||
if FZoomBar <> nil then FZoomBar.HandleMouseUp;
|
||||
end;
|
||||
|
||||
procedure TPanafallPanel.PanZoomDblClick(Sender: TObject);
|
||||
begin
|
||||
if FZoomBar <> nil then FZoomBar.HandleDblClick;
|
||||
end;
|
||||
|
||||
end.
|
||||
+316
-215
@@ -1,9 +1,16 @@
|
||||
# Мультислайсы (несколько приёмных фильтров, как во FlexRadio)
|
||||
# Мультислайсы и панадаптеры (как во FlexRadio)
|
||||
|
||||
Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
|
||||
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
|
||||
`3049f91`, `a3ac459`). Этап 3 (аппаратные DDC для слайса вне захваченной
|
||||
полосы) — не начат. Ниже: что вошло по факту + дизайн/остаток.
|
||||
`3049f91`, `a3ac459`).
|
||||
|
||||
**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
|
||||
edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер**
|
||||
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
|
||||
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
|
||||
альтернативы» в конце файла. Реализация Этапа 3 не начата.
|
||||
|
||||
---
|
||||
|
||||
## Что реализовано (Этапы 1 + 2)
|
||||
|
||||
@@ -22,260 +29,354 @@
|
||||
Ctrl+ЛКМ по спектру = добавить слайс. Флаг главного (A) виден когда скрыта
|
||||
левая панель ЛИБО есть слайсы B+ (синхронизируется с правками с левой панели).
|
||||
- **UI-полировка флагов** (`30a6616`, `3049f91`, `a3ac459`):
|
||||
- `MainForm.LayoutFlags` — единый packing всех флагов (A+B+) в один ряд: каждый
|
||||
рядом со своей полосой фильтра, но флаги НЕ наезжают (сосед слева толкает
|
||||
правого; обратный поджим при выходе за правый край). `PositionVfoOverlay`/
|
||||
`PositionSliceFlags` — тонкие обёртки над ним.
|
||||
- Идентификация уехавшего флага — буквой слайса по центру его полосы фильтра
|
||||
сверху (`DrawBandLetter` CPU + `DrawBandLetterGL` GL с кэшем текстур букв);
|
||||
у главного A буква только когда есть слайсы.
|
||||
- Палитра слайсов `VfoOverlay.SliceColor(L)` (A зелёный … F фиолетовый, не
|
||||
красный — не путать с TX): один цвет для бейджа-буквы во флаге и для буквы
|
||||
на полосе.
|
||||
- Кнопки флага — общий `FlatButton.PaintFlatButton` (плоские, как левая
|
||||
панель; без «стеклянного» градиента), центровка бейджа через `TextRect`.
|
||||
- Флаг полностью тематизирован: структурные `CLR_*` выводятся из `TAppTheme`
|
||||
(`ApplyThemeColors`), тема раздаётся из `ApplyDarkTheme` + `WireOverlayEvents`.
|
||||
Статус-акценты (TX/SPLIT) фиксированы намеренно.
|
||||
- `MainForm.LayoutFlags` — единый packing всех флагов (A+B+) в один ряд;
|
||||
идентификация уехавшего флага буквой по центру полосы (`DrawBandLetter`
|
||||
CPU + `DrawBandLetterGL` GL с кэшем текстур букв); палитра
|
||||
`VfoOverlay.SliceColor(L)`; кнопки через `FlatButton.PaintFlatButton`;
|
||||
полная тематизация из `TAppTheme`.
|
||||
- **Перф** — S-метр развязан с кэшем «хрома» флага (`FMeterDirty` + meter-only
|
||||
`RefreshMeter` через `EnsureRendered`); `InvalidateVfoOverlay` не дёргает
|
||||
глобальный перезалив оверлеев на тик метра.
|
||||
|
||||
Остаток/риски (не сделано): персист слайсов в `Settings` (список + устройство);
|
||||
per-slice AGC-Top (сейчас общий с главным); полноценный мультислайс-TX (пока TX
|
||||
только главный); Этап 3 (HW DDC). Исходный план/дизайн — ниже без изменений.
|
||||
Остаток Этапов 1-2 (не сделано): персист слайсов в `Settings` (список +
|
||||
устройство); per-slice AGC-Top (сейчас общий с главным); полноценный
|
||||
мультислайс-TX (пока TX только главный).
|
||||
|
||||
## Предыстория (UI-каркас до мультислайса — исторично)
|
||||
---
|
||||
|
||||
> Ниже — состояние ДО реализации Этапа 2 (одиночный флаг, захардкоженная `'A'`).
|
||||
> Актуальное состояние см. в «Что реализовано» выше. Оставлено как контекст.
|
||||
## Модель FlexRadio (итог разбора 2026-07-10)
|
||||
|
||||
`VfoOverlay.pas` переделан в **«флаг слайса»** в стиле SmartSDR (6 коммитов
|
||||
`4a92256..a04c592`, уже в `main`). Это ровно UI-управление одним слайсом из
|
||||
Этапа 2, только пока в единственном экземпляре:
|
||||
Во Flex **две независимые сущности**, и это ключ ко всему дизайну:
|
||||
|
||||
- Флаг-накладка поверх спектра с буквой слайса (`FSlice`, сейчас захардкожено `'A'`),
|
||||
fly-out пикерами **MODE / DSP / AUD**, слайдером громкости, mute, SPLIT,
|
||||
кликабельным **TX-бейджем** (выбор TX-слайса) и S-метром.
|
||||
- **Per-flag выбор аудио-устройства** уже есть: `SetAudioDevices` + `OnAudioDevice`
|
||||
(`VfoOverlay.pas:31,125,136`) — прокручиваемый список в AUD-fly-out. Это готовый
|
||||
UI под «своё звуковое устройство на слайс» из абстракции `TSlice.DeviceIndex`.
|
||||
- События наружу: `OnSelect/OnDSPChange/OnAGCChange/OnVolume/OnMute/OnAudioDevice/`
|
||||
`OnSplit/OnTxSelect/OnInvalidate` (`VfoOverlay.pas:131-139`).
|
||||
- Автопозиционирование: флаг флипается влево, если не влезает справа (`6ad5f29`).
|
||||
- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
|
||||
широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
|
||||
- **Слайс** — узкополосный приёмник, ВСЕГДА прикреплён к какому-то панадаптеру
|
||||
и рисуется на нём флагом. Слайс никогда не имеет собственного спектра.
|
||||
Тянешь слайс за край — панорама скроллится за ним.
|
||||
|
||||
Привязка в приложении **пока одиночная**: один `FVfoOverlay` в `MainForm`
|
||||
(`MainForm.pas:125`), созданный/подключённый на `MainForm.pas:962-972`, привязан к
|
||||
VFO A (`UpdateVfo(FController.FVfoA)`, `MainForm.pas:3265`); хендлеры
|
||||
`OnVfoOverlay*` шлют изменения в главный тракт. `SpectrumView` держит один
|
||||
`FVfoOverlay` и рисует его в `DrawOverlay` (`SpectrumView.pas:72,220,1010`).
|
||||
Хочешь слушать другой участок — открываешь **второй панадаптер** (радио выделяет
|
||||
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
|
||||
6600 — 4/4, 6700 — 8/8.
|
||||
|
||||
Чтобы превратить это в мультислайс — нужно из одиночного флага сделать **коллекцию
|
||||
флагов (по одному на слайс)** и развести их события на конкретный `SliceId` вместо
|
||||
глобального VFO A. Детали — в обновлённом Этапе 2.
|
||||
**Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit):
|
||||
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
|
||||
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
|
||||
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
|
||||
антенну. Слайсы наследуют антенну своего пана. Diversity (когерентное сложение
|
||||
двух SCU) — отдельный режим поверх.
|
||||
|
||||
## Цель
|
||||
**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
|
||||
перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный
|
||||
SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели.
|
||||
|
||||
Несколько независимых приёмных «слайсов» на одном ADC/одном IQ-потоке:
|
||||
у каждого слайса своя частота, режим, полоса, AGC и **своё звуковое
|
||||
устройство** (как DAX/наушники/динамик во Flex). Панадаптер (спектр+водопад)
|
||||
остаётся один, слайсы рисуются маркерами поверх.
|
||||
---
|
||||
|
||||
## Как это устроено у Flex и что из этого берём
|
||||
|
||||
Слайс во Flex — это **не «ещё один фильтр» на одном приёмнике**, а полноценный
|
||||
независимый узкополосный приёмник (свой DDC в FPGA + демодулятор + фильтр + AGC +
|
||||
аудио). Общий у слайсов — только панадаптер (широкий FFT-захват).
|
||||
|
||||
Один WDSP-канал = один демодулятор = один фильтр = один аудиопоток. Значит для
|
||||
«разный звук/режим/полоса на каждый слайс» нужен **отдельный WDSP-канал на слайс**.
|
||||
Способ получить узкополосный IQ для канала различается по железу — см. ниже.
|
||||
|
||||
## Ключевое различие бэкендов
|
||||
## Итоговая модель EWSDR
|
||||
|
||||
```
|
||||
openHPSDR (Protocol 2, V4.3, MAX_DDCS=80):
|
||||
ADC0 ─┬─ DDC (FPGA-тюнер) → IQ-поток порт 1035+n → WDSP-канал → фильтр/AGC → AudioOut[dev]
|
||||
├─ DDC → ...
|
||||
└─ DDC → ...
|
||||
• Аппаратные DDC: слайс тюнится КУДА УГОДНО в полосе ADC (весь КВ), дёшево по CPU.
|
||||
• На Angelia/Orion/Saturn DDC0/DDC1 заняты PureSignal → слайсы с DDC2+ (в коде DDCBase=2).
|
||||
• RX2 = ВТОРОЙ ADC (ADC1), отдельная антенна/diversity — ОРТОГОНАЛЬНО слайсам, не нужно.
|
||||
|
||||
Pluto (AD9363, libiio):
|
||||
один RX-тракт: один LO, один rf_bandwidth, один IQ-поток (притворяется одним DDC,
|
||||
пушится как TDDCIQPacket в тот же конвейер).
|
||||
• Аппаратных DDC НЕТ. Слайсы = СОФТОВЫЙ fan-out одного IQ-блока на N WDSP-каналов,
|
||||
каждый со своим SetRXAShiftFreq в пределах захваченной полосы вокруг LO.
|
||||
• Для QO-100 идеально: весь транспондер (~500 кГц) в захвате, LO залочен на маяк.
|
||||
Панадаптер (спектр+водопад; свой захват IQ; свой ADC-источник; свой span/зум/
|
||||
│ палитра/wf-AGC; свой WDSP-анализатор)
|
||||
└─ Слайсы (флаги внутри панорамы; софт-fan-out IQ ЭТОГО панадаптера;
|
||||
свой WDSP-канал/режим/фильтр/AGC/DSP/звуковуха — уже реализовано)
|
||||
```
|
||||
|
||||
## Единая абстракция
|
||||
Правила:
|
||||
1. Слайс всегда принадлежит панадаптеру (`PanId`). Понятия «off-span слайс»
|
||||
больше нет: хочешь слайс вне захвата — создай панадаптер там.
|
||||
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
|
||||
(WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
|
||||
(каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
|
||||
один слайс в его центре (поведение Flex).
|
||||
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
|
||||
В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
|
||||
маппинг «вход→АЦП» жёсткий, антенная абстракция не нужна.
|
||||
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
|
||||
буква = личность слайса, пан = место жительства.
|
||||
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
|
||||
прячет/блокирует кнопки, а не хардкодит «if Pluto».
|
||||
|
||||
```pascal
|
||||
TSlice = record
|
||||
Id: Integer;
|
||||
TargetHz: Double; // абсолютная частота слайса
|
||||
Mode: TRxMode; // USB/LSB/CW/FM/...
|
||||
FilterLo,
|
||||
FilterHi: Integer;
|
||||
AGCMode: TWDSPAGCMode;
|
||||
Volume: Double;
|
||||
WDSPChannel: Integer; // канал WDSP (0..N); 0=главный, 1=TX заняты
|
||||
Source: (srcSoftShift, srcHardDDC); // как получаем узкий IQ
|
||||
DDCIndex: Integer; // если srcHardDDC — какой аппаратный DDC
|
||||
ShiftHz: Double; // если srcSoftShift — сдвиг = TargetHz - CapturedCenterHz
|
||||
AudioOut: TAudioOutput; // свой инстанс со своим DeviceIndex
|
||||
DeviceIndex: Integer; // выбранная звуковуха (или -1 = default)
|
||||
Enabled: Boolean;
|
||||
end;
|
||||
Матрица бэкендов:
|
||||
|
||||
| Бэкенд | Панадаптеров | Незав. частоты панов | АЦП | Примечание |
|
||||
|-------------------------|--------------|----------------------|-----|------------|
|
||||
| HPSDR 1 ADC (Hermes) | до 4 (DDC) | да | 1 | ADC-селектор скрыт |
|
||||
| HPSDR 2 ADC (Angelia/Orion/Saturn) | до 4 (DDC, DDCBase=2) | да | 2 | селектор A1/A2 на пан |
|
||||
| Pluto (обычный) | 1 | — | 1 | как сейчас, ничего не меняется |
|
||||
| Pluto+ (2r2t) | 2 | **нет — общий LO** | 2 | пан RX2 частотно связан с RX1 |
|
||||
|
||||
```
|
||||
openHPSDR (Protocol 2, MAX_DDCS=80):
|
||||
ADC0 ─┬─ DDC2 (широкий) → Пан 0 → анализатор + слайсы A,B…
|
||||
└─ DDC3 (узкий) → Пан 1 → анализатор + слайсы C,D…
|
||||
ADC1 ──── DDC4 → Пан 2 (другая антенна) → …
|
||||
• Каждый DDC тюнится куда угодно, свой rate. DDC0/1 заняты PureSignal
|
||||
на Angelia/Orion/Saturn (DDCBase=2, HPSDRNetwork.pas:1485).
|
||||
|
||||
Pluto+ (AD9363 2r2t, libiio):
|
||||
RX LO ─┬─ RX1 → псевдо-DDC0 → Пан 0
|
||||
(один!)└─ RX2 → псевдо-DDC1 → Пан 1 (та же частота/rate, другая антенна)
|
||||
• Оба канала на ОДНОМ LO/rate, фазокогерентны (база под diversity/пеленгацию).
|
||||
• Обычный Pluto: один RX разведён на плате — Пан 1 недоступен.
|
||||
```
|
||||
|
||||
Правило маршрутизации по частоте (одно на оба бэкенда):
|
||||
- слайс **внутри** текущего захваченного span → `srcSoftShift` (Pluto и HPSDR);
|
||||
- слайс **вне** захваченного span → `srcHardDDC` (только HPSDR; Pluto так не умеет).
|
||||
---
|
||||
|
||||
## Порядок работ
|
||||
## Этап 3 (переработан): «DDC = панадаптер»
|
||||
|
||||
### Этап 1 — Софтовый fan-out в WDSPEngine (backend-agnostic) ✅ РЕАЛИЗОВАН
|
||||
Подэтапы упорядочены так, чтобы каждый собирался и работал сам по себе.
|
||||
3.0 — чистый рефакторинг без новых фич, дальше функциональность наращивается.
|
||||
|
||||
Даёт слайсы сразу на Pluto/QO-100 (единственный режим) и на HPSDR для слайсов
|
||||
внутри видимой полосы. Pluto уже пушит IQ через тот же DDC-конвейер, поэтому
|
||||
fan-out живёт в движке и не зависит от бэкенда.
|
||||
### 3.0 — TPanafallPanel: выделить панадаптер из MainForm
|
||||
|
||||
- `WDSPEngine.pas`: сейчас `RXA_CHAN=0` один канал, один `FRXOut`, один аудиовыход
|
||||
(`WDSPEngine.pas:96`, alloc буферов ~`:159`, OpenChannel ~`:1081`).
|
||||
- Добавить массив слайс-каналов: для каждого активного слайса `OpenChannel(chN, ...)`
|
||||
с той же геометрией, что RXA_CHAN, + `SetRXAShiftRun/SetRXAShiftFreq(chN, ShiftHz)`
|
||||
(механизм уже используется: `WDSPEngine.pas:1099-1100`), `SetRXAMode`,
|
||||
`RXASetPassband`, AGC, `SetRXAPanelGain1`.
|
||||
- В DSP-потоке после накопления IQ-блока (`FRXAccI/Q`, `FRXAccPos`) прогонять
|
||||
`fexchange0` по КАЖДОМУ активному каналу из ОДНОГО и того же входного буфера
|
||||
(входной буфер не портить in-place между каналами — держать копию, как `FSpecBuf`).
|
||||
- Выход каждого канала → свой callback `OnSliceAudio(SliceId, L, R, Count)`.
|
||||
- `AudioOutput.pas`: уже готов к мульти-устройству — `DeviceIndex`, `EnumOutputDevices`,
|
||||
`FindDeviceByName` (`AudioOutput.pas:137-151`). Создавать **отдельный `TAudioOutput`
|
||||
на слайс**, каждый со своим `DeviceIndex`.
|
||||
- `RadioController.pas`: `TSlice[]` (add/remove/retune/setmode/setdevice), роутинг
|
||||
`OnSliceAudio` → нужный `TAudioOutput`.
|
||||
**Статус 2026-07-10: фаза A СДЕЛАНА (собирается, стартует; на экране не
|
||||
проверено).** Создан `PanafallPanel.pas`: панель владеет view (CPU/GL),
|
||||
контролами (спектр/линейка/сплиттер/водопад/пан-зум/кнопки зума), полной
|
||||
геометрией стека (`Layout`), механикой сплиттера и мышью пан/зум-полосы.
|
||||
MainForm держит алиасы (`FSpecView`, `PbSpectrum`, …) на члены `FPan` — весь
|
||||
остальной код не тронут; размеры областей читаются из
|
||||
`FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight`. Семантика мыши
|
||||
спектра/водопада, флаги слайсов, оверлеи, тик — ПОКА в MainForm (фаза B).
|
||||
**GL-аудит per-instance ПРОЙДЕН**: в `SpectrumViewOpengl`/`WaterfallViewOpengl`
|
||||
все текстуры (`FGridLabelTex`…`FLetterTex`, `FSliceTex`, `FTexture`,
|
||||
`FMarkerTex`) и LUT водопада — поля экземпляра, глобальных/class var GL-ресурсов
|
||||
нет; палитры — read-only const (шарить безопасно).
|
||||
|
||||
Проверка Этапа 1: Pluto/QO-100 — 2 слайса (VFO + маяк-монитор), звук в две разные
|
||||
звуковухи, независимые режимы/полосы. Сборка: `lazbuild --ws=qt6 ewsdr.lpr`.
|
||||
Самый большой и самый важный кусок. Сейчас панадаптер размазан по MainForm:
|
||||
`FSpecView: TSpectrumView` создаётся и прошивается ~20 свойствами напрямую
|
||||
(`MainForm.pas:1018-1035`), канва `PbSpectrum: TControl` (TOpenGLControl,
|
||||
`MainForm.pas:2220`), `FPanZoomBar` (`MainForm.pas:312`), мышиные хендлеры
|
||||
`PbSpectrumMouse*` (`MainForm.pas:439-445`), `LayoutFlags`, тик обновления.
|
||||
|
||||
### Этап 2 — UI слайсов на водопаде + выбор устройства ✅ РЕАЛИЗОВАН (кроме персиста)
|
||||
Сделать composite `TPanafallPanel` (новый модуль `PanafallPanel.pas`,
|
||||
наследник TCustomControl/TPanel), который владеет:
|
||||
|
||||
Каркас флага слайса уже сделан (см. «Что уже готово»): один `TVfoOverlay` со всеми
|
||||
пикерами и per-flag выбором устройства. Остаётся размножить его на N слайсов и
|
||||
привязать к позиции по частоте. Выпадашка устройства и dropdown-паттерн **уже есть в
|
||||
самом флаге** (AUD fly-out) — отдельный `DeviceForm` не нужен.
|
||||
- канвой (`TOpenGLControl` или `TPaintBox` по `USE_OPENGL` — как сейчас);
|
||||
- своим `TSpectrumView`/`TSpectrumViewOpenGL`;
|
||||
- своим `TPanZoomBar` и линейкой;
|
||||
- коллекцией флагов слайсов этого пана (`FVfoOverlay` пана + `FSliceFlags`)
|
||||
и `LayoutFlags` (packing переезжает сюда);
|
||||
- мышиной логикой канвы (тюн/drag/колесо/Ctrl+ЛКМ/drag маркера слайса);
|
||||
- шапкой пана (см. 3.3) — в пане 0 шапка скрыта до появления пана 1.
|
||||
|
||||
- **Коллекция флагов вместо одного.** Из `MainForm.FVfoOverlay` (одиночный,
|
||||
`MainForm.pas:125`) сделать `array of TVfoOverlay` / список, по флагу на слайс;
|
||||
`SpectrumView.FVfoOverlay` (`SpectrumView.pas:72`) — тоже коллекция, рисовать все в
|
||||
`DrawOverlay` (`SpectrumView.pas:1010`). Разместить: `SetSlice(Id, Letter)` в оверлей,
|
||||
события `OnVfoOverlay*` должны нести `SliceId` (сейчас бьют по VFO A напрямую —
|
||||
завести маппинг оверлей→слайс, роутить в нужный `TSlice`/WDSP-канал из Этапа 1).
|
||||
- **Позиционирование по частоте.** `PositionVfoOverlay` (`MainForm.pas:518`) сейчас
|
||||
ставит один флаг; расширить на маппинг `TargetHz → X` через `GetViewWindow`/
|
||||
`SyncSpecViewFreq` (зум-работа). Слайс за краем видимой полосы — маркер у края
|
||||
(флип влево/вправо уже есть, `6ad5f29`).
|
||||
- **ЛКМ по водопаду** = поставить/двинуть активный слайс; выделение активного
|
||||
(подсветка буквы/рамки флага). Хит-тест мыши флага уже разведён
|
||||
(`HandleMouseDown/Move/Up`, `MainForm.pas:4424`) — добавить «клик мимо флага по
|
||||
спектру = ретюн активного слайса».
|
||||
- **TX-select** уже кликабелен (`OnTxSelect`, `08c71c4`) — при мультислайсе выбирать,
|
||||
какой слайс идёт в TX; снимать флаг с остальных.
|
||||
- Персист: список слайсов в `Settings.pas` (per-band или глобально — решить), включая
|
||||
`DeviceIndex`/имя устройства каждого флага.
|
||||
Наружу: события `OnTune(PanId, Hz)`, `OnAddSlice(PanId, Hz)`, `OnSlice*`
|
||||
(существующие слайс-события + PanId), `OnPanClose/OnPanPopOut/OnPanADC`;
|
||||
входные сеттеры состояния (центр/span/mode/wf-настройки) — то, что сейчас
|
||||
раздаётся FSpecView напрямую.
|
||||
|
||||
### Этап 3 — Аппаратные DDC для openHPSDR (слайс куда угодно по КВ) ⏳ НЕ НАЧАТ
|
||||
MainForm после рефакторинга держит `FPans: array of TPanafallPanel`
|
||||
(пан 0 всегда), общий тик раздаёт кадры по панам, весь остальной код
|
||||
(левая панель, FreqDisplay, контроллер) работает с «активным слайсом» —
|
||||
механика `FActiveFlag/CurSliceId` уже есть, добавляется лишь `ActivePan`.
|
||||
|
||||
Апгрейд поверх общего каркаса; нужен только чтобы ставить слайс ВНЕ захваченной
|
||||
полосы. Плита ~на 70% есть:
|
||||
- `ConfigureDDCs(NumDDCs, ...)` уже включает N DDC с per-DDC конфигом/ADC-source
|
||||
(`HPSDRNetwork.pas:220`, `DDCConfig[ddc*6+0]=ADCSource`).
|
||||
- Приём разведён по DDC: порты `1035+n` → `HandleDDCIQ(Buf,Len,DDCIdx)`
|
||||
(`HPSDRNetwork.pas:392`).
|
||||
- High Priority уже держит частоту на КАЖДЫЙ DDC (bytes 9-12=DDC0, 13-16=DDC1, …)
|
||||
`HPSDRProtocol.pas:271`.
|
||||
Критерий готовности 3.0: поведение приложения ИДЕНТИЧНО текущему
|
||||
(один пан, все фичи слайсов работают), diff в MainForm — только вынос.
|
||||
|
||||
Чего не хватает:
|
||||
1. Per-DDC частота: сейчас `SetRunAndFreq(...; DDC0FreqHz; ...)` знает только DDC0
|
||||
(`HPSDRNetwork.pas:224`) — писать частотное слово каждого DDC в High Priority.
|
||||
2. Роутинг `HandleDDCIQ(DDCIdx)` → соответствующий WDSP-канал слайса.
|
||||
3. Учёт DDCBase=2 (PureSignal) и Ethernet-бюджета (реально 4–8 слайсов, не 80).
|
||||
Отдельная задача внутри 3.0 — **аудит GL-ресурсов** `SpectrumViewOpengl` на
|
||||
per-instance: каждое окно/канва = свой GL-контекст, текстуры между контекстами
|
||||
НЕ шарятся. Проверить: текстуры водопада, LUT палитры, кэш текстур букв
|
||||
(`DrawBandLetterGL`), текстуры флагов (`FSliceTex`), шейдеры/списки — ничего
|
||||
static/глобального. (CPU-LUT палитры общий — это ок.)
|
||||
|
||||
`feature/rx2-support` — это про ВТОРОЙ ADC, не про слайсы; но её каркас
|
||||
(доп. WDSP-канал + доп. аудиовыход + роутинг по индексу) частично переиспользуем.
|
||||
### 3.1 — Бэкенд HPSDR: per-DDC частота/rate/ADC + роутинг
|
||||
|
||||
### Этап 3 — рабочие заметки (черновик, обсуждение НЕ закончено)
|
||||
Плита готова ~на 70 %, дельты точечные:
|
||||
|
||||
Разобрали фактический data-path и набросали дизайн; вернёмся позже.
|
||||
1. **Per-DDC частота в High Priority.** `SendFullHP`
|
||||
(`HPSDRNetwork.pas:1552`) пишет частотное слово только активного DDC
|
||||
(`Buf[9 + DDCBase*4]`). Сделать цикл по активным DDC:
|
||||
`Buf[9+ddc*4] := FreqToPhaseWord(FDDCFreq[ddc])`. Контроллер держит
|
||||
`FDDCFreq: array[0..MAX_DDCS-1] of Double`, обновляет при ретюне пана.
|
||||
2. **Per-DDC rate/ADC/enable в `ConfigureDDCs`** (`HPSDRNetwork.pas:1460`):
|
||||
сейчас один rate/ADCSource на все N. Принимать массив записей
|
||||
`(Enabled, RateKHz, ADCSource)` — протокол это позволяет
|
||||
(`DDCConfig[ddc*6+0]=ADCSource`, rate там же per-DDC).
|
||||
3. **Роутинг IQ по панам.** `RadioController.OnDDCIQ`
|
||||
(`RadioController.pas:828`) сейчас гейтит `DDCIndex = FActiveDDC`
|
||||
(`:875`), остальное выбрасывает. Заменить на маппинг
|
||||
`DDCIndex → PanId`: пакет пана 0 — в существующий путь, пакеты
|
||||
панов N>0 — в `WDSPEngine.PushPanIQ(PanId, Data)` (см. 3.2).
|
||||
4. **Аллокатор DDC**: пул hw-индексов от `DDCBase+1` (DDCBase=2 на
|
||||
PureSignal-платах, `HPSDRNetwork.pas:1485`), захват при создании пана,
|
||||
освобождение при закрытии, слот-стабильность как у слайсов.
|
||||
5. **Rate слайс-панов**: выбор из 192/384/768 кГц (дефолт 384). Бюджет
|
||||
Ethernet: поток DDC ≈ rate × 6 байт (24-бит I+Q) → 384 кГц ≈ 18 Мбит/с;
|
||||
3 доп. пана + главный на 1.536 МГц укладываются даже в 100 Мбит с запасом,
|
||||
но кап «до 4 панов» держим жёстко (и по CPU тоже).
|
||||
|
||||
**Что есть сейчас (по коду):**
|
||||
- Один аппаратный DDC: `ConfigureDDCs(1, rate, ADCSource=0)` (`RadioController.pas:3314`),
|
||||
IQ на порт `1035+n` → `HandleDDCIQ(DDCIdx)` → `OnDDCIQ(DDCIndex)`. В WDSP подаётся
|
||||
**только `FActiveDDC`** (`RadioController.pas:843`), остальные DDC отбрасываются.
|
||||
- Слайсы = софт fan-out этого одного IQ: `SetSliceShift(Id, TargetHz-FCenterFreq)`
|
||||
(`RefreshSliceShifts:1063`). Гейт `SliceFitsCapture` ±47.5% rate вокруг центра
|
||||
(`:1081`); вне полосы `AddSlice` отказывает (`:1097`).
|
||||
- HP-фрейм шлёт частотное слово **только DDC0**: `Buf[9 + DDCBase*4]`
|
||||
(`SendFullHP:1567`); слова остальных DDC = нули.
|
||||
- `ConfigureDDCs` умеет N DDC (per-DDC enable), но пишет **один** rate/ADCSource на все;
|
||||
`DDCBase=2` на Orion/Angelia/Saturn (DDC0/1=PureSignal), `NumADCs=2` на 2-АЦП платах.
|
||||
Ретюн пана N>0 = запись `FDDCFreq[ddc]` + `SendFullHP` (без реконфига DDC);
|
||||
смена rate/ADC = `ConfigureDDCs` (как сейчас при смене sample rate).
|
||||
|
||||
**Что нужно (дельты по точкам):**
|
||||
1. Per-DDC частота в HP: `SendFullHP` — цикл `Buf[9+ddc*4]=FreqToPhaseWord(FDDCFreq[ddc])`;
|
||||
контроллер держит `FDDCFreq[]`, обновляет при ретюне слайса.
|
||||
2. Per-DDC rate + ADCSource в `ConfigureDDCs` (принимать массив): главный DDC широкий
|
||||
(панорама), слайс-DDC узкие ~48–96 кГц (дёшево по линку). Протокол это позволяет.
|
||||
3. Аллокатор DDC: пул hw-индексов от `DDCBase+1`, захват/освобождение, кап (см. решения).
|
||||
4. **Ключевое — два типа слайс-канала в WDSPEngine:**
|
||||
- *soft* (Этап 1): вход = копия главного IQ, rate главного, NCO shift = `Target-Center`,
|
||||
общий аккумулятор.
|
||||
- *hard* (Этап 3): вход = **свой** DDC-пакет, rate его DDC, NCO shift = 0, **свой**
|
||||
аккумулятор `FSliceAccI/Q`. Нужен `PushSlicePacket(chan, ...)` + роутинг
|
||||
`OnDDCIQ(DDCIndex)→WDSP-канал привязанного слайса`.
|
||||
5. RX2 (второй ADC): `TSlice.ADCSource (0/1)` → слайс-DDC на нужный ADC. Даёт двух-антенные
|
||||
слайсы (главный RX на ANT1, слайс на ANT2). Ортогонально «внутри/вне полосы».
|
||||
### 3.2 — WDSPEngine: анализатор и IQ-тракт на панадаптер
|
||||
|
||||
**Принятые решения (предварительно):**
|
||||
- Лимит: **до 4–6 hard-DDC слайсов**, со строгим контролем суммарного rate (бюджет Ethernet).
|
||||
- **Сначала статика**: источник (soft/hard) выбирается при добавлении/явном ретюне;
|
||||
авто-promotion/demotion (soft↔hard при смене центра/зума) — отдельным этапом позже.
|
||||
- RX2/ADCSource — не решено (нужно знать, как заведены антенны у пользователя).
|
||||
Сейчас: анализаторы `RX_DISP_ID=0` / `TX_DISP_ID=1` (`WDSPEngine.pas:133`),
|
||||
создание `XCreateAnalyzer` (`:1140`), настройки+span-clip
|
||||
`ApplyRXAnalyzerSettings` (`:1163-1211`), снапшот спектра
|
||||
`Spectrum0(1, RX_DISP_ID, …)` из DSP-потока (`:1597`), выдача пикселей
|
||||
через `FOnSpectrum` (`:2850`). Слайсы едят копию ГЛАВНОГО аккумулятора
|
||||
в `ProcessSlices`.
|
||||
|
||||
**UI-концепция (черновик, не финал — выбор представления off-span НЕ сделан):**
|
||||
Принцип: **цвет+буква слайса = его личность во всех местах** (флаг, полоса, edge-маркер,
|
||||
чип рейла, ADC-бейдж) — переиспользуем палитру `SliceColor`, тему, `PaintFlatButton`.
|
||||
- Слайс в полосе — как сейчас (флаг сбоку + буква на полосе, packing).
|
||||
- Слайс **вне** видимой панорамы (hard) — приклеенный к краю спектра edge-маркер:
|
||||
цветная буква + стрелка (◄/►) + абсолютная частота. Клик = выбрать, двойной = перевести
|
||||
главную панораму на него. Почти бесплатно (флип/клэмп к краю уже есть).
|
||||
- **Slices-рейл** — тонкая полоса-менеджер (под панорамой или в правой панели): чип на слайс
|
||||
(цветная точка+буква · частота · режим · мини-S-метр · ADC-бейдж), сортировка по частоте,
|
||||
`[+]` = добавить с вводом частоты (для off-span, где Ctrl-клик не поможет). Единственное
|
||||
место, где off-span слайсы всегда видны/управляемы.
|
||||
- **ADC/антенна** — бейдж `A1`/`A2` в шапке флага и в чипе (только на 2-АЦП платах).
|
||||
- **soft/hard** — тонкий признак (плотная vs полая точка, или значок `⧉`); не кричать.
|
||||
- Позже «вау»: мини-панадаптер (свой спектр/водопад) на каждый hard-слайс в чипе рейла.
|
||||
Дельты:
|
||||
|
||||
Развилка для следующего захода: как показывать off-span слайсы —
|
||||
(a) edge-маркеры + рейл [рекомендация], (b) только edge-маркеры, (c) рейл с мини-панадаптерами.
|
||||
1. **Пан-анализаторы**: `PAN_DISP_BASE = 8` (не 4/5 — они заняты в ветке
|
||||
`feature/qo100-zoom-panel`, при будущем merge не столкнёмся);
|
||||
`XCreateAnalyzer(PAN_DISP_BASE+PanId)` при создании пана,
|
||||
`ApplyPanAnalyzerSettings(PanId)` — параметризованная копия
|
||||
`ApplyRXAnalyzerSettings` (свой rate, свой span-clip/zoom, свои
|
||||
FFT/avg/detector — можно стартовать с общих настроек, разделить позже).
|
||||
2. **Per-pan аккумулятор**: `TPanChan = record` (`FPanAccI/Q`, `FPanAccPos`,
|
||||
rate, disp id, активные слайсы). `PushPanIQ(PanId, Data)` из сетевого
|
||||
потока кладёт в аккумулятор пана; по накоплению блока в DSP-потоке:
|
||||
копия → `Spectrum0(1, PAN_DISP_BASE+PanId, …)` + `ProcessSlices` по
|
||||
слайсам ЭТОГО пана.
|
||||
3. **Слайс знает пана**: в `TSliceChan` добавляется `PanId`; NCO shift
|
||||
слайса = `TargetHz − PanCenterHz`; rate канала слайса = rate его пана
|
||||
(для панов N>0 это 192-768 кГц — WDSP-каналу дешевле, чем полный rate!).
|
||||
`RefreshSliceShifts` обобщается на «центр пана», гейт `SliceFitsCapture` —
|
||||
на «span пана».
|
||||
4. **Выдача пикселей**: `FOnPanSpectrum(PanId, Pixels, N)` (или существующий
|
||||
колбэк + PanId), MainForm-тик раздаёт по `FPans[PanId]`.
|
||||
5. **Пул WDSP-каналов** общий: 0=RXA (слайс A пана 0), 1=TX, слайсы 2..7
|
||||
(`SLICE_CHAN_BASE`, `MAX_SLICES=6`) — на все паны суммарно. Этого хватает:
|
||||
6 доп. слайсов + главный = 7 приёмников.
|
||||
|
||||
Блокировки: `FSliceLock` уже сериализует слайсы vs DSP; добавить такую же
|
||||
дисциплину для add/remove пана (сетевой поток пишет в аккумулятор пана —
|
||||
создание/удаление пана только под локом, как переоткрытие слайсов при
|
||||
ChangeSampleRate).
|
||||
|
||||
### 3.3 — UI: второй панадаптер в главном окне
|
||||
|
||||
- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
|
||||
`TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист.
|
||||
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
|
||||
частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
|
||||
кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
|
||||
появляется только когда панов ≥ 2.
|
||||
- **Добавить пан**: кнопка `+` в шапке/тулбаре (только если
|
||||
`caps.MaxPans > 1`): диалог/дефолт — частота (ввод или «текущая ±»),
|
||||
rate, ADC. Создание пана автоматически создаёт его первый слайс в центре.
|
||||
- **Активный пан** = пан активного слайса. Клик по канве пана делает его
|
||||
слайс активным (существующая механика `FActiveFlag`), левая панель /
|
||||
FreqDisplay / CAT правят активный слайс — как сейчас.
|
||||
- **Ctrl+ЛКМ** на любом пане = добавить слайс в этот пан (буква из
|
||||
глобального пула).
|
||||
- **Тик/FPS**: общий таймер, кадры раздаются каждому пану; FPS-гейт из
|
||||
перф-работ применяется per-pan. Свёрнутый/скрытый пан кадры не рендерит.
|
||||
- **TX**: TX-бейдж на флаге работает с любого слайса любого пана — DUC
|
||||
независим от DDC, TX-частота = частота слайса. (Полный мультислайс-TX —
|
||||
отдельная старая задача, тут ничего нового не ломаем.)
|
||||
|
||||
### 3.4 — ADC-селектор панадаптера
|
||||
|
||||
- `TPanafall.ADCSource: 0/1`; в шапке пана селектор/бейдж `A1`/`A2`.
|
||||
Показывается только при `NumADCs=2` (детект платы уже есть — `NumADCs`
|
||||
в ConfigureDDCs-пути).
|
||||
- Переключение = `ConfigureDDCs` с новым ADCSource для DDC этого пана.
|
||||
- Пан 0 тоже можно перекинуть на ADC1 — это закрывает сценарий старой ветки
|
||||
`feature/rx2-support` (вторая антенна) бесплатно, как частный случай.
|
||||
- Diversity (sync-DDC, оба АЦП когерентно в один канал) — НЕ здесь; протокол
|
||||
умеет, дизайну панов не мешает, отдельная будущая фича.
|
||||
|
||||
### 3.5 — Pop-out: панадаптер в отдельном OS-окне
|
||||
|
||||
Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке.
|
||||
|
||||
- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
|
||||
`TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
|
||||
- **Риск №1 — GL + reparent**: перенос живого `TOpenGLControl` между
|
||||
окнами на Qt6/Wayland может убить контекст. Безопасный путь: НЕ
|
||||
репарентить канву, а пересоздать вьюху в новом окне (панель отдаёт
|
||||
state: центр/span/wf-историю можно не сохранять — водопад начнётся
|
||||
заново; слайсы/флаги живут в контроллере и переподключаются).
|
||||
Сначала попробовать честный reparent, fallback — пересоздание.
|
||||
- **Риск №2 — общие GL-ресурсы**: закрывается аудитом из 3.0.
|
||||
- Персист: флаг «пан N в отдельном окне» + геометрия окна.
|
||||
|
||||
### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе
|
||||
|
||||
Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).
|
||||
|
||||
- **Детект**: число RX-каналов в iio-контексте (`voltage0/1` + `voltage2/3`
|
||||
у `cf-ad9361-lpc`); прошивка должна быть с `adi,2rx-2tx-mode-enable`
|
||||
(на Pluto+ обычно так). Если каналов 2 → `caps.MaxPans=1`, как сейчас.
|
||||
- **PlutoBackend** (`PlutoBackend.pas:222-236`): сейчас RX-поток пушит
|
||||
`FOnDDCIQ(0, pkt)`. В 2r2t буфер отдаёт 4 канала интерливом —
|
||||
деинтерлив на два пакета → `FOnDDCIQ(0, …)` и `FOnDDCIQ(1, …)`.
|
||||
Дальше это обычные псевдо-DDC, роутинг из 3.1/3.2 работает без изменений.
|
||||
- **Частотная связка (главное отличие)**: оба RX на одном LO и одном rate.
|
||||
`caps.IndependentPanFreq = False` → пан 1 создаётся ТОЛЬКО с центром/rate
|
||||
пана 0; ретюн любого пана ретюнит общий LO (двигает оба); в шапке пана 1 —
|
||||
признак связки (`🔗 LO`), контролы центра либо скрыты, либо честно тюнят
|
||||
общий LO. Зум/span-clip у каждого пана свой (анализаторы независимы).
|
||||
- **Пропускная способность**: ×2 потока. Через гигабитный Ethernet Pluto+
|
||||
(наш libiio network context) проходит свободно; на USB2 пришлось бы резать
|
||||
rate — не наш кейс.
|
||||
- **QO-100 beacon-lock**: лок правит общий LO — оба пана едут вместе,
|
||||
инвариант «всё стоит на месте» сохраняется, конфликта нет.
|
||||
- **Бонус на потом**: каналы фазокогерентны — готовая база под честный
|
||||
diversity (сложение с фазовращателем) и пеленгацию. Не в этом этапе.
|
||||
- TX2 (второй выход, общий TX LO) — вне этого плана.
|
||||
|
||||
---
|
||||
|
||||
## Персист (собрать в один заход после 3.3)
|
||||
|
||||
В `Settings.pas`:
|
||||
- список панов: центр, rate, ADC, span/зум, высота в стеке, pop-out+геометрия;
|
||||
- список слайсов: PanId, TargetHz, mode/filter/AGC/DSP, звуковуха
|
||||
(DeviceIndex + имя для повторного матчинга), громкость/mute;
|
||||
- восстановление на старте: сначала паны (ConfigureDDCs), потом слайсы.
|
||||
Это закрывает и старый долг «персист слайсов» из Этапа 2.
|
||||
|
||||
---
|
||||
|
||||
## Оговорки / риски
|
||||
|
||||
- CPU софтовых слайсов ~линейно от их числа: каждый WDSP-канал жуёт полный rate и
|
||||
децимирует сам (аппаратный DDC дешевле — децимирует железо). Для QO-100 span мал,
|
||||
несколько слайсов — норма.
|
||||
- Не портить входной IQ-буфер между каналами (fexchange0 пишет in-place).
|
||||
- Pluto: слайсы только в пределах захваченной полосы; выход за край = ретюн LO
|
||||
(двигает всё, при QO-100 LO залочен на маяк — не трогать).
|
||||
- Пороговая аккуратность: старт/стоп WDSP-каналов на лету (см. заметку про задержки
|
||||
RXA стоп/старт в `WDSPEngine.pas:~230`).
|
||||
- **CPU**: слайс на пане N>0 дешевле слайса на пане 0 (WDSP-канал жуёт
|
||||
192-768 кГц вместо полного rate). Дорогое место — рендер: каждый пан =
|
||||
свой спектр+водопад; FPS-гейт и перф-дисциплина из [[project_perf_profiling]]
|
||||
обязательны per-pan.
|
||||
- **Не портить входной IQ-буфер** между каналами (fexchange0 in-place) —
|
||||
правило Этапа 1, действует и для пан-аккумуляторов (копия до Spectrum0,
|
||||
как `FSpecBuf`, см. `WDSPEngine.pas:235`).
|
||||
- **Старт/стоп WDSP-каналов на лету** — задержки RXA стоп/старт
|
||||
(`WDSPEngine.pas:~230`), создание/закрытие пана держать вне аудио-глитчей.
|
||||
- **Ethernet**: кап 4 пана суммарно; следить за суммарным rate при выборе
|
||||
rate панов.
|
||||
- **DDCBase=2** на PureSignal-платах: доп. панам достаются DDC3+ — при
|
||||
4 панах на Angelia/Orion/Saturn это DDC2..5, ок.
|
||||
- **Qt6/Wayland + GL**: reparent канвы (3.5) и per-instance ресурсы (3.0) —
|
||||
два главных технических риска всего этапа; аудит 3.0 делать первым.
|
||||
- Смена sample rate главного пана переоткрывает его слайсы (уже реализовано) —
|
||||
панов N>0 это касаться не должно (их rate независим).
|
||||
|
||||
## Открытые вопросы (решить по ходу, не блокеры)
|
||||
|
||||
- CAT: что репортить как VFO A/B при активном слайсе на пане N>0
|
||||
(сейчас CAT смотрит на главный тракт; вероятно «активный слайс = VFO A»).
|
||||
- Перетаскивание слайса МЕЖДУ панами (drag флага на другой пан): вкусно,
|
||||
но не в первую итерацию — сначала слайс живёт в своём пане (кламп по
|
||||
span пана, как сейчас по захвату).
|
||||
- Авто-promotion (слайс уехал за край пана → предлагать новый пан) — нет,
|
||||
осознанно: только явное создание пана (решение «сначала статика» остаётся).
|
||||
- Настройки водопада (палитра/AGC/avg) per-pan или глобально — стартуем с
|
||||
наследования глобальных, разделим по запросу.
|
||||
|
||||
## Отвергнутые альтернативы (для истории)
|
||||
|
||||
Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
|
||||
панорамы на своём hard-DDC, представленный edge-маркером у края спектра
|
||||
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
|
||||
чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC =
|
||||
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
|
||||
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
|
||||
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
|
||||
но не как замена панадаптеру. Палитра «цвет+буква = личность слайса во всех
|
||||
местах» из черновика — остаётся в силе.
|
||||
|
||||
## Связанные заметки памяти
|
||||
|
||||
- project_pluto_integration, project_qo100_zoom_panel, project_qo100_decoder_lock
|
||||
- project_spectrum_zoom (GetViewWindow / оверлеи / маппинг мыши)
|
||||
- project_channels_feature (dropdown/форма-редактор как паттерн UI)
|
||||
- project_slices_plan (статус), project_pluto_integration, project_perf_profiling
|
||||
- project_spectrum_zoom (GetViewWindow / span-clip / маппинг мыши)
|
||||
- project_qo100_zoom_panel (прецедент доп. анализаторов WDSP, disp id 4/5!)
|
||||
- project_lazopengl_component (QOpenGLWidget/контексты на Qt6 — база для 3.5)
|
||||
|
||||
Reference in New Issue
Block a user