diff --git a/README.md b/README.md
index afb21fc..e337ed1 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/VPlasma.App/PlasmaCanvas.cs b/src/VPlasma.App/PlasmaCanvas.cs
index 613a30f..f42b393 100644
--- a/src/VPlasma.App/PlasmaCanvas.cs
+++ b/src/VPlasma.App/PlasmaCanvas.cs
@@ -11,10 +11,14 @@ namespace VPlasma.App;
///
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 (N−1) 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();
}
+ /// Control size at a given dot pitch: the dot field plus bezel.
+ public static Size SizeFor(int dotPitch) => new(
+ VPlasmaDevice.Width * dotPitch + 2 * Bezel,
+ VPlasmaDevice.Height * dotPitch + 2 * Bezel);
+
/// Snapshot the device state and repaint. Call on the UI thread.
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);
}
}
diff --git a/src/VRio.App/MainForm.cs b/src/VRio.App/MainForm.cs
index 857cfd8..9733d8b 100644
--- a/src/VRio.App/MainForm.cs
+++ b/src/VRio.App/MainForm.cs
@@ -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;
///
/// 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 () 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
+/// (, ) 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.
///
internal sealed class MainForm : Form
{
@@ -23,9 +28,22 @@ internal sealed class MainForm : Form
///
private const string PreferredPort = "COM11";
+ ///
+ /// 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).
+ ///
+ 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;
}
///
- /// Startup convenience: if 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.
///
- 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 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 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ private void RunPlasmaSelfTest()
+ {
+ byte[] bytes = PlasmaSelfTest.BuildPage(_selfTestPage);
+ _plasmaDevice.OnReceived(bytes, bytes.Length);
+ _selfTestPage = (_selfTestPage + 1) % PlasmaSelfTest.PageCount;
+ }
+
// ---- Keyboard / gamepad input -------------------------------------------
///
diff --git a/src/VRio.App/PanelCanvas.cs b/src/VRio.App/PanelCanvas.cs
index b17a99b..6be1582 100644
--- a/src/VRio.App/PanelCanvas.cs
+++ b/src/VRio.App/PanelCanvas.cs
@@ -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.
///
+/// The strip's left slot hosts the built-in plasma glass: MainForm parks
+/// its child at , and the
+/// strip height and gauge positions derive from the glass size.
+///
/// Left-click = momentary press (release on mouse-up). Right-click =
/// latch the button down / release it (handy for testing holds).
///
@@ -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 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);
}
}
diff --git a/src/VRio.App/VRio.App.csproj b/src/VRio.App/VRio.App.csproj
index e9c0da0..c6325f1 100644
--- a/src/VRio.App/VRio.App.csproj
+++ b/src/VRio.App/VRio.App.csproj
@@ -17,6 +17,11 @@
+
+
+