Add per-channel independent limit controls

- Separate Lim A / Lim B trackbars (0-200, default 30 each)
- Separate Scale A / Scale B checkboxes (clamp vs scale per channel)
- State.SetLimitA/SetLimitB with independent LimitModeA/LimitModeB
- ConsumeTick applies per-channel limit and mode
This commit is contained in:
2026-08-08 11:26:44 +00:00
parent 325022d27c
commit 33e289d596
3 changed files with 211 additions and 54 deletions
+109
View File
@@ -0,0 +1,109 @@
using System.Drawing.Drawing2D;
namespace Substation;
public class HeatMap : Panel
{
record Tick(byte[] Freq, byte[] Intensity);
readonly List<Tick> _history = new();
readonly int _maxTicks;
public HeatMap(int maxTicks = 60)
{
_maxTicks = maxTicks;
DoubleBuffered = true;
BackColor = Color.FromArgb(20, 20, 22);
}
public void AddTick(byte[] freq, byte[] intensity, bool active)
{
if (_history.Count >= _maxTicks)
_history.RemoveAt(0);
if (active)
_history.Add(new Tick(freq, intensity));
else
_history.Add(new Tick(Array.Empty<byte>(), Array.Empty<byte>()));
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.None;
int w = Width;
int h = Height;
int colW = Math.Max(1, w / _maxTicks);
// Draw axis labels
using var font = new Font(FontFamily.GenericMonospace, 7);
using var textBrush = new SolidBrush(Color.FromArgb(100, 100, 100));
g.DrawString("100Hz", font, textBrush, 2, 2);
g.DrawString("1Hz", font, textBrush, 2, h - 14);
int plotX = 36;
int plotW = w - plotX - 4;
int plotH = h - 4;
colW = Math.Max(1, plotW / _maxTicks);
for (int i = 0; i < _history.Count; i++)
{
var tick = _history[i];
int xPos = plotX + i * colW;
if (tick.Freq.Length == 0)
{
// Inactive — dark cell
using var dark = new SolidBrush(Color.FromArgb(25, 25, 28));
g.FillRectangle(dark, xPos, 2, colW, plotH);
continue;
}
// 4 sub-ticks side by side within the column
int subW = Math.Max(1, colW / 4);
for (int s = 0; s < 4; s++)
{
int freq = tick.Freq[s]; // 10-240
int inten = tick.Intensity[s]; // 0-100
if (inten > 100) inten = 0; // 101 sentinel = off
// Map freq byte (10-240) to Y position (top=high freq, bottom=low freq)
float normFreq = (freq - 10f) / (240f - 10f);
int y = (int)(2 + plotH * (1f - normFreq));
if (y < 2) y = 2;
if (y > plotH) y = plotH;
var color = HeatColor(inten);
using var brush = new SolidBrush(color);
g.FillRectangle(brush, xPos + s * subW, y, subW, 3);
}
}
}
static Color HeatColor(int intensity)
{
// 0=black, 30=blue, 60=green, 80=gold, 100=red
if (intensity <= 0) return Color.FromArgb(20, 20, 22);
if (intensity < 30)
{
float t = intensity / 30f;
return Color.FromArgb(0, (int)(50 * t), (int)(80 + 80 * t));
}
if (intensity < 60)
{
float t = (intensity - 30) / 30f;
return Color.FromArgb((int)(60 * t), (int)(130 + 80 * t), (int)(160 - 100 * t));
}
if (intensity < 80)
{
float t = (intensity - 60) / 20f;
return Color.FromArgb((int)(60 + 180 * t), 210, (int)(60 - 30 * t));
}
float t2 = (intensity - 80) / 20f;
return Color.FromArgb(255, (int)(240 - 140 * t2), (int)(30 - 10 * t2));
}
}