feat(slices): per-pan дисплей — палитра/gamma/уровни на пан («◑»-поповер)

Кнопка «◑» в шапке каждого пана открывает компактный поповер настроек
отображения (палитра, gamma, ref/range спектра, авто-уровень водопада +
offset/ручные min-max). Модель «глобальный дефолт + override на пан»: пан 0
правит глобальные поля рендера (зеркалятся в контроллер), паны N ставят
DisplayOverride и персистятся в воркспейс раздельно КВ/трансвертер.

- PanDisplayPopup.pas (new): borderless fsStayOnTop форма, гаснет по
  OnDeactivate; Flat*-контролы; крестик «×» закрытия; дим-хинт «правки
  применяются и сохраняются сами»; кнопки «⟳ Reset»/«⇄ Copy to all».
- TPanafallPanel: FBtnDisplay + OnDisplayClick + DisplayOverride;
  PositionHeaderChildren каскадит [×][⧉][◑] справа по Visible.
- MainForm: OnPanDisplayClick/Applied/Reset/ApplyAll + ApplyGlobalRenderToView
  + MarkPanDisplayDirty. При смене палитры/сбросе чистим историю GL-водопада
  (TSpectrumView.ClearWaterfall) — GL хранит уже раскрашенные пиксели.
- Settings/TPanCfg: под-объект 'display' (палитра/gamma/ref/range/wf-уровни),
  пишется только при override.

Глобальными осознанно оставлены разрешение анализатора, MSAA, скорость
водопада, детектор/усреднение (движок/перф, не косметика).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 13:49:19 +03:00
co-authored by Claude Opus 4.8
parent 543453007e
commit 3566f10191
6 changed files with 666 additions and 10 deletions
+52 -3
View File
@@ -110,6 +110,17 @@ type
StackShare: Double; // доля высоты в стеке панов
ColShare: Double; // доля ширины в грид-раскладке
SplitRatio: Double; // доля высоты спектра (сплиттер спектр/водопад)
// Per-pan дисплей (3.6): override глобального рендера. HasDisplay=False ⇒
// пан наследует глобальный дефолт (поля игнорируются).
HasDisplay: Boolean;
DPalette: Integer; // 0=Classic/1=Inferno/2=Turbo
DGamma: Double;
DSpecRef: Double; // верх шкалы спектра, dBm
DSpecRange: Double; // диапазон шкалы, dB
DWfAGC: Boolean; // авто-уровень водопада
DWfOffset: Double; // яркость водопада при AGC, dB
DWfHigh: Double; // ручной верх водопада, dBm
DWfLow: Double; // ручной низ водопада, dBm
Slices: array of TSliceCfg;
end;
@@ -1608,6 +1619,15 @@ begin
P.Pans[i].StackShare := 1.0;
P.Pans[i].ColShare := 1.0;
P.Pans[i].SplitRatio := 0.40; // = TPanafallPanel дефолт
P.Pans[i].HasDisplay := False;
P.Pans[i].DPalette := 0;
P.Pans[i].DGamma := 1.0;
P.Pans[i].DSpecRef := -20.0;
P.Pans[i].DSpecRange := 110.0;
P.Pans[i].DWfAGC := True;
P.Pans[i].DWfOffset := 0.0;
P.Pans[i].DWfHigh := -60.0;
P.Pans[i].DWfLow := -120.0;
SetLength(P.Pans[i].Slices, 0);
end;
end;
@@ -1615,7 +1635,7 @@ end;
procedure TSettingsManager.SavePans(const MAC: array of Byte; const Ctx: string;
const P: TPansConfig);
var
Root, PanObj, SliceObj: TJSONObject;
Root, PanObj, SliceObj, DispObj: TJSONObject;
PanArr, SliceArr: TJSONArray;
i, j, Idx: Integer;
begin
@@ -1643,6 +1663,20 @@ begin
PanObj.Add('stack_share', P.Pans[i].StackShare);
PanObj.Add('col_share', P.Pans[i].ColShare);
PanObj.Add('split_ratio', P.Pans[i].SplitRatio);
// Per-pan дисплей: пишем блок только при override (иначе пан = глоб. дефолт).
if P.Pans[i].HasDisplay then
begin
DispObj := TJSONObject.Create;
PanObj.Add('display', DispObj);
DispObj.Add('palette', P.Pans[i].DPalette);
DispObj.Add('gamma', P.Pans[i].DGamma);
DispObj.Add('spec_ref', P.Pans[i].DSpecRef);
DispObj.Add('spec_range', P.Pans[i].DSpecRange);
DispObj.Add('wf_agc', P.Pans[i].DWfAGC);
DispObj.Add('wf_offset', P.Pans[i].DWfOffset);
DispObj.Add('wf_high', P.Pans[i].DWfHigh);
DispObj.Add('wf_low', P.Pans[i].DWfLow);
end;
SliceArr := TJSONArray.Create;
PanObj.Add('slices', SliceArr);
for j := 0 to High(P.Pans[i].Slices) do
@@ -1667,8 +1701,8 @@ function TSettingsManager.LoadPans(const MAC: array of Byte; const Ctx: string;
out P: TPansConfig): Boolean;
var
MacStr: string;
DevObj, PansObj, Root, PanObj, SliceObj: TJSONObject;
ListData, SlData: TJSONData;
DevObj, PansObj, Root, PanObj, SliceObj, DispObj: TJSONObject;
ListData, SlData, DispData: TJSONData;
Arr, SArr: TJSONArray;
i, id, k, n: Integer;
begin
@@ -1701,6 +1735,21 @@ begin
P.Pans[id].StackShare := JD(PanObj, 'stack_share', 1.0);
P.Pans[id].ColShare := JD(PanObj, 'col_share', 1.0);
P.Pans[id].SplitRatio := EnsureRange(JD(PanObj, 'split_ratio', 0.40), 0.05, 0.95);
// Per-pan дисплей: присутствие блока 'display' = override.
DispData := PanObj.Find('display');
if (DispData <> nil) and (DispData is TJSONObject) then
begin
DispObj := TJSONObject(DispData);
P.Pans[id].HasDisplay := True;
P.Pans[id].DPalette := EnsureRange(JI(DispObj, 'palette', 0), 0, 2);
P.Pans[id].DGamma := EnsureRange(JD(DispObj, 'gamma', 1.0), 0.30, 3.00);
P.Pans[id].DSpecRef := JD(DispObj, 'spec_ref', -20.0);
P.Pans[id].DSpecRange := JD(DispObj, 'spec_range', 110.0);
P.Pans[id].DWfAGC := JB(DispObj, 'wf_agc', True);
P.Pans[id].DWfOffset := JD(DispObj, 'wf_offset', 0.0);
P.Pans[id].DWfHigh := JD(DispObj, 'wf_high', -60.0);
P.Pans[id].DWfLow := JD(DispObj, 'wf_low', -120.0);
end;
SlData := PanObj.Find('slices');
if (SlData = nil) or not (SlData is TJSONArray) then Continue;
SArr := TJSONArray(SlData);