Editor: rework to the original Win32 RIO control-panel layout
Reworked the profile/mapping editor to mirror the original unfinished Win32 RIO driver's control-panel design (docs/Win32RIO, by FASA / Michel Lowrance) instead of the flat config-sheet grid. The wallpaper region positions are a VGA chroma-split display artifact, not the cockpit's logical shape. - RioJoy.Core.Editing.CockpitPanel: the functional layout — five MFD clusters, four board columns (Throttle/Secondary/Screen/Joystick-Hat), an encoder-gauge strip, and the two later-added 4x4 keypads (Internal/External). Places every address 0x00-0x47 / 0x50-0x6F exactly once (unit-tested). - Tray PanelView/PanelCanvas render the panel; ProfileEditorForm drives it. Buttons show label + assigned function (ButtonBinding.Describe -> CTL-N, JOY1, POV-U, Mse LB, RIO command names; unit-tested). Lamp shade is driven by the IsLit flag (not by whether a function is assigned); keypads are neutral blue with no Lit checkbox. Added an Unassign button next to Apply. - Removed the superseded sheet-grid UI (SheetView/SheetCanvas); SheetLayout + config-sheet.csv stay as reference data. docs/Win32RIO: the original driver (tasgame.sys, a 32-bit kernel HID minidriver that opened the serial port, set baud/8N1/DTR, and spoke the *identical* RIO protocol -- AnalogRequest/Reply, Button Pressed/Released, Check/Lamp/Reset/ Version -- validating our protocol port), oemsetup.inf, RemoteDriver.doc, and the extracted control-panel / game-controllers mockups. ~236 xUnit tests green. PLAN.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
namespace RioJoy.Core.Editing;
|
||||
|
||||
/// <summary>Shape of a panel group.</summary>
|
||||
public enum PanelGroupKind
|
||||
{
|
||||
/// <summary>A 4×2 MFD button cluster (lamp buttons).</summary>
|
||||
Mfd,
|
||||
|
||||
/// <summary>A vertical 1×8 board column (lamp buttons).</summary>
|
||||
Column,
|
||||
|
||||
/// <summary>A 4×4 keypad (added later; no lamps).</summary>
|
||||
Keypad,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One functional group on the cockpit control panel: a titled block of address
|
||||
/// buttons laid out in <see cref="Rows"/> × <see cref="Cols"/> (row-major, null =
|
||||
/// empty slot). <see cref="OriginCol"/>/<see cref="OriginRow"/> place the block on
|
||||
/// the panel in button-cell units (the renderer scales cells to pixels).
|
||||
/// </summary>
|
||||
public sealed record PanelGroup(
|
||||
string Title,
|
||||
PanelGroupKind Kind,
|
||||
int Cols,
|
||||
int Rows,
|
||||
bool LampCapable,
|
||||
int OriginCol,
|
||||
int OriginRow,
|
||||
IReadOnlyList<int?> Addresses);
|
||||
|
||||
/// <summary>One address button placed on the panel (absolute cell coordinates).</summary>
|
||||
public sealed record PanelButton(int Address, PanelGroup Group, int Col, int Row, bool LampCapable);
|
||||
|
||||
/// <summary>
|
||||
/// The cockpit control panel's logical layout — the functional grouping from the
|
||||
/// original Win32 RIO design (docs/Win32RIO/control-panel-mockup.png): five MFD
|
||||
/// clusters, four board columns, and the two later-added 4×4 keypads. Every RIO
|
||||
/// address 0x00–0x47 / 0x50–0x6F is placed exactly once. Pure data so the mapping
|
||||
/// is unit-tested; the WinForms editor renders these groups and binds each button.
|
||||
/// </summary>
|
||||
public static class CockpitPanel
|
||||
{
|
||||
// Helper: 4×2 MFD address block, top row then bottom row (addresses descend).
|
||||
private static int?[] Mfd(int hi) => new int?[]
|
||||
{
|
||||
hi, hi - 1, hi - 2, hi - 3,
|
||||
hi - 4, hi - 5, hi - 6, hi - 7,
|
||||
};
|
||||
|
||||
// Helper: vertical 1×8 column from a base address upward.
|
||||
private static int?[] Col8(int baseAddr) => new int?[]
|
||||
{
|
||||
baseAddr, baseAddr + 1, baseAddr + 2, baseAddr + 3,
|
||||
baseAddr + 4, baseAddr + 5, baseAddr + 6, baseAddr + 7,
|
||||
};
|
||||
|
||||
// Helper: 4×4 keypad. Physical keys 1 2 3 C / 4 5 6 D / 7 8 9 E / A 0 B F map to
|
||||
// address = base + that hex digit.
|
||||
private static int?[] Keypad(int baseAddr)
|
||||
{
|
||||
int[] digits = { 0x1, 0x2, 0x3, 0xC, 0x4, 0x5, 0x6, 0xD, 0x7, 0x8, 0x9, 0xE, 0xA, 0x0, 0xB, 0xF };
|
||||
var a = new int?[16];
|
||||
for (int i = 0; i < 16; i++) a[i] = baseAddr + digits[i];
|
||||
return a;
|
||||
}
|
||||
|
||||
/// <summary>All panel groups, positioned for rendering.</summary>
|
||||
public static IReadOnlyList<PanelGroup> Groups { get; } = new[]
|
||||
{
|
||||
// Upper MFD row.
|
||||
new PanelGroup("Upper Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 1, Mfd(0x2F)),
|
||||
new PanelGroup("Upper Middle MFD", PanelGroupKind.Mfd, 4, 2, true, 5, 1, Mfd(0x27)),
|
||||
new PanelGroup("Upper Right MFD", PanelGroupKind.Mfd, 4, 2, true, 10, 1, Mfd(0x37)),
|
||||
|
||||
// Lower MFD row.
|
||||
new PanelGroup("Lower Left MFD", PanelGroupKind.Mfd, 4, 2, true, 0, 5, Mfd(0x0F)),
|
||||
new PanelGroup("Lower Right MFD", PanelGroupKind.Mfd, 4, 2, true, 10, 5, Mfd(0x07)),
|
||||
|
||||
// Board columns.
|
||||
new PanelGroup("Throttle", PanelGroupKind.Column, 1, 8, true, 0, 9, Col8(0x38)),
|
||||
new PanelGroup("Secondary", PanelGroupKind.Column, 1, 8, true, 5, 9, Col8(0x10)),
|
||||
new PanelGroup("Screen", PanelGroupKind.Column, 1, 8, true, 7, 9, Col8(0x18)),
|
||||
new PanelGroup("Joystick / Hat", PanelGroupKind.Column, 1, 8, true, 13, 9, Col8(0x40)),
|
||||
|
||||
// Keypads (no lamps): internal in the middle (below Upper Middle MFD, above
|
||||
// the Secondary/Screen columns); external between those columns and Joystick.
|
||||
new PanelGroup("Internal Keypad", PanelGroupKind.Keypad, 4, 4, false, 5, 4, Keypad(0x50)),
|
||||
new PanelGroup("External Keypad", PanelGroupKind.Keypad, 4, 4, false, 9, 9, Keypad(0x60)),
|
||||
};
|
||||
|
||||
/// <summary>Flatten the groups to positioned <see cref="PanelButton"/>s (absolute cells).</summary>
|
||||
public static IReadOnlyList<PanelButton> Buttons()
|
||||
{
|
||||
var buttons = new List<PanelButton>();
|
||||
foreach (PanelGroup g in Groups)
|
||||
{
|
||||
for (int i = 0; i < g.Addresses.Count; i++)
|
||||
{
|
||||
if (g.Addresses[i] is not { } addr) continue;
|
||||
int localCol = i % g.Cols;
|
||||
int localRow = i / g.Cols;
|
||||
// +1 row leaves space under the title.
|
||||
buttons.Add(new PanelButton(addr, g, g.OriginCol + localCol, g.OriginRow + 1 + localRow, g.LampCapable));
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,39 @@ public sealed class ButtonBinding
|
||||
/// <summary>True when this binding does nothing (an empty slot).</summary>
|
||||
public bool IsUnmapped => ToWord() == 0;
|
||||
|
||||
/// <summary>
|
||||
/// A short human-readable description of the assigned function — e.g.
|
||||
/// <c>CTL-N</c>, <c>JOY1</c>, <c>POV-U</c>, <c>Mse LB</c>, or a RIO command name.
|
||||
/// Empty when unmapped. Used as the editor cell caption.
|
||||
/// </summary>
|
||||
public string Describe() => Kind switch
|
||||
{
|
||||
_ when IsUnmapped => string.Empty,
|
||||
RioRouteKind.Keyboard =>
|
||||
(Ctrl ? "CTL-" : "") + (Shift ? "SFT-" : "") + (Alt ? "ALT-" : "") + KeyCatalog.NameFor(Value),
|
||||
RioRouteKind.Joystick => $"JOY{Value}",
|
||||
RioRouteKind.Hat => (RioHat)Value switch
|
||||
{
|
||||
RioHat.Up => "POV-U",
|
||||
RioHat.Right => "POV-R",
|
||||
RioHat.Down => "POV-D",
|
||||
RioHat.Left => "POV-L",
|
||||
_ => "POV",
|
||||
},
|
||||
RioRouteKind.Mouse => "Mse " + (MouseAction)Value switch
|
||||
{
|
||||
MouseAction.MoveUp => "Up",
|
||||
MouseAction.MoveDown => "Dn",
|
||||
MouseAction.MoveLeft => "Lt",
|
||||
MouseAction.MoveRight => "Rt",
|
||||
MouseAction.LeftClick => "LB",
|
||||
MouseAction.RightClick => "RB",
|
||||
_ => Value.ToString(),
|
||||
},
|
||||
RioRouteKind.RioCommand => ((RioCommandCode)Value).ToString(),
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
/// <summary>Pack to the 16-bit <c>iRIO</c> map word.</summary>
|
||||
public ushort ToWord() =>
|
||||
RioMapEntry.Create(Kind, Value, IsLit, Shift, Ctrl, Alt, Extended).Raw;
|
||||
|
||||
Reference in New Issue
Block a user