vRIO builds in the vPLASMA display

The plasma glass now lives in vRIO's encoder strip: PlasmaCanvas grew a
dot-pitch parameter (default keeps the standalone glass pixel-identical)
and is compile-linked into VRio.App at pitch 3, parked at the strip's
left slot. The axis gauges move to the grid's right edge, the strip is
as tall as the glass, and the status text sits between them with the
axis readout split onto two lines. The button grid also compacts: rows
shift up past the layout's empty row 0 and the spare bottom row is
trimmed (the shared CockpitLayout is untouched, so coordinates still
match RIOJoy's map).

Control strip: the COM row labels are now the device names -- vRIO and
vPLASMA -- coloured by port status (green open, gray closed), replacing
the port-status line. The plasma row auto-opens COM12 at startup like
the RIO row does COM11; one Rescan refreshes both pickers. Plasma log
lines share the wire log under a vPLASMA: prefix, and the standalone
gestures carry over (double-click self-test, right-click reset).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-10 15:30:51 -05:00
co-authored by Claude Fable 5
parent 359e1c0a40
commit 90bf6723dc
5 changed files with 217 additions and 71 deletions
+9 -5
View File
@@ -107,11 +107,15 @@ a null-modem cable work the same way.
The cockpit's second serial device is the **plasma display**: a 128×32
dot-matrix panel on COM2 (9600 8N1, no flow control) that the game draws
mission text and status graphics on. `VPlasma.App` is its software replica:
a bare-glass window that opens **COM12** (the device end of the plasma's
null-modem pair) on startup — retrying while the port is missing or busy,
port status in the title bar — decodes the display's command stream, and
renders the dot matrix in plasma orange, text mode included.
mission text and status graphics on. The software replica comes in two
forms. **Built into vRIO**: the panel's encoder strip hosts the glass at
top left, with its own port row in the control strip (label colour = port
status; auto-opens **COM12** at startup when present). And standalone,
`VPlasma.App`: a bare-glass window that opens **COM12** (the device end of
the plasma's null-modem pair) on startup — retrying while the port is
missing or busy, port status in the title bar. Both decode the display's
command stream and render the dot matrix in plasma orange, text mode
included; only one can hold COM12 at a time.
The command set was recovered from two Tesla 4.10 artifacts:
`CODE\RP\MUNGA_L4\L4PLASMA.CPP` (the game's driver — it renders everything
+19 -10
View File
@@ -11,10 +11,14 @@ namespace VPlasma.App;
/// </summary>
internal sealed class PlasmaCanvas : Control
{
private const int DotPitch = 5; // 4 px dot + 1 px gap → a 640×160 dot field
private const int DotSize = 4;
private const int Bezel = 4;
// Dot geometry: an N px pitch renders (N1) px dots with a 1 px gap. The
// standalone glass uses the default pitch 5 (4 px dots, a 640×160 dot
// field); vRIO embeds the same control at pitch 3 to fit its encoder strip.
private readonly int _dotPitch;
private readonly int _dotSize;
private static readonly Color BezelColor = Color.FromArgb(20, 18, 16);
private static readonly Color GlassColor = Color.FromArgb(26, 14, 6);
private static readonly Color UnlitDot = Color.FromArgb(46, 24, 10);
@@ -30,14 +34,14 @@ internal sealed class PlasmaCanvas : Control
private PlasmaCursorMode _cursorMode;
private int _cellWidth = 6, _cellHeight = 8;
public PlasmaCanvas()
public PlasmaCanvas(int dotPitch = 5)
{
_dotPitch = dotPitch;
_dotSize = dotPitch - 1;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.FixedWidth |
ControlStyles.FixedHeight, true);
Size = new Size(
VPlasmaDevice.Width * DotPitch + 2 * Bezel,
VPlasmaDevice.Height * DotPitch + 2 * Bezel);
Size = SizeFor(dotPitch);
BackColor = BezelColor;
_blinkTimer.Tick += (_, _) =>
@@ -49,6 +53,11 @@ internal sealed class PlasmaCanvas : Control
_blinkTimer.Start();
}
/// <summary>Control size at a given dot pitch: the dot field plus bezel.</summary>
public static Size SizeFor(int dotPitch) => new(
VPlasmaDevice.Width * dotPitch + 2 * Bezel,
VPlasmaDevice.Height * dotPitch + 2 * Bezel);
/// <summary>Snapshot the device state and repaint. Call on the UI thread.</summary>
public void UpdateFrom(VPlasmaDevice device)
{
@@ -77,7 +86,7 @@ internal sealed class PlasmaCanvas : Control
using (var glass = new SolidBrush(GlassColor))
g.FillRectangle(glass, Bezel - 4, Bezel - 4,
VPlasmaDevice.Width * DotPitch + 7, VPlasmaDevice.Height * DotPitch + 7);
VPlasmaDevice.Width * _dotPitch + 7, VPlasmaDevice.Height * _dotPitch + 7);
using var unlit = new SolidBrush(UnlitDot);
using var lit = new SolidBrush(LitDot);
@@ -86,7 +95,7 @@ internal sealed class PlasmaCanvas : Control
for (int y = 0; y < VPlasmaDevice.Height; ++y)
{
int rowOffset = y * VPlasmaDevice.Width;
int py = Bezel + y * DotPitch;
int py = Bezel + y * _dotPitch;
for (int x = 0; x < VPlasmaDevice.Width; ++x)
{
byte dot = _frame[rowOffset + x];
@@ -96,7 +105,7 @@ internal sealed class PlasmaCanvas : Control
{
brush = (dot & VPlasmaDevice.PixelHalf) != 0 ? half : lit;
}
g.FillRectangle(brush, Bezel + x * DotPitch, py, DotSize, DotSize);
g.FillRectangle(brush, Bezel + x * _dotPitch, py, _dotSize, _dotSize);
}
}
@@ -108,7 +117,7 @@ internal sealed class PlasmaCanvas : Control
int cy = _cursor.Row * _cellHeight + _cellHeight - 1;
if (cy < VPlasmaDevice.Height)
for (int i = 0; i < _cellWidth && cx + i < VPlasmaDevice.Width; ++i)
g.FillRectangle(lit, Bezel + (cx + i) * DotPitch, Bezel + cy * DotPitch, DotSize, DotSize);
g.FillRectangle(lit, Bezel + (cx + i) * _dotPitch, Bezel + cy * _dotPitch, _dotSize, _dotSize);
}
}
+139 -37
View File
@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.IO.Ports;
using VPlasma.App;
using VPlasma.Core.Device;
using VRio.Core.Device;
using VRio.Core.Input;
@@ -8,11 +10,14 @@ namespace VRio.App;
/// <summary>
/// vRIO main window: the interactive cockpit panel on the left (the same
/// functional map RIOJoy's profile editor shows) and a control strip on the
/// right — COM port, device settings, and a live wire log. At startup the
/// usual port (<see cref="PreferredPort"/>) is opened automatically when it's
/// available; otherwise open a COM port by hand. Point RIOJoy at the other
/// end of the null-modem pair, and every click here arrives at RIOJoy exactly
/// as if the physical cockpit sent it.
/// right — COM ports, device settings, and a live wire log. The panel's
/// encoder strip also hosts the built-in plasma glass (the vPLASMA display
/// emulator) on its own COM port. At startup the usual ports
/// (<see cref="PreferredPort"/>, <see cref="PreferredPlasmaPort"/>) are
/// opened automatically when available; otherwise open them by hand. Point
/// RIOJoy at the other end of the null-modem pair, and every click here
/// arrives at RIOJoy exactly as if the physical cockpit sent it; point the
/// game's COM2 passthrough at the plasma pair and the glass lights up.
/// </summary>
internal sealed class MainForm : Form
{
@@ -23,9 +28,22 @@ internal sealed class MainForm : Form
/// </summary>
private const string PreferredPort = "COM11";
/// <summary>
/// The built-in plasma's usual port: the device end of the COM2⇄COM12
/// com0com pair (the game's COM2 passthrough opens the other end).
/// </summary>
private const string PreferredPlasmaPort = "COM12";
private readonly VRioDevice _device = new();
private readonly VRioSerialService _service;
private readonly VPlasmaDevice _plasmaDevice = new();
private readonly VPlasmaSerialService _plasmaService;
private readonly PanelCanvas _canvas = new();
private readonly PlasmaCanvas _plasmaCanvas = new(PanelCanvas.PlasmaDotPitch)
{
Location = PanelCanvas.PlasmaSlot,
};
private int _selfTestPage;
private readonly InputRouter _router;
private readonly XInputGamepad _gamepad = new();
private readonly KeyboardLampMirror _lampMirror;
@@ -34,6 +52,16 @@ internal sealed class MainForm : Form
private readonly string _bindingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "vRIO", "bindings.txt");
// Port rows: one line per built-in device — a name label whose colour is
// the port status (green = open, gray = closed), the port picker, and an
// Open/Close button. The one Rescan button refreshes both pickers.
private readonly Label _rioLabel = new()
{
Text = "vRIO",
Location = new Point(12, 15),
AutoSize = true,
ForeColor = Color.Gray,
};
private readonly ComboBox _portBox = new()
{
Location = new Point(80, 12),
@@ -42,13 +70,20 @@ internal sealed class MainForm : Form
};
private readonly Button _rescan = new() { Text = "Rescan", Location = new Point(214, 11), Width = 52 };
private readonly Button _openClose = new() { Text = "Open", Location = new Point(272, 11), Width = 46 };
private readonly Label _linkStatus = new()
private readonly Label _plasmaLabel = new()
{
Text = "Port closed.",
Location = new Point(12, 42),
Text = "vPLASMA",
Location = new Point(12, 45),
AutoSize = true,
ForeColor = Color.Gray,
};
private readonly ComboBox _plasmaPortBox = new()
{
Location = new Point(80, 42),
Width = 128,
DropDownStyle = ComboBoxStyle.DropDownList,
};
private readonly Button _plasmaOpenClose = new() { Text = "Open", Location = new Point(272, 41), Width = 46 };
private readonly CheckBox _spring = new() { Text = "Stick springs back to center", Location = new Point(10, 24), AutoSize = true, Checked = true };
private readonly Button _centerAxes = new() { Text = "Center all axes", Location = new Point(10, 52), Width = 140, Height = 26 };
@@ -112,7 +147,8 @@ internal sealed class MainForm : Form
AutoSize = true,
ForeColor = Color.Gray,
Text = "Left-click a cell: momentary press. Right-click: latch it down. " +
"Drag the X/Y box and the Z / L / R gauges to move the axes.",
"Drag the X/Y box and the Z / L / R gauges to move the axes. " +
"Double-click the plasma glass to cycle its self-test pages; right-click it to reset.",
};
private readonly TextBox _logBox = new()
@@ -153,11 +189,14 @@ internal sealed class MainForm : Form
KeyPreview = true; // form-level key routing for the input bindings
_service = new VRioSerialService(_device);
_plasmaService = new VPlasmaSerialService(_plasmaDevice);
_router = new InputRouter(_device);
_lampMirror = new KeyboardLampMirror(_device);
// Panel canvas, scrolled if the window is smaller than the grid.
// Panel canvas, scrolled if the window is smaller than the grid. The
// plasma glass rides along as a child in the encoder strip's left slot.
var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) };
_canvas.Controls.Add(_plasmaCanvas);
scroller.Controls.Add(_canvas);
Controls.Add(scroller);
@@ -186,8 +225,22 @@ internal sealed class MainForm : Form
_service.HostHandshake += high => RunOnUi(() =>
PrependLog(high ? "Host raised DTR (board-reset handshake)" : "Host dropped DTR"));
// Built-in plasma glass: a pure listener on its own port, same gestures
// as the standalone vPLASMA — double-click cycles the self-test pages,
// right-click resets the display to its power-on state.
_plasmaDevice.Updated += () => RunOnUi(() => _plasmaCanvas.UpdateFrom(_plasmaDevice));
_plasmaService.Logged += line => RunOnUi(() => PrependLog($"vPLASMA: {line}"));
_plasmaService.ConnectionChanged += open => RunOnUi(() => OnPlasmaConnectionChanged(open));
_plasmaCanvas.DoubleClick += (_, _) => RunPlasmaSelfTest();
_plasmaCanvas.MouseClick += (_, e) =>
{
if (e.Button == MouseButtons.Right)
_plasmaDevice.Reset();
};
_rescan.Click += (_, _) => RefreshPorts();
_openClose.Click += (_, _) => ToggleOpen();
_plasmaOpenClose.Click += (_, _) => TogglePlasmaOpen();
_spring.CheckedChanged += (_, _) => _canvas.StickSpringsBack = _spring.Checked;
_centerAxes.Click += (_, _) =>
{
@@ -251,13 +304,15 @@ internal sealed class MainForm : Form
_lampMirror.Dispose();
_rawKeyboard.Dispose();
_service.Dispose();
_plasmaService.Dispose();
};
RefreshPorts();
UpdateStatus();
PrependLog("vRIO ready. Open a COM port, then point RIOJoy at the other end of the pair.");
LoadBindings();
AutoOpenPreferredPort();
_plasmaCanvas.UpdateFrom(_plasmaDevice); // paint the power-on frame
AutoOpenPreferredPorts();
}
private Panel BuildControlStrip()
@@ -275,11 +330,13 @@ internal sealed class MainForm : Form
BorderStyle = BorderStyle.FixedSingle,
};
panel.Controls.Add(new Label { Text = "COM port:", Location = new Point(12, 15), AutoSize = true });
panel.Controls.Add(_rioLabel);
panel.Controls.Add(_portBox);
panel.Controls.Add(_rescan);
panel.Controls.Add(_openClose);
panel.Controls.Add(_linkStatus);
panel.Controls.Add(_plasmaLabel);
panel.Controls.Add(_plasmaPortBox);
panel.Controls.Add(_plasmaOpenClose);
var device = new GroupBox { Text = "Device", Location = new Point(12, 68), Size = new Size(306, 88) };
device.Controls.AddRange(new Control[] { _spring, _centerAxes, _lampsOff });
@@ -306,52 +363,80 @@ internal sealed class MainForm : Form
private void RefreshPorts()
{
string? current = _portBox.SelectedItem?.ToString();
_portBox.Items.Clear();
foreach (string name in SerialPort.GetPortNames().Distinct().OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
_portBox.Items.Add(name);
string[] names = SerialPort.GetPortNames().Distinct()
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ToArray();
// An open service's picker is disabled and shows the served port;
// leave it alone so the display can't drift from reality.
if (!_service.IsOpen)
RefreshPicker(_portBox, names);
if (!_plasmaService.IsOpen)
RefreshPicker(_plasmaPortBox, names);
}
if (_portBox.Items.Count == 0)
private static void RefreshPicker(ComboBox box, string[] portNames)
{
string? current = box.SelectedItem?.ToString();
box.Items.Clear();
foreach (string name in portNames)
box.Items.Add(name);
if (box.Items.Count == 0)
return;
int idx = current is null ? -1 : _portBox.Items.IndexOf(current);
_portBox.SelectedIndex = idx >= 0 ? idx : 0;
int idx = current is null ? -1 : box.Items.IndexOf(current);
box.SelectedIndex = idx >= 0 ? idx : 0;
}
/// <summary>
/// Startup convenience: if <see cref="PreferredPort"/> exists, select it
/// and try to open it. Failures (port missing, or busy because another
/// vRIO/app holds it) just log — no modal box at launch — and leave the
/// manual picker in charge.
/// Startup convenience: if a device's usual port exists, select it and try
/// to open it. Failures (port missing, or busy because another vRIO/app
/// holds it) just log — no modal box at launch — and leave the manual
/// pickers in charge.
/// </summary>
private void AutoOpenPreferredPort()
private void AutoOpenPreferredPorts()
{
int idx = _portBox.Items.IndexOf(PreferredPort);
AutoOpen(_portBox, PreferredPort, _service.Open);
AutoOpen(_plasmaPortBox, PreferredPlasmaPort, _plasmaService.Open);
}
private void AutoOpen(ComboBox box, string port, Action<string> open)
{
int idx = box.Items.IndexOf(port);
if (idx < 0)
{
PrependLog($"{PreferredPort} not present — pick a port and open it manually.");
PrependLog($"{port} not present — pick a port and open it manually.");
return;
}
_portBox.SelectedIndex = idx;
box.SelectedIndex = idx;
try
{
_service.Open(PreferredPort);
open(port);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
PrependLog($"{PreferredPort} is present but could not be opened ({ex.Message.TrimEnd('.')}) — open it manually once it frees up.");
PrependLog($"{port} is present but could not be opened ({ex.Message.TrimEnd('.')}) — open it manually once it frees up.");
}
}
private void ToggleOpen()
{
if (_service.IsOpen)
{
_service.Close();
return;
else
OpenFromPicker(_portBox, _service.Open);
}
if (_portBox.SelectedItem?.ToString() is not { } port)
private void TogglePlasmaOpen()
{
if (_plasmaService.IsOpen)
_plasmaService.Close();
else
OpenFromPicker(_plasmaPortBox, _plasmaService.Open);
}
private void OpenFromPicker(ComboBox box, Action<string> open)
{
if (box.SelectedItem?.ToString() is not { } port)
{
MessageBox.Show(this, "No COM port selected. On a single PC, install a com0com virtual " +
"null-modem pair and open one end here.", "vRIO", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -360,7 +445,7 @@ internal sealed class MainForm : Form
try
{
_service.Open(port);
open(port);
}
catch (Exception ex)
{
@@ -372,12 +457,29 @@ internal sealed class MainForm : Form
private void OnConnectionChanged(bool open)
{
_openClose.Text = open ? "Close" : "Open";
_portBox.Enabled = _rescan.Enabled = !open;
_linkStatus.Text = open ? $"Serving {_service.PortName} @ 9600 8N1." : "Port closed.";
_linkStatus.ForeColor = open ? Color.ForestGreen : Color.Gray;
_portBox.Enabled = !open;
_rioLabel.ForeColor = open ? Color.ForestGreen : Color.Gray;
UpdateStatus();
}
private void OnPlasmaConnectionChanged(bool open)
{
_plasmaOpenClose.Text = open ? "Close" : "Open";
_plasmaPortBox.Enabled = !open;
_plasmaLabel.ForeColor = open ? Color.ForestGreen : Color.Gray;
}
/// <summary>
/// Feed the next canned self-test page through the plasma's wire parser —
/// the same end-to-end exercise the standalone vPLASMA runs on double-click.
/// </summary>
private void RunPlasmaSelfTest()
{
byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage);
_plasmaDevice.OnReceived(bytes, bytes.Length);
_selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount;
}
// ---- Keyboard / gamepad input -------------------------------------------
/// <summary>
+44 -18
View File
@@ -1,3 +1,4 @@
using VPlasma.App;
using VRio.Core.Device;
using VRio.Core.Panel;
using VRio.Core.Protocol;
@@ -16,6 +17,10 @@ namespace VRio.App;
/// fill toward full travel (negative on the wire for the throttle, positive
/// for the pedals), and the stick covers ±80 around its center.
///
/// <para>The strip's left slot hosts the built-in plasma glass: MainForm parks
/// its <see cref="PlasmaCanvas"/> child at <see cref="PlasmaSlot"/>, and the
/// strip height and gauge positions derive from the glass size.</para>
///
/// <para>Left-click = momentary press (release on mouse-up). Right-click =
/// latch the button down / release it (handy for testing holds).</para>
/// </summary>
@@ -24,16 +29,27 @@ internal sealed class PanelCanvas : Control
// Cell geometry — identical to RIOJoy's PanelView so the two panels align.
private const int CellW = 66;
private const int CellH = 34;
private const int TopStrip = 78; // encoder strip: gauges end at 74, small gap to the grid
private static readonly IReadOnlyList<PanelButton> AllButtons = CockpitLayout.Buttons();
// Encoder-strip geometry (same placement as RIOJoy's PanelView): Z gauge,
// the pedal gauges L / R, and the X/Y stick box.
// First grid row any group occupies (the shared layout leaves row 0
// empty). Rendering shifts all rows up by this, so the button grid starts
// right under the encoder strip instead of a blank cell row below it.
private static readonly int FirstRow = CockpitLayout.Groups.Min(g => g.OriginRow);
// Encoder-strip geometry. The built-in plasma glass parks at the strip's
// left edge (MainForm adds its PlasmaCanvas as a child at PlasmaSlot),
// the axis gauges — Z, the pedal gauges L / R, and the X/Y stick box —
// hug the grid's right edge, and the green status text fills the gap
// between them. The strip is exactly as tall as the glass.
internal const int PlasmaDotPitch = 3; // 2 px dots: the 128×32 glass fits the strip
internal static readonly Size PlasmaSize = PlasmaCanvas.SizeFor(PlasmaDotPitch);
private const int StripTop = 6;
private const int StripH = 68;
internal static readonly Point PlasmaSlot = new(6, StripTop);
private static readonly int StripH = PlasmaSize.Height;
private static readonly int TopStrip = StripTop + StripH + 6; // button grid starts under the strip
private const int Bar = 30;
private static readonly int BaseX = 6 * CellW - 100;
private static readonly int BaseX = GridWidth() - 6 - 200; // the gauge cluster spans 200 px, right-aligned
private static readonly Rectangle BoxZ = new(BaseX, StripTop, Bar, StripH);
private static readonly Rectangle BoxL = new(BaseX + 34, StripTop, Bar, StripH);
private static readonly Rectangle BoxR = new(BaseX + 34 + 82 - Bar, StripTop, Bar, StripH);
@@ -109,19 +125,27 @@ internal sealed class PanelCanvas : Control
base.Dispose(disposing);
}
// Split from GridSize so the static gauge layout can use the width
// without touching TopStrip (which initializes after AllButtons).
private static int GridWidth()
{
int maxCol = 0;
foreach (PanelButton b in AllButtons)
if (b.Col > maxCol) maxCol = b.Col;
return (maxCol + 1) * CellW + 6;
}
private static Size GridSize()
{
int maxCol = 0, maxRow = 0;
int maxRow = 0;
foreach (PanelButton b in AllButtons)
{
if (b.Col > maxCol) maxCol = b.Col;
if (b.Row > maxRow) maxRow = b.Row;
}
return new Size((maxCol + 1) * CellW + 6, TopStrip + (maxRow + 2) * CellH);
// The last used row plus a small margin under the lowest group frame.
return new Size(GridWidth(), TopStrip + (maxRow - FirstRow + 1) * CellH + 6);
}
private static Rectangle Cell(int col, int row) =>
new(col * CellW + 2, TopStrip + row * CellH + 2, CellW - 4, CellH - 4);
new(col * CellW + 2, TopStrip + (row - FirstRow) * CellH + 2, CellW - 4, CellH - 4);
// ---- Painting ----------------------------------------------------------
@@ -142,7 +166,7 @@ internal sealed class PanelCanvas : Control
{
var frame = new Rectangle(
grp.OriginCol * CellW + 1,
TopStrip + grp.OriginRow * CellH + 1,
TopStrip + (grp.OriginRow - FirstRow) * CellH + 1,
grp.Cols * CellW,
(grp.Rows + 1) * CellH);
g.DrawRectangle(groupPen, frame);
@@ -288,10 +312,12 @@ internal sealed class PanelCanvas : Control
new Rectangle(BoxXY.X, BoxXY.Bottom - 16, BoxXY.Width, 14), Color.Gainsboro,
TextFormatFlags.HorizontalCenter | TextFormatFlags.Bottom);
// The mockup's green status/instruction area, right of the gauges; the
// live axis readout sits directly under the status lines (painted per
// frame, so drags track).
var statusRect = new Rectangle(BoxXY.Right + 16, StripTop, Width - BoxXY.Right - 24, TopStrip - StripTop - 6);
// The mockup's green status/instruction area, between the plasma glass
// and the gauges; the live axis readout sits directly under the status
// lines (painted per frame, so drags track).
var statusRect = new Rectangle(
PlasmaSlot.X + PlasmaSize.Width + 12, StripTop,
BoxZ.Left - PlasmaSlot.X - PlasmaSize.Width - 24, StripH);
if (statusRect.Width > 60)
{
using var statusFont = new Font("Consolas", 8f);
@@ -307,11 +333,11 @@ internal sealed class PanelCanvas : Control
short WireAxis(RioAxis a) => WireAxisProvider?.Invoke(a) ?? Axis(a);
string readout =
$"Z {WireAxis(RioAxis.Throttle),4} L {WireAxis(RioAxis.LeftPedal),3} R {WireAxis(RioAxis.RightPedal),3} " +
$"Z {WireAxis(RioAxis.Throttle),4} L {WireAxis(RioAxis.LeftPedal),3} R {WireAxis(RioAxis.RightPedal),3}\n" +
$"X {WireAxis(RioAxis.JoystickX),3} Y {WireAxis(RioAxis.JoystickY),3}";
TextRenderer.DrawText(g, readout, statusFont,
new Rectangle(statusRect.X, readoutTop, statusRect.Width, statusRect.Bottom - readoutTop), green,
TextFormatFlags.Left | TextFormatFlags.Top | TextFormatFlags.SingleLine);
TextFormatFlags.Left | TextFormatFlags.Top);
}
}
+5
View File
@@ -17,6 +17,11 @@
<ItemGroup>
<ProjectReference Include="..\VRio.Core\VRio.Core.csproj" />
<!-- The built-in plasma glass: vPLASMA's device/protocol core, plus its
canvas control compiled in directly (shared file, not a library —
VPlasma.Core stays UI-free). -->
<ProjectReference Include="..\VPlasma.Core\VPlasma.Core.csproj" />
<Compile Include="..\VPlasma.App\PlasmaCanvas.cs" Link="PlasmaCanvas.cs" />
</ItemGroup>
<!-- Stamp commit date + short sha into InformationalVersion; the window