From 8d2d0b71aac44a595d8c739263d21d8a92fef2ab Mon Sep 17 00:00:00 2001 From: Cyd Date: Sat, 27 Jun 2026 17:13:48 -0500 Subject: [PATCH] Phase 7: cockpit overlay generator, region extraction, and profile editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overlay generator (pure, tested) — RioJoy.Core/Overlay: - FontFitter ports the legacy calc-fontsize auto-fit search (validated against a brute-force oracle); OverlayLayoutEngine ports create-data-layer's fit + h/v justification and adds per-region 90 deg CCW rotation; OverlayTemplate/Region + OverlayTemplateStore hold cell geometry/colour/rotation (regions.json). - GoobieDataImporter parses the legacy .data label sheet into label rows. - src/RioJoy.Overlay: SkiaSharp rasterizer (SkiaTextMeasurer shared with the engine so measured layout == drawn output; SkiaOverlayRenderer -> PNG; ProfileWallpaperGenerator). Verified end-to-end on the real cockpit art (regions.json + riojoy.png + TEST.data) by OverlayRenderIntegrationTests. - RioJoy.Tray/WallpaperApplier applies via SystemParametersInfo; RioCoordinator generates+applies on profile activation (opt-in via AppConfig.OverlayTemplatePath). Region geometry — tools/XcfRegionExtract parses riojoy.xcf (a GIMP-format binary reader, no GIMP needed) into the 119-cell regions.json: per-layer offsets/size/ font/colour, with 90 deg CCW rotation on a-00 + b-10..b-1F. Base image riojoy.png. NOTE: the wallpaper positions are a VGA chroma-split display artifact (6 displays), not the cockpit's logical layout. Mapping editor (first cut) — RioJoy.Tray/Editor/ProfileEditorForm renders the config sheet's logical grid (SheetLayout parses the sheet CSV export). The iRIO word is edited via ButtonBinding <-> RioMapEntry.Create (no hex bit-twiddling), with a context-sensitive picker: keyboard keys by name (KeyCatalog VK names), joystick Button N, hat direction, mouse/RIO-command enum names; modifiers only for keyboard. Opened from the tray ("Edit profile..."). The sheet-grid layout is a starting point that needs rework from a better-formatted source. ~220 xUnit tests green. Docs (PLAN.md, README) updated. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 4 + README.md | 21 +- RioJoy.sln | 7 + docs/PLAN.md | 60 +- .../customBackground/config-sheet.csv | 49 + docs/reference/customBackground/regions.json | 1214 +++++++++++++++++ docs/reference/customBackground/riojoy.png | Bin 0 -> 139267 bytes src/RioJoy.Core/Editing/SheetLayout.cs | 120 ++ src/RioJoy.Core/Mapping/ButtonBinding.cs | 56 + src/RioJoy.Core/Mapping/KeyCatalog.cs | 83 ++ src/RioJoy.Core/Mapping/RioMapEntry.cs | 36 + src/RioJoy.Core/Overlay/FontFitter.cs | 64 + src/RioJoy.Core/Overlay/GoobieDataImporter.cs | 118 ++ src/RioJoy.Core/Overlay/ITextMeasurer.cs | 17 + .../Overlay/OverlayLayoutEngine.cs | 97 ++ src/RioJoy.Core/Overlay/OverlayModels.cs | 115 ++ .../Overlay/OverlayTemplateStore.cs | 48 + src/RioJoy.Core/Profiles/AppConfig.cs | 7 + src/RioJoy.Core/Profiles/RioProfile.cs | 8 + .../ProfileWallpaperGenerator.cs | 56 + src/RioJoy.Overlay/RioJoy.Overlay.csproj | 21 + src/RioJoy.Overlay/SkiaOverlayRenderer.cs | 132 ++ src/RioJoy.Overlay/SkiaTextMeasurer.cs | 35 + src/RioJoy.Tray/Editor/ProfileEditorForm.cs | 255 ++++ src/RioJoy.Tray/Editor/SheetCanvas.cs | 57 + src/RioJoy.Tray/Editor/SheetView.cs | 83 ++ src/RioJoy.Tray/RioCoordinator.cs | 43 + src/RioJoy.Tray/RioJoy.Tray.csproj | 1 + src/RioJoy.Tray/TrayApplicationContext.cs | 46 + src/RioJoy.Tray/WallpaperApplier.cs | 42 + .../Editing/SheetLayoutTests.cs | 78 ++ .../Mapping/ButtonBindingTests.cs | 84 ++ .../Mapping/KeyCatalogTests.cs | 42 + .../Overlay/CockpitRegionsTests.cs | 68 + .../Overlay/FontFitterTests.cs | 75 + .../Overlay/GoobieDataImporterTests.cs | 57 + .../Overlay/LinearTextMeasurer.cs | 23 + .../Overlay/OverlayLayoutEngineTests.cs | 143 ++ .../Overlay/OverlayRenderIntegrationTests.cs | 50 + .../Overlay/OverlayTemplateStoreTests.cs | 53 + .../Overlay/ProfileWallpaperGeneratorTests.cs | 46 + .../Overlay/SkiaOverlayRendererTests.cs | 82 ++ tests/RioJoy.Core.Tests/Overlay/TestRepo.cs | 16 + .../RioJoy.Core.Tests.csproj | 1 + tools/XcfRegionExtract/Program.cs | 133 ++ tools/XcfRegionExtract/README.md | 31 + .../XcfRegionExtract/XcfRegionExtract.csproj | 10 + 47 files changed, 3866 insertions(+), 21 deletions(-) create mode 100644 docs/reference/customBackground/config-sheet.csv create mode 100644 docs/reference/customBackground/regions.json create mode 100644 docs/reference/customBackground/riojoy.png create mode 100644 src/RioJoy.Core/Editing/SheetLayout.cs create mode 100644 src/RioJoy.Core/Mapping/ButtonBinding.cs create mode 100644 src/RioJoy.Core/Mapping/KeyCatalog.cs create mode 100644 src/RioJoy.Core/Overlay/FontFitter.cs create mode 100644 src/RioJoy.Core/Overlay/GoobieDataImporter.cs create mode 100644 src/RioJoy.Core/Overlay/ITextMeasurer.cs create mode 100644 src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs create mode 100644 src/RioJoy.Core/Overlay/OverlayModels.cs create mode 100644 src/RioJoy.Core/Overlay/OverlayTemplateStore.cs create mode 100644 src/RioJoy.Overlay/ProfileWallpaperGenerator.cs create mode 100644 src/RioJoy.Overlay/RioJoy.Overlay.csproj create mode 100644 src/RioJoy.Overlay/SkiaOverlayRenderer.cs create mode 100644 src/RioJoy.Overlay/SkiaTextMeasurer.cs create mode 100644 src/RioJoy.Tray/Editor/ProfileEditorForm.cs create mode 100644 src/RioJoy.Tray/Editor/SheetCanvas.cs create mode 100644 src/RioJoy.Tray/Editor/SheetView.cs create mode 100644 src/RioJoy.Tray/WallpaperApplier.cs create mode 100644 tests/RioJoy.Core.Tests/Editing/SheetLayoutTests.cs create mode 100644 tests/RioJoy.Core.Tests/Mapping/ButtonBindingTests.cs create mode 100644 tests/RioJoy.Core.Tests/Mapping/KeyCatalogTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/CockpitRegionsTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/FontFitterTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/GoobieDataImporterTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/LinearTextMeasurer.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/OverlayLayoutEngineTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/OverlayRenderIntegrationTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/OverlayTemplateStoreTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/ProfileWallpaperGeneratorTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs create mode 100644 tests/RioJoy.Core.Tests/Overlay/TestRepo.cs create mode 100644 tools/XcfRegionExtract/Program.cs create mode 100644 tools/XcfRegionExtract/README.md create mode 100644 tools/XcfRegionExtract/XcfRegionExtract.csproj diff --git a/.gitignore b/.gitignore index 8deaa88..2fa9152 100644 --- a/.gitignore +++ b/.gitignore @@ -372,3 +372,7 @@ FodyWeavers.xsd *.pvk # Local run-time config that shouldn't be versioned *.local.json + +# Generated overlay/editor previews (regenerable, not committed) +docs/reference/customBackground/riojoy-preview.png +docs/reference/customBackground/editor-preview.png diff --git a/README.md b/README.md index 9000198..3cb9fad 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Red Planet — talk to the RIO directly and do not use this app.) | [`src/RioJoy.Tray`](src/RioJoy.Tray/) | Background tray application | | [`tests/RioJoy.Core.Tests`](tests/RioJoy.Core.Tests/) | xUnit tests for the protocol core | | [`driver/`](driver/) | `RioGamepad` virtual HID driver (KMDF + VHF) — replaces vJoy | +| [`tools/RioJoySmokeTest`](tools/RioJoySmokeTest/) | On-cabinet end-to-end check of the feeder → driver path | +| [`tools/XcfRegionExtract`](tools/XcfRegionExtract/) | Extracts cockpit label regions from `riojoy.xcf` → `regions.json` | | [`docs/PLAN.md`](docs/PLAN.md) | Full modernization plan (7 phases) | | [`docs/PROTOCOL.md`](docs/PROTOCOL.md) | RIO wire format + `iRIO` input-map reference | | [`docs/reference/`](docs/reference/) | Cockpit overlay art & the legacy labeling pipeline | @@ -35,11 +37,14 @@ dotnet test RioJoy.sln ## Status -Phases 1–5 are implemented and tested (136 unit tests): the `RioGamepad` virtual -HID driver compiles to `.sys` against the EWDK (KMDF + VHF), and the C# side -covers the serial + RIO protocol core, input mapping + output routing, axis -calibration + plasma display, the tray app + profiles (JSON config, `RIO.ini` -importer, three-state auto-switch), and the HID report packer that matches the -driver's wire format. Remaining work is **deploy-side**: test-sign + install the -driver, wire the real `DeviceIoControl` feeder, and verify on a cabinet. See -[`docs/PLAN.md`](docs/PLAN.md) for the full roadmap. +Phases 1–5 are implemented and tested (136 unit tests). The `RioGamepad` virtual +HID driver is built (KMDF + VHF), **test-signed, installed, and verified**: it +enumerates in `joy.cpl`, and the C# HID feeder (`DeviceIoControl` → +`RioGamepad.sys`) drives its axes, buttons, and hat end-to-end (see +[`tools/RioJoySmokeTest`](tools/RioJoySmokeTest/)). The C# side covers the serial ++ RIO protocol core, input mapping + output routing, axis calibration + plasma +display, the tray app + profiles (JSON config, `RIO.ini` importer, three-state +auto-switch), and the HID report packer that matches the driver's wire format. +Remaining work is **on-cabinet** (real RIO serial/axis/plasma/auto-switch +verification) plus packaging (Phase 6) and the profile editor + overlay +generator (Phase 7). See [`docs/PLAN.md`](docs/PLAN.md) for the full roadmap. diff --git a/RioJoy.sln b/RioJoy.sln index d109dc4..334ba6c 100644 --- a/RioJoy.sln +++ b/RioJoy.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9FFCA6FA EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RioJoy.Core.Tests", "tests\RioJoy.Core.Tests\RioJoy.Core.Tests.csproj", "{4A950510-432A-48C2-92E5-B87D075BA7DB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RioJoy.Overlay", "src\RioJoy.Overlay\RioJoy.Overlay.csproj", "{1A532110-DD43-40FB-87C9-ECFF014D5799}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,10 +36,15 @@ Global {4A950510-432A-48C2-92E5-B87D075BA7DB}.Debug|Any CPU.Build.0 = Debug|x64 {4A950510-432A-48C2-92E5-B87D075BA7DB}.Release|Any CPU.ActiveCfg = Release|x64 {4A950510-432A-48C2-92E5-B87D075BA7DB}.Release|Any CPU.Build.0 = Release|x64 + {1A532110-DD43-40FB-87C9-ECFF014D5799}.Debug|Any CPU.ActiveCfg = Debug|x64 + {1A532110-DD43-40FB-87C9-ECFF014D5799}.Debug|Any CPU.Build.0 = Debug|x64 + {1A532110-DD43-40FB-87C9-ECFF014D5799}.Release|Any CPU.ActiveCfg = Release|x64 + {1A532110-DD43-40FB-87C9-ECFF014D5799}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(NestedProjects) = preSolution {C81FEF57-A33B-4529-BDB7-02787407A545} = {CDDAA3FE-6A05-40A0-85CC-4DB0B3EEEA9C} {FA2BAEAC-D8D5-47D9-B5EC-C9D10530D351} = {CDDAA3FE-6A05-40A0-85CC-4DB0B3EEEA9C} {4A950510-432A-48C2-92E5-B87D075BA7DB} = {9FFCA6FA-1A95-4492-9A18-39D4F5720519} + {1A532110-DD43-40FB-87C9-ECFF014D5799} = {CDDAA3FE-6A05-40A0-85CC-4DB0B3EEEA9C} EndGlobalSection EndGlobal diff --git a/docs/PLAN.md b/docs/PLAN.md index 6033d76..d9c5809 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -100,7 +100,7 @@ tray menu is always available. ## Phases ### Phase 0 — Repo & scaffold ✅ (this commit) -- `git init`, remote `https://gitea.mysticmachines.com/VWE/riovjoy2.git`. +- `git init`, remote `https://gitea.mysticmachines.com/VWE/RIOJoy.git`. - Legacy C++ moved to `legacy/`; cockpit art to `docs/reference/`. - Solution `RioJoy.sln` with `src/RioJoy.Core` (lib) + `src/RioJoy.Tray` (tray app); `driver/` placeholder for the WDK project. @@ -183,7 +183,7 @@ Implemented in `src/RioJoy.Core/Calibration` + `Plasma` (105 xUnit tests total): ### Phase 5 — Tray app + profiles — code-complete ✅ Core logic in `src/RioJoy.Core/Profiles` + `RioRuntime`; UI/OS in `src/RioJoy.Tray` -(123 xUnit tests total): +(136 xUnit tests total across the suite): - `RioProfile` + `AppConfig` model; `ConfigStore` JSON persistence (round-trip tested); `RioIniImporter` ports the legacy `RIO.ini` (buttons/inverts/greeting). - `AutoSwitchResolver` + `AutoSwitchWatcher`: the three-state decision @@ -205,20 +205,54 @@ Core logic in `src/RioJoy.Core/Profiles` + `RioRuntime`; UI/OS in `src/RioJoy.Tr acquire/release lifecycle against real RIO hardware. ### Phase 6 — Packaging / signing / deploy -- Driver install via `pnputil`; app installer; test-signing script. -- Cabinet deployment doc. (Production option: attestation signing.) +- Driver test-signing + `pnputil` install already scripted (`driver/sign.ps1`, + `driver/install.ps1`, `driver/uninstall.ps1`, [`driver/README.md`](../driver/README.md)); + proven on the cabinet. ⏳ Still to do: app installer, a single bundled deploy, + and a cabinet deployment doc. (Production option: attestation signing.) -### Phase 7 — Profile/mapping editor + cockpit overlay generator +### Phase 7 — Profile/mapping editor + cockpit overlay generator — in progress Replaces the legacy Google-Sheet → `.data` → GIMP → Script-Fu pipeline (see [`docs/reference/customBackground/`](reference/customBackground/)). -- **Mapping editor:** per-button UI to set action + label + lamp, no hex - bit-twiddling; clone-from-existing profile. -- **Overlay generator:** render labels into named regions over a base cockpit - image using **SkiaSharp**, porting the auto-fit/justification logic from - `sg-goobie-MFD.scm`; export the per-profile wallpaper and re-apply via - `SystemParametersInfo`. -- **Region authoring:** extract the label rectangles from `riojoy.xcf` into a - `regions.json`; in-app box editor for future tweaks. +- **Overlay generator — done ✅, verified on real assets.** `RioJoy.Core/Overlay` + is the pure, unit-tested layout engine: `FontFitter` is a faithful port of the + `calc-fontsize` auto-fit search (validated against a brute-force oracle), + `OverlayLayoutEngine` ports `create-data-layer`'s fit + horizontal/vertical + justification, and `OverlayTemplate`/`OverlayRegion` (a `regions.json` via + `OverlayTemplateStore`) hold the cell geometry/color. Label text is per-profile + (`RioProfile.OverlayLabels`); `GoobieDataImporter` reads the legacy `.data` + sheet into label rows. The rasterizer lives in `src/RioJoy.Overlay` + (**SkiaSharp**): `SkiaTextMeasurer` (shared with the engine so measured layout + == drawn output) + `SkiaOverlayRenderer` (draw labels → PNG). The full chain is + exercised end-to-end on the real cockpit art (`regions.json` + `riojoy.png` + + `TEST.data`) by `OverlayRenderIntegrationTests`. `RioJoy.Tray/WallpaperApplier` + applies the result via `SystemParametersInfo`. +- **Region authoring — extraction done ✅.** `tools/XcfRegionExtract` parses the + GIMP source (`riojoy.xcf`) and writes the 119-cell + `docs/reference/customBackground/regions.json` (per-layer offsets/size/font/color, + `BaseImagePath` → exported `riojoy.png`), anchored by `CockpitRegionsTests`. + ⏳ Still to do: an in-app box editor for tweaks. +- **Runtime wiring — done ✅ (opt-in).** `RioJoy.Overlay/ProfileWallpaperGenerator` + renders a profile's labels onto the template base image; `RioCoordinator` + generates + applies the wallpaper on profile activation when + `AppConfig.OverlayTemplatePath` is set (best-effort, off by default, never breaks + activation). The live `SystemParametersInfo` apply changes a user setting, so it + is gated behind config and not exercised by tests. ⏳ Optional: restore the prior + wallpaper when going dormant. +- **Mapping editor — first cut done ✅.** `RioJoy.Tray/Editor/ProfileEditorForm` + shows the cockpit as a scrollable, clickable grid that mirrors the legacy + authoring spreadsheet's **logical** layout — parsed from the sheet's CSV export + (`docs/reference/customBackground/config-sheet.csv`) by `SheetLayout`, coloured + by section. (The layout deliberately follows the sheet, **not** the wallpaper + region positions: the wallpaper drives 6 displays via VGA chroma-channel + splitting, so its label positions stack and don't reflect the physical cockpit.) + Clicking an address cell edits its label and action/modifiers/lamp; the `iRIO` + word is read/written through `ButtonBinding` ↔ `RioMapEntry.Create` (round-trip + tested) — no hex bit-twiddling. The value field is a **context-sensitive picker**: + keyboard actions pick a key by name (`KeyCatalog` VK names), joystick a Button N, + hat a direction, mouse/RIO-command the enum name; modifiers enable only for + keyboard. Opened from the tray ("Edit profile…"); Save persists the profile. + ⏳ Still to refine: grouping a button's two bank addresses (edit once → both), + and clone-from-existing. - The unified profile JSON supersedes both `RIO.ini` and the Google Sheet, with importers for each. - To confirm at Phase 7: target wallpaper resolution(s); static wallpaper vs. diff --git a/docs/reference/customBackground/config-sheet.csv b/docs/reference/customBackground/config-sheet.csv new file mode 100644 index 0000000..0496ee2 --- /dev/null +++ b/docs/reference/customBackground/config-sheet.csv @@ -0,0 +1,49 @@ +"","","","","IMAGE NAME","","","","defalt","","","","Plasma greeting","","","","Hello,","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","","S","C","A","KEY","S","","A","KEY","S","C","A","KEY","S","C","A","KEY","","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","" +"","","","","G_KEY","","","","H_KEY","","","","I_KEY","","","","J_KEY","","","","","K_KEY","","","","L_KEY","","","","M_KEY","","","","N_KEY","","","","","O_KEY","","","","P_KEY","","","","Q_KEY","","","","R_KEY","" +"","","LABEL","","G","","LABEL","","H","","LABEL","","I","","LABEL","","J","","","LABEL","","K","","","","L","","LABEL","","M","","LABEL","","N","","","LABEL","","O","","LABEL","","P","","LABEL","","Q","","LABEL","","R","" +"","IsLit","","","2F","IsLit","","","2E","IsLit","","","2D","IsLit","","","2C","","IsLit","","","27","IsLit","","","26","IsLit","","","25","IsLit","","","24","","IsLit","","","37","IsLit","","","36","IsLit","","","35","IsLit","","","34","" +"","x","","","","x","","","","x","","","","x","","","","","x","","","","x","","","","x","","","","x","","","","","x","","","","x","","","","x","","","","x","","","","" +"","IsLit","","","2B","IsLit","","","2A","IsLit","","","29","IsLit","","","28","","IsLit","","","23","IsLit","","","22","IsLit","","","21","IsLit","","","20","","IsLit","","","33","IsLit","","","32","IsLit","","","31","IsLit","","","30","" +"","x","","","","x","","","","x","","","","x","","","","","x","","","","x","","","","x","","","","x","","","","","x","","","","x","","","","x","","","","x","","","","" +"","","LABEL","","S","","LABEL","","T","","LABEL","","U","","LABEL","","V","","","LABEL","","<","","","","^","","LABEL","","v","","LABEL","",">","","","LABEL","","W","","LABEL","","X","","LABEL","","Y","","LABEL","","Z","" +"","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","","S","C","A","KEY","S","","A","KEY","S","C","A","KEY","S","C","A","KEY","","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","" +"","","","","S_KEY","","","","T_KEY","","","","U_KEY","","","","V_KEY","","","","","LEFT","","","","UP","","","","DOWN","","","","RIGHT","","","","","W_KEY","","","","X_KEY","","","","Y_KEY","","","","Z_KEY","" +"","","","","KEYPAD","","","","LABELS","","","","JOYSTICK AIS","INVERT","","","","","S","C","A","KEY","KEY PAD","","","","BUTTONS","","","","S","C","A","KEY","","S","C","A","KEY","IsLit","","","THROTTLE BOARD","S","C","A","KEY","IsLit","","","JOYSTICK BOARD","" +"","","0","","0","","8","","8","","","","X","","","","","","","","","0_KEY","50","","","","58","","","","","","","8_KEY","","","","","MouseU","","","","38","","","","J1","","","","40","" +"","","1","","1","","9","","9","","","","Y","","","","","","","","","1_KEY","51","","","","59","","","","","","","9_KEY","","","","","MouseR","","","","39","","","","HatD","","","","41","" +"","","2","","2","","A","","A","","","","Z","","","","","","","","","2_KEY","52","","","","5A","","","","","","","A_KEY","","","","","MouseD","","","","3A","","","","HatU","","","","42","" +"","","3","","3","","B","","B","","","","R (L PEDDLE)","","","","","","","","","3_KEY","53","","","","5B","","","","","","","B_KEY","","","","","MouseL","","","","3B","","","","HatR","","","","43","" +"","","4","","4","","C","","C","","","","YR (R PEDDLE)","","","","","","","","","4_KEY","54","","","","5C","","","","","","","C_KEY","","","","","MouseLC","","","","3C","","","","HatL","","","","44","" +"","","5","","5","","D","","D","","","","ZR (RUDDER)","","","","","","","","","5_KEY","55","","","","5D","","","","","","","D_KEY","","","","","ESCAPE","x","","","3D","","","","J4","","","","45","" +"","","6","","6","","E","","E","","","","ZR (DISSABLE)","","","","","","","","","6_KEY","56","","","","5E","","","","","","","E_KEY","","","","","MouseRC","","","","3E","","","","J3","","","","46","" +"","","7","","7","","F","","F","","","","","","","","","","","","","7_KEY","57","","","","5F","","","","","","","F_KEY","","","","","J8","","","","3F","","","","J2","","","","47","" +"","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","","S","C","A","KEY","IsLit","","","SECONDARY","IsLit","","","SCREEN","S","C","A","KEY","","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","" +"","","","","HOME","","","","COMMA","","","","PERIOD","","","","END","","","","","MouseU","X","","","10","X","","","18","","","","RETURN","","X","","","LEFT","X","","","RIGHT","X","","","DOWN","X","x","x","UP","" +"","","LABEL","","TOP CONTACT","","LABEL","","UP CONTACT","","LABEL","","DOWN CON","","LABEL","","LAST CONTACT","","","","","MouseR","X","","","11","X","","","19","","","","MENU(ALT)","","","LABEL","","OFFENCE","","LABEL","","DEFENCE","","LABEL","","DRIVE","","LABEL","","BALANCE","" +"","IsLit","","","0F","IsLit","","","0E","IsLit","","","0D","IsLit","","","0C","","","","","MouseD","X","","","12","X","","","1A","","","","CONTROL","","IsLit","","","07","IsLit","","","06","IsLit","","","05","IsLit","","","04","" +"","X","","","","X","","","","X","","","","X","","","","","","","","MouseL","X","","","13","X","","","1B","","","","SHIFT","","x","","","","x","","","","x","","","","x","","","","" +"","IsLit","","","0B","IsLit","","","0A","IsLit","","","09","IsLit","","","08","","","","","MouseLC","X","","","14","X","","","1C","","","","VOLUME_UP","","IsLit","","","03","IsLit","","","02","IsLit","","","01","IsLit","","","00","" +"","X","","","","X","","","","X","","","","X","","","","","","","","TAB","X","","","15","X","","","1D","","","","VOLUME_DOWN","","x","","","","x","","","","x","","","","x","","","","" +"","","LABEL","","NEAR ENEMY","","LABEL","","NEXT ENEMY","","LABEL","","SUB TARGET","","LABEL","","LAST AGGESSOR","","","","","NULL","","","","16","","","","1E","","","","NULL","","","LABEL","","LDS","","LABEL","","UNDOCK","","LABEL","","ZOOM","","LABEL","","LSDI","" +"","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","","","","","NULL","","","","17","","","","1F","","","","NULL","","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","S","C","A","KEY","" +"","","","","R_KEY","","","","E_KEY","","","","Y_KEY","","","","Q_KEY","","","","","","","","","SECONDARY","","","","LABELS","","","","","","","","","L_KEY","","","","U_KEY","","","","Z_KEY","","","","I_KEY","" +"","","","","","","","","","","","","","","","","","","","","","","","10","","M U","","18","","ENTER","","","","","","","","","THROTTLE LABELS","","","","","","","","JOYSTICK LABELS","","","","","" +"","S","C","A","KEY","EXTERNAL KEY PAD","","","","BUTTONS","","","","S","C","A","KEY","","","","","","","11","","M R","","19","","ALT","","","","","","","38","","M ^","","","","","","40","","FIRE/ACCEPT","","","","","" +"","","","","RIO_AnalogAllReset","60","","","","68","","","","","","","RIO_AxesReadout","","","","","","","12","","M D","","1A","","CTRL","","","","","","","39","","M >","","","","","","41","","H v","","","","","" +"","","","","RIO_ResetThrottle","61","","","","69","","","","","","","RIO_FrequencyReadout","","","","","","","13","","M L","","1B","","SHIFT","","","","","","","3A","","M v","","","","","","42","","H ^","","","","","" +"","","","","RIO_ResetLeftPedal","62","","","","6A","","","","","","","RIO_more1","","","","","","","14","","MLC","","1C","","VL U","","","","","","","3B","","M <","","","","","","43","","H >","","","","","" +"","","","","RIO_ResetRightPedal","63","","","","6B","","","","","","","RIO_more2","","","","","","","15","","TAB","","1D","","VL D","","","","","","","3C","","ML","","","","","","44","","H <","","","","","" +"","","","","RIO_ResetVerticalJoystick","64","","","","6C","","","","","","","RIO_more3","","","","","","","16","","","","1E","","","","","","","","","3D","","ESC","","","","","","45","","NEXT PRIMEARY","","","","","" +"","","","","RIO_ResetHorizontalJoystick","65","","","","6D","","","","","","","RIO_more4","","","","","","","17","","","","1F","","","","","","","","","3E","","MR","","","","","","46","","NEXT SECOND","","","","","" +"","","","","RIO_DigitalReset","66","","","","6E","","","","","","","RIO_more5","","","","","","","","","","","","","","","","","","","","3F","","REVERSE","","","","","","47","","TARGET/BACK","","","","","" +"","","","","RIO_StatusCheck","67","","","","6F","","","","","","","RIO_more6","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","","","EXT KEYPAD","","","","LABLES","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","0","","RIO_0","","8","","RIO_8","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","1","","RIO_1","","9","","RIO_9","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","2","","RIO_2","","A","","RIO_A","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","3","","RIO_3","","B","","RIO_B","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","4","","RIO_4","","C","","RIO_C","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","5","","RIO_5","","D","","RIO_D","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","6","","RIO_6","","E","","RIO_E","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"","","","","","","7","","RIO_7","","F","","RIO_F","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" \ No newline at end of file diff --git a/docs/reference/customBackground/regions.json b/docs/reference/customBackground/regions.json new file mode 100644 index 0000000..7a1faa3 --- /dev/null +++ b/docs/reference/customBackground/regions.json @@ -0,0 +1,1214 @@ +{ + "BaseImagePath": "riojoy.png", + "Width": 2720, + "Height": 600, + "Regions": [ + { + "Name": "a-00", + "X": 810, + "Y": 332, + "Width": 197, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-00", + "X": 2558, + "Y": 428, + "Width": 153, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-01", + "X": 2398, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-02", + "X": 2238, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-03", + "X": 2078, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-04", + "X": 2558, + "Y": 5, + "Width": 153, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-05", + "X": 2399, + "Y": 5, + "Width": 149, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-06", + "X": 2238, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-07", + "X": 2078, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-08", + "X": 2558, + "Y": 428, + "Width": 153, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-09", + "X": 2398, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-0A", + "X": 2238, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-0B", + "X": 2078, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-0C", + "X": 2558, + "Y": 5, + "Width": 153, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-0D", + "X": 2398, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-0E", + "X": 2238, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-0F", + "X": 2078, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-10", + "X": 823, + "Y": 428, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-11", + "X": 926, + "Y": 428, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-12", + "X": 1035, + "Y": 428, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-13", + "X": 1143, + "Y": 428, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-14", + "X": 1247, + "Y": 428, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-15", + "X": 1354, + "Y": 428, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-16", + "X": 1329, + "Y": 370, + "Width": 50, + "Height": 50, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-17", + "X": 1382, + "Y": 370, + "Width": 50, + "Height": 50, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-18", + "X": 820, + "Y": 5, + "Width": 54, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-19", + "X": 927, + "Y": 6, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-1A", + "X": 1035, + "Y": 5, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-1B", + "X": 1140, + "Y": 5, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-1C", + "X": 1246, + "Y": 5, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-1D", + "X": 1354, + "Y": 6, + "Width": 53, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 38, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-1E", + "X": 1330, + "Y": 67, + "Width": 50, + "Height": 50, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-1F", + "X": 1388, + "Y": 67, + "Width": 50, + "Height": 50, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FFFFFF", + "RotationDegrees": 90 + }, + { + "Name": "b-20", + "X": 1918, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-21", + "X": 1758, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-22", + "X": 1598, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-23", + "X": 1438, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-24", + "X": 1919, + "Y": 6, + "Width": 150, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-25", + "X": 1759, + "Y": 6, + "Width": 149, + "Height": 52, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-26", + "X": 1599, + "Y": 6, + "Width": 149, + "Height": 52, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-27", + "X": 1438, + "Y": 5, + "Width": 150, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#00FF00" + }, + { + "Name": "b-28", + "X": 1918, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-29", + "X": 1758, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-2A", + "X": 1598, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-2B", + "X": 1438, + "Y": 428, + "Width": 150, + "Height": 52, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-2C", + "X": 1918, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-2D", + "X": 1758, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-2E", + "X": 1598, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-2F", + "X": 1438, + "Y": 6, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#FF0000" + }, + { + "Name": "b-30", + "X": 1918, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-31", + "X": 1758, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-32", + "X": 1598, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-33", + "X": 1438, + "Y": 428, + "Width": 151, + "Height": 53, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-34", + "X": 1918, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-35", + "X": 1758, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-36", + "X": 1598, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-37", + "X": 1438, + "Y": 5, + "Width": 151, + "Height": 54, + "FontFamily": "Sans", + "FontSizePx": 45, + "ColorHex": "#0000FF" + }, + { + "Name": "b-38", + "X": 2349, + "Y": 105, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-39", + "X": 2349, + "Y": 139, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-3A", + "X": 2349, + "Y": 173, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-3B", + "X": 2349, + "Y": 207, + "Width": 50, + "Height": 32, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-3C", + "X": 2349, + "Y": 242, + "Width": 50, + "Height": 32, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-3D", + "X": 2349, + "Y": 277, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-3E", + "X": 2349, + "Y": 311, + "Width": 50, + "Height": 32, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-3F", + "X": 2349, + "Y": 346, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-40", + "X": 2509, + "Y": 105, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-41", + "X": 2509, + "Y": 277, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-42", + "X": 2509, + "Y": 242, + "Width": 50, + "Height": 32, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-43", + "X": 2509, + "Y": 346, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-44", + "X": 2509, + "Y": 311, + "Width": 50, + "Height": 32, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-45", + "X": 2509, + "Y": 207, + "Width": 50, + "Height": 32, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-46", + "X": 2509, + "Y": 173, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-47", + "X": 2509, + "Y": 139, + "Width": 50, + "Height": 31, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "b-50", + "X": 1682, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-51", + "X": 1602, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-52", + "X": 1682, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-53", + "X": 1762, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-54", + "X": 1602, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-55", + "X": 1682, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-56", + "X": 1762, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-57", + "X": 1602, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-58", + "X": 1682, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-59", + "X": 1762, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-5A", + "X": 1602, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-5B", + "X": 1762, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-5C", + "X": 1842, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-5D", + "X": 1842, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-5E", + "X": 1842, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-5F", + "X": 1842, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#00FF00" + }, + { + "Name": "b-60", + "X": 1682, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-61", + "X": 1602, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-62", + "X": 1682, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-63", + "X": 1762, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-64", + "X": 1602, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-65", + "X": 1682, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-66", + "X": 1762, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-67", + "X": 1602, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-68", + "X": 1682, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-69", + "X": 1762, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-6A", + "X": 1602, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-6B", + "X": 1762, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-6C", + "X": 1842, + "Y": 105, + "Width": 77, + "Height": 65, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-6D", + "X": 1842, + "Y": 173, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-6E", + "X": 1842, + "Y": 242, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "b-6F", + "X": 1842, + "Y": 311, + "Width": 77, + "Height": 66, + "FontFamily": "Sans", + "FontSizePx": 40, + "ColorHex": "#FF0000" + }, + { + "Name": "KeyPad:INT", + "X": 1695, + "Y": 79, + "Width": 132, + "Height": 21, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "KeyPad:EXT", + "X": 1695, + "Y": 79, + "Width": 132, + "Height": 21, + "FontFamily": "Sans", + "FontSizePx": 20, + "ColorHex": "#00FF00" + }, + { + "Name": "HatRight", + "X": 2402, + "Y": 350, + "Width": 104, + "Height": 27, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "HatLeft", + "X": 2402, + "Y": 315, + "Width": 104, + "Height": 28, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "HatDown", + "X": 2402, + "Y": 282, + "Width": 104, + "Height": 26, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "HatUp", + "X": 2402, + "Y": 247, + "Width": 104, + "Height": 27, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Lower", + "X": 2402, + "Y": 212, + "Width": 104, + "Height": 27, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Middle", + "X": 2402, + "Y": 178, + "Width": 104, + "Height": 26, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Upper", + "X": 2402, + "Y": 144, + "Width": 104, + "Height": 26, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Throttle", + "X": 2242, + "Y": 351, + "Width": 104, + "Height": 26, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Panic", + "X": 2242, + "Y": 282, + "Width": 104, + "Height": 26, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Trigger", + "X": 2402, + "Y": 110, + "Width": 104, + "Height": 26, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "Throttle:", + "X": 2243, + "Y": 81, + "Width": 103, + "Height": 20, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + }, + { + "Name": "JoyStick:", + "X": 2402, + "Y": 80, + "Width": 104, + "Height": 21, + "FontFamily": "Sans", + "FontSizePx": 18, + "ColorHex": "#00FF00" + } + ] +} \ No newline at end of file diff --git a/docs/reference/customBackground/riojoy.png b/docs/reference/customBackground/riojoy.png new file mode 100644 index 0000000000000000000000000000000000000000..d5510743182e2c77b3f34efb95ff6c967b5adc14 GIT binary patch literal 139267 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

JSWK}K>5AwKn2L10cvCJFeahHK9)(i{#fsw=Tr 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%0Axqwo&+{`@mgPDbZ3+!k5p03a)6UI~eE6 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

HGy& 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 literal 0 HcmV?d00001 diff --git a/src/RioJoy.Core/Editing/SheetLayout.cs b/src/RioJoy.Core/Editing/SheetLayout.cs new file mode 100644 index 0000000..1f70f16 --- /dev/null +++ b/src/RioJoy.Core/Editing/SheetLayout.cs @@ -0,0 +1,120 @@ +using System.Globalization; +using System.Text; + +namespace RioJoy.Core.Editing; + +///

Section of the authoring sheet a cell belongs to (drives editor colour). +public enum SheetSection +{ + None, + Contacts, // 0x00–0x0F + Secondary, // 0x10–0x17 + Screen, // 0x18–0x1F + Letters, // 0x20–0x37 + Throttle, // 0x38–0x3F + Joystick, // 0x40–0x47 + Keypad, // 0x50–0x5F + ExtKeypad, // 0x60–0x6F +} + +/// +/// One cell of the cockpit authoring spreadsheet: its grid /, +/// the cell , and — when the text is a RIO address (two hex +/// digits, 0x00–0x6F) — the it edits and its +/// . +/// +public sealed record SheetCell(int Row, int Col, string Text, int? Address, SheetSection Section); + +/// +/// Parses the legacy authoring spreadsheet (exported CSV, see +/// docs/reference/customBackground/config-sheet.csv) into a grid of +/// s. This is the cockpit's logical layout — the +/// shape the editor mirrors — as opposed to the wallpaper's pixel positions, which +/// are a VGA chroma-split display artifact. Pure, so it is unit-tested. +/// +public static class SheetLayout +{ + public static IReadOnlyList Load(string path) => Parse(File.ReadAllText(path)); + + /// + /// The RIO address a cell's text encodes (two hex digits in 0x00–0x6F), or null. + /// + public static int? AddressFromText(string text) + { + string t = text.Trim(); + if (t.Length != 2) return null; + if (!int.TryParse(t, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int a)) return null; + return a is >= 0 and <= 0x6F ? a : null; + } + + /// Classify an address into a sheet section. + public static SheetSection SectionForAddress(int address) => address switch + { + >= 0x00 and <= 0x0F => SheetSection.Contacts, + >= 0x10 and <= 0x17 => SheetSection.Secondary, + >= 0x18 and <= 0x1F => SheetSection.Screen, + >= 0x20 and <= 0x37 => SheetSection.Letters, + >= 0x38 and <= 0x3F => SheetSection.Throttle, + >= 0x40 and <= 0x47 => SheetSection.Joystick, + >= 0x50 and <= 0x5F => SheetSection.Keypad, + >= 0x60 and <= 0x6F => SheetSection.ExtKeypad, + _ => SheetSection.None, + }; + + /// + /// Parse the CSV (one spreadsheet row per line) into non-empty cells. Cell text + /// longer than is dropped as a merged-cell + /// artifact; the clean export has none. + /// + public static IReadOnlyList Parse(string csv, int maxTextLength = 40) + { + ArgumentNullException.ThrowIfNull(csv); + + string[] lines = csv.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'); + var cells = new List(); + + for (int row = 0; row < lines.Length; row++) + { + if (lines[row].Length == 0) continue; + List fields = ParseLine(lines[row]); + for (int col = 0; col < fields.Count; col++) + { + string text = fields[col].Trim(); + if (text.Length == 0 || text.Length > maxTextLength) continue; + + int? address = AddressFromText(text); + SheetSection section = address is { } a ? SectionForAddress(a) : SheetSection.None; + cells.Add(new SheetCell(row, col, text, address, section)); + } + } + + return cells; + } + + // Split one CSV line into fields, honouring "quoted" fields and "" escapes. + private static List ParseLine(string line) + { + var fields = new List(); + var sb = new StringBuilder(); + bool inQuotes = false; + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (inQuotes) + { + if (c == '"') + { + if (i + 1 < line.Length && line[i + 1] == '"') { sb.Append('"'); i++; } + else inQuotes = false; + } + else sb.Append(c); + } + else if (c == '"') inQuotes = true; + else if (c == ',') { fields.Add(sb.ToString()); sb.Clear(); } + else sb.Append(c); + } + fields.Add(sb.ToString()); + return fields; + } +} diff --git a/src/RioJoy.Core/Mapping/ButtonBinding.cs b/src/RioJoy.Core/Mapping/ButtonBinding.cs new file mode 100644 index 0000000..d47c638 --- /dev/null +++ b/src/RioJoy.Core/Mapping/ButtonBinding.cs @@ -0,0 +1,56 @@ +namespace RioJoy.Core.Mapping; + +/// +/// A mutable, editor-friendly view of one iRIO map word: the routing +/// , payload , lamp flag, and keyboard +/// modifiers. Two-way binds cleanly to UI controls and converts to/from the packed +/// 16-bit word via / (which round-trip +/// through ). The button's display label is stored +/// separately (the profile's overlay labels), not here. +/// +public sealed class ButtonBinding +{ + /// How the input is routed (keyboard / joystick / hat / mouse / RIO command). + public RioRouteKind Kind { get; set; } = RioRouteKind.Keyboard; + + /// Payload byte: VK code, joystick button #, hat direction, or mouse action. + public byte Value { get; set; } + + /// Lighted button (lamp feedback on press/release). + public bool IsLit { get; set; } + + /// Keyboard modifier: SHIFT. + public bool Shift { get; set; } + + /// Keyboard modifier: CTRL. + public bool Ctrl { get; set; } + + /// Keyboard modifier: ALT. + public bool Alt { get; set; } + + /// Apply KEYEVENTF_EXTENDEDKEY to the key. + public bool Extended { get; set; } + + /// True when this binding does nothing (an empty slot). + public bool IsUnmapped => ToWord() == 0; + + /// Pack to the 16-bit iRIO map word. + public ushort ToWord() => + RioMapEntry.Create(Kind, Value, IsLit, Shift, Ctrl, Alt, Extended).Raw; + + /// Build an editable binding from a packed map word. + public static ButtonBinding FromWord(ushort raw) + { + var e = new RioMapEntry(raw); + return new ButtonBinding + { + Kind = e.Kind, + Value = e.Value, + IsLit = e.HasLamp, + Shift = e.Shift, + Ctrl = e.Ctrl, + Alt = e.Alt, + Extended = e.Extended, + }; + } +} diff --git a/src/RioJoy.Core/Mapping/KeyCatalog.cs b/src/RioJoy.Core/Mapping/KeyCatalog.cs new file mode 100644 index 0000000..c94c2ed --- /dev/null +++ b/src/RioJoy.Core/Mapping/KeyCatalog.cs @@ -0,0 +1,83 @@ +namespace RioJoy.Core.Mapping; + +/// A named keyboard key and its Windows virtual-key code. +public readonly record struct KeyName(string Name, byte Value); + +/// +/// Friendly Windows virtual-key names ↔ VK codes, for the profile editor's key +/// picker so a keyboard action is chosen by name instead of a raw hex byte. The +/// keyboard iRIO payload is a VK code (see / +/// SendInputSink); modifiers are separate flags, so they are not listed here +/// as keys. +/// +public static class KeyCatalog +{ + /// All catalogued keys, in menu order (letters, digits, F-keys, …). + public static IReadOnlyList Entries { get; } = Build(); + + private static readonly Dictionary ByValue = + Entries.GroupBy(e => e.Value).ToDictionary(g => g.Key, g => g.First().Name); + + private static readonly Dictionary ByName = + Entries.ToDictionary(e => e.Name, e => e.Value, StringComparer.OrdinalIgnoreCase); + + /// Friendly name for a VK code, or "0xHH" when uncatalogued. + public static string NameFor(byte vk) => ByValue.TryGetValue(vk, out string? n) ? n : $"0x{vk:X2}"; + + /// Look up a VK code by friendly name. + public static bool TryGetValue(string name, out byte vk) => ByName.TryGetValue(name, out vk); + + private static List Build() + { + var list = new List(); + void Add(string name, int vk) => list.Add(new KeyName(name, (byte)vk)); + + for (char c = 'A'; c <= 'Z'; c++) Add(c.ToString(), c); // 0x41–0x5A + for (int d = 0; d <= 9; d++) Add(d.ToString(), 0x30 + d); // 0x30–0x39 + for (int f = 1; f <= 12; f++) Add($"F{f}", 0x6F + f); // F1–F12 = 0x70–0x7B + + Add("Space", 0x20); + Add("Enter", 0x0D); + Add("Escape", 0x1B); + Add("Tab", 0x09); + Add("Backspace", 0x08); + Add("Delete", 0x2E); + Add("Insert", 0x2D); + Add("Home", 0x24); + Add("End", 0x23); + Add("Page Up", 0x21); + Add("Page Down", 0x22); + + Add("Left", 0x25); + Add("Up", 0x26); + Add("Right", 0x27); + Add("Down", 0x28); + + for (int n = 0; n <= 9; n++) Add($"NumPad {n}", 0x60 + n); // 0x60–0x69 + Add("NumPad *", 0x6A); + Add("NumPad +", 0x6B); + Add("NumPad -", 0x6D); + Add("NumPad .", 0x6E); + Add("NumPad /", 0x6F); + + Add("; :", 0xBA); + Add("= +", 0xBB); + Add(", <", 0xBC); + Add("- _", 0xBD); + Add(". >", 0xBE); + Add("/ ?", 0xBF); + Add("` ~", 0xC0); + Add("[ {", 0xDB); + Add("\\ |", 0xDC); + Add("] }", 0xDD); + Add("' \"", 0xDE); + + Add("Caps Lock", 0x14); + Add("Print Screen", 0x2C); + Add("Scroll Lock", 0x91); + Add("Pause", 0x13); + Add("Num Lock", 0x90); + + return list; + } +} diff --git a/src/RioJoy.Core/Mapping/RioMapEntry.cs b/src/RioJoy.Core/Mapping/RioMapEntry.cs index d62feeb..d9a7043 100644 --- a/src/RioJoy.Core/Mapping/RioMapEntry.cs +++ b/src/RioJoy.Core/Mapping/RioMapEntry.cs @@ -47,6 +47,42 @@ public readonly struct RioMapEntry public RioMapEntry(ushort raw) => Raw = raw; + /// + /// Build a map word from decoded components (the inverse of the decode + /// properties) — used by the profile editor to write the iRIO table. + /// sets the routing flags; modifiers/lamp/extended set + /// their high-byte bits; is the low byte. + /// + public static RioMapEntry Create( + RioRouteKind kind, + byte value, + bool lit = false, + bool shift = false, + bool ctrl = false, + bool alt = false, + bool extended = false) + { + ushort raw = value; + + raw |= kind switch + { + RioRouteKind.Keyboard => 0, + RioRouteKind.Joystick => FlagJoy, + RioRouteKind.Hat => FlagHat, + RioRouteKind.Mouse => FlagMouse, + RioRouteKind.RioCommand => RioCommandMask, + _ => 0, + }; + + if (lit) raw |= FlagHasLamp; + if (extended) raw |= FlagExtended; + if (alt) raw |= FlagAlt; + if (ctrl) raw |= FlagCtrl; + if (shift) raw |= FlagShift; + + return new RioMapEntry(raw); + } + /// This input drives a lighted button (lamp feedback on press/release). public bool HasLamp => (Raw & FlagHasLamp) != 0; diff --git a/src/RioJoy.Core/Overlay/FontFitter.cs b/src/RioJoy.Core/Overlay/FontFitter.cs new file mode 100644 index 0000000..5723ec7 --- /dev/null +++ b/src/RioJoy.Core/Overlay/FontFitter.cs @@ -0,0 +1,64 @@ +namespace RioJoy.Core.Overlay; + +/// +/// Finds the largest font size whose text fits a cell — a faithful port of the +/// legacy calc-fontsize Script-Fu routine +/// (docs/reference/customBackground/sg-goobie-MFD.scm, lines 50–67). +/// +/// The original is an expand-then-narrow search: grow the size by a factor +/// (adjust, starting at 2) while the text fits; on the first overflow, halve +/// the growth factor's excess-over-1 and retry from the last size that fit. It +/// converges when the (truncated) size stops changing or the measured extents +/// repeat, and never returns below . +/// +public static class FontFitter +{ + /// Minimum font size the legacy routine will return (px). + public const double MinFontSize = 6; + + // Guard against a pathological measurer that never converges. + private const int MaxIterations = 1000; + + /// + /// Largest font size (px) at which fits within + /// × , measured by + /// . + /// + public static double BestFitSize( + string text, string fontFamily, double width, double height, ITextMeasurer measurer) + { + ArgumentNullException.ThrowIfNull(measurer); + + // State mirrors the Scheme named-let: (fontsize last-extents last-fontsize adjust). + double fontsize = 6; // minimum possible fontsize (the loop's seed) + double lastFontsize = 3; + double adjust = 2; + TextExtent? lastExtents = null; + + for (int i = 0; i < MaxIterations; i++) + { + TextExtent extents = measurer.Measure(text, fontFamily, fontsize); + + // Converged: size stopped moving, or the extents repeated. + if (lastFontsize == fontsize || (lastExtents is { } prev && prev == extents)) + return Math.Max(fontsize, MinFontSize); + + if (extents.Width > width || extents.Height > height) + { + // Overflow: shrink the growth factor and retry from the last good size. + adjust = (adjust - 1) * 0.5 + 1; + fontsize = Math.Truncate(lastFontsize * adjust); + // lastExtents / lastFontsize unchanged — they hold the last fit. + } + else + { + // Fits: remember it and keep growing by the current factor. + lastExtents = extents; + lastFontsize = fontsize; + fontsize = Math.Truncate(fontsize * adjust); + } + } + + return Math.Max(fontsize, MinFontSize); + } +} diff --git a/src/RioJoy.Core/Overlay/GoobieDataImporter.cs b/src/RioJoy.Core/Overlay/GoobieDataImporter.cs new file mode 100644 index 0000000..fbad471 --- /dev/null +++ b/src/RioJoy.Core/Overlay/GoobieDataImporter.cs @@ -0,0 +1,118 @@ +using System.Text; + +namespace RioJoy.Core.Overlay; + +/// +/// Imports the legacy "Goobie" label data file — the Google-Sheet export consumed +/// by sg-goobie-MFD.scm (see docs/reference/customBackground/TEST.data). The format +/// is Lisp-ish: ; starts a comment to end of line, a parenthesized list of +/// bare field names (matching the template layer/region names) comes first, then +/// one parenthesized list of quoted strings per output image (row). Field 0 is the +/// row's file tag (output name); the rest are the per-region label text. +/// +/// Each row maps directly to an label set +/// (region name → text), so this is the bridge from the old sheet to a profile's +/// . +/// +public static class GoobieDataImporter +{ + /// A parsed data file: the field names and one label map per row. + public sealed record Sheet( + IReadOnlyList Fields, + IReadOnlyList> Rows); + + public static Sheet Load(string path) => Parse(File.ReadAllText(path)); + + /// Parse the data file text into fields + rows. + public static Sheet Parse(string text) + { + ArgumentNullException.ThrowIfNull(text); + + List> lists = ReadLists(text); + if (lists.Count == 0) + throw new FormatException("No lists found; expected a header list of field names."); + + List fields = lists[0]; + var rows = new List>(); + + for (int i = 1; i < lists.Count; i++) + { + List values = lists[i]; + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int f = 0; f < fields.Count && f < values.Count; f++) + map[fields[f]] = values[f]; + rows.Add(map); + } + + return new Sheet(fields, rows); + } + + /// + /// Convenience: the label map for a single row (default the first), with empty + /// values dropped — ready to hand to . + /// + public static IReadOnlyDictionary LabelsForRow(Sheet sheet, int rowIndex = 0) + { + ArgumentNullException.ThrowIfNull(sheet); + if (rowIndex < 0 || rowIndex >= sheet.Rows.Count) + throw new ArgumentOutOfRangeException(nameof(rowIndex)); + + return sheet.Rows[rowIndex] + .Where(kv => !string.IsNullOrEmpty(kv.Value)) + .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); + } + + // Tokenize into parenthesized lists. Barewords and "quoted strings" are both + // returned as their text (quotes stripped); ';' comments run to end of line. + private static List> ReadLists(string s) + { + var lists = new List>(); + List? current = null; + int i = 0; + + while (i < s.Length) + { + char c = s[i]; + + if (c == ';') + { + while (i < s.Length && s[i] != '\n') i++; + } + else if (char.IsWhiteSpace(c)) + { + i++; + } + else if (c == '(') + { + current = new List(); + i++; + } + else if (c == ')') + { + if (current is not null) { lists.Add(current); current = null; } + i++; + } + else if (c == '"') + { + i++; + var sb = new StringBuilder(); + while (i < s.Length && s[i] != '"') + { + if (s[i] == '\\' && i + 1 < s.Length) i++; // unescape \" and \\ + sb.Append(s[i++]); + } + i++; // closing quote + current?.Add(sb.ToString()); + } + else + { + int start = i; + while (i < s.Length && !char.IsWhiteSpace(s[i]) && s[i] is not ('(' or ')' or '"' or ';')) + i++; + current?.Add(s[start..i]); + } + } + + return lists; + } +} diff --git a/src/RioJoy.Core/Overlay/ITextMeasurer.cs b/src/RioJoy.Core/Overlay/ITextMeasurer.cs new file mode 100644 index 0000000..ae25386 --- /dev/null +++ b/src/RioJoy.Core/Overlay/ITextMeasurer.cs @@ -0,0 +1,17 @@ +namespace RioJoy.Core.Overlay; + +/// The pixel size of a rendered string (its bounding box). +public readonly record struct TextExtent(double Width, double Height); + +/// +/// Measures rendered text. Abstracts the rasterizer (SkiaSharp, GDI, …) so the +/// overlay layout math (, ) +/// stays pure and unit-testable against a fake measurer — the same split the rest +/// of the codebase uses for output sinks. The legacy pipeline measured with GIMP's +/// gimp-text-get-extents-fontname (see docs/reference/customBackground/sg-goobie-MFD.scm). +/// +public interface ITextMeasurer +{ + /// Bounding-box size of at the given font/size, in pixels. + TextExtent Measure(string text, string fontFamily, double fontSizePx); +} diff --git a/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs b/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs new file mode 100644 index 0000000..b2949ae --- /dev/null +++ b/src/RioJoy.Core/Overlay/OverlayLayoutEngine.cs @@ -0,0 +1,97 @@ +namespace RioJoy.Core.Overlay; + +/// +/// Lays out per-region label text over a cockpit template into positioned +/// s ready for a rasterizer. Pure: all measurement goes +/// through an , so the placement/auto-fit math is +/// unit-tested without a real font. Ports the per-field logic of +/// create-data-layer (sg-goobie-MFD.scm, lines 69–113): optional auto-fit +/// when the text overflows, then vertical (and here also horizontal) justification. +/// +public sealed class OverlayLayoutEngine +{ + private readonly ITextMeasurer _measurer; + + public OverlayLayoutEngine(ITextMeasurer measurer) => + _measurer = measurer ?? throw new ArgumentNullException(nameof(measurer)); + + /// + /// Lay out every region that has non-empty label text. Regions without an entry + /// in (or with empty text) are skipped — matching the + /// legacy behavior where a blank field produced no visible text. + /// + public IReadOnlyList Layout( + OverlayTemplate template, + IReadOnlyDictionary labels, + OverlayLayoutOptions? options = null) + { + ArgumentNullException.ThrowIfNull(template); + ArgumentNullException.ThrowIfNull(labels); + options ??= new OverlayLayoutOptions(); + + var placed = new List(template.Regions.Count); + foreach (OverlayRegion region in template.Regions) + { + if (labels.TryGetValue(region.Name, out string? text) && !string.IsNullOrEmpty(text)) + placed.Add(Place(region, text, options)); + } + return placed; + } + + /// Lay out a single label within a region. + public PlacedLabel Place(OverlayRegion region, string text, OverlayLayoutOptions? options = null) + { + ArgumentNullException.ThrowIfNull(region); + ArgumentException.ThrowIfNullOrEmpty(text); + options ??= new OverlayLayoutOptions(); + + double rotation = region.RotationDegrees ?? 0; + + // A quarter turn swaps the axes the text is fitted against: a 90°/270° label + // runs its length along the cell's height and its line-height along the width. + bool quarterTurn = (int)Math.Abs(rotation) % 180 == 90; + double fitWidth = quarterTurn ? region.Height : region.Width; + double fitHeight = quarterTurn ? region.Width : region.Height; + + double size = region.FontSizePx; + TextExtent ext = _measurer.Measure(text, region.FontFamily, size); + + // Auto-fit only in Fit mode and only when the base size actually overflows + // (legacy: the calc-fontsize call is guarded by size-handling == 0 && overflow). + if (options.SizeMode == OverlaySizeMode.Fit && (ext.Width > fitWidth || ext.Height > fitHeight)) + { + size = FontFitter.BestFitSize(text, region.FontFamily, fitWidth, fitHeight, _measurer); + ext = _measurer.Measure(text, region.FontFamily, size); + } + + if (rotation != 0) + { + // Center the (unrotated) text box in the cell; the renderer turns it about + // the cell center, so a centered box rotates in place. Justification N/A. + double cxLeft = region.X + (region.Width - ext.Width) / 2; + double cyTop = region.Y + (region.Height - ext.Height) / 2; + return new PlacedLabel(region.Name, text, cxLeft, cyTop, ext.Width, ext.Height, region.FontFamily, size, rotation); + } + + OverlayHJust hjust = region.HJust ?? options.HJust; + OverlayVJust vjust = region.VJust ?? options.VJust; + + // Vertical placement is a direct port of the script; horizontal is the + // generalization of its left-anchored behavior (its header comment intends + // "centered within the bounds", so Center is the engine default). + double x = hjust switch + { + OverlayHJust.Center => region.X + (region.Width - ext.Width) / 2, + OverlayHJust.Right => region.X + (region.Width - ext.Width), + _ => region.X, + }; + double y = vjust switch + { + OverlayVJust.Center => region.Y + (region.Height - ext.Height) / 2, + OverlayVJust.Bottom => region.Y + region.Height - ext.Height, + _ => region.Y, + }; + + return new PlacedLabel(region.Name, text, x, y, ext.Width, ext.Height, region.FontFamily, size); + } +} diff --git a/src/RioJoy.Core/Overlay/OverlayModels.cs b/src/RioJoy.Core/Overlay/OverlayModels.cs new file mode 100644 index 0000000..1790630 --- /dev/null +++ b/src/RioJoy.Core/Overlay/OverlayModels.cs @@ -0,0 +1,115 @@ +namespace RioJoy.Core.Overlay; + +/// Horizontal placement of a label within its region. +public enum OverlayHJust { Left, Center, Right } + +/// Vertical placement of a label within its region (legacy "Vertical justification"). +public enum OverlayVJust { Top, Center, Bottom } + +/// +/// What to do when a label's text does not fit its region at the base font size. +/// Mirrors the legacy script's "Font sizing (if too large)" option +/// (sg-goobie-MFD.scm): shrinks the font to fit (auto-fit), +/// keeps the size and clips to the region, +/// keeps the size and lets the text spill outside the region. +/// +public enum OverlaySizeMode { Fit, Crop, Overflow } + +/// +/// One labelled cell on a cockpit template: the rectangle (and font) a label is +/// laid out within. Equivalent to a named text layer in the legacy GIMP template +/// (layer name = , e.g. a RIO address tag like b-00); the +/// layer's offsets/size/font gave the cell geometry. Geometry is shared by all +/// profiles for a given cockpit image; only the label text is per-profile. +/// +public sealed class OverlayRegion +{ + /// Region key — matches the template layer name and the profile's label key. + public string Name { get; set; } = ""; + + /// Cell left edge, in template pixels. + public double X { get; set; } + + /// Cell top edge, in template pixels. + public double Y { get; set; } + + /// Cell width, in template pixels. + public double Width { get; set; } + + /// Cell height, in template pixels. + public double Height { get; set; } + + /// Font family for this cell's text. + public string FontFamily { get; set; } = "Sans"; + + /// Base font size (px) from the template; the starting point before any auto-fit. + public double FontSizePx { get; set; } = 12; + + /// Text color as #RRGGBB/#AARRGGBB; null = renderer default. + public string? ColorHex { get; set; } + + /// Per-region horizontal justification override; null = use the engine default. + public OverlayHJust? HJust { get; set; } + + /// Per-region vertical justification override; null = use the engine default. + public OverlayVJust? VJust { get; set; } + + /// + /// Counter-clockwise rotation of the label in degrees; null/0 = upright. Used + /// for cockpit cells whose labels read sideways (e.g. the vertically-oriented + /// button column). Rotated labels are centered in the cell (justification N/A). + /// + public double? RotationDegrees { get; set; } +} + +/// +/// A cockpit overlay template: the base image plus the labelled regions, extracted +/// once from the source artwork (docs/reference/customBackground/riojoy.xcf) into a +/// regions.json. Persisted by . +/// +public sealed class OverlayTemplate +{ + /// Path to the base cockpit image the labels are drawn over. + public string? BaseImagePath { get; set; } + + /// Base image width in pixels (the output wallpaper size). + public int Width { get; set; } + + /// Base image height in pixels. + public int Height { get; set; } + + /// The labelled cells. + public List Regions { get; set; } = new(); + + /// Find a region by name (case-insensitive), or null. + public OverlayRegion? FindRegion(string name) => + Regions.FirstOrDefault(r => string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase)); +} + +/// +/// A laid-out label ready to rasterize: the resolved text, top-left position, +/// measured size, and the (possibly auto-fitted) font. The renderer just draws it. +/// +public readonly record struct PlacedLabel( + string RegionName, + string Text, + double X, + double Y, + double Width, + double Height, + string FontFamily, + double FontSizePx, + double RotationDegrees = 0); + +/// Engine-wide defaults for a layout pass; per-region overrides win. +public sealed class OverlayLayoutOptions +{ + /// How to handle text that overflows its region at the base size. + public OverlaySizeMode SizeMode { get; set; } = OverlaySizeMode.Fit; + + /// Default horizontal justification (regions may override). + public OverlayHJust HJust { get; set; } = OverlayHJust.Center; + + /// Default vertical justification (regions may override). + public OverlayVJust VJust { get; set; } = OverlayVJust.Center; +} diff --git a/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs b/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs new file mode 100644 index 0000000..d7ed400 --- /dev/null +++ b/src/RioJoy.Core/Overlay/OverlayTemplateStore.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace RioJoy.Core.Overlay; + +/// +/// Loads and saves an as a regions.json — the +/// cockpit cell geometry extracted from the source artwork +/// (docs/reference/customBackground/riojoy.xcf). Same JSON conventions as +/// (indented, enums as strings, null-skipping). +/// +public static class OverlayTemplateStore +{ + private static readonly JsonSerializerOptions Options = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() }, + }; + + public static string Serialize(OverlayTemplate template) + { + ArgumentNullException.ThrowIfNull(template); + return JsonSerializer.Serialize(template, Options); + } + + public static OverlayTemplate Deserialize(string json) + { + ArgumentException.ThrowIfNullOrWhiteSpace(json); + return JsonSerializer.Deserialize(json, Options) + ?? throw new JsonException("Overlay template JSON deserialized to null."); + } + + public static void Save(OverlayTemplate template, string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + string? dir = Path.GetDirectoryName(Path.GetFullPath(path)); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + File.WriteAllText(path, Serialize(template)); + } + + public static OverlayTemplate Load(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + return Deserialize(File.ReadAllText(path)); + } +} diff --git a/src/RioJoy.Core/Profiles/AppConfig.cs b/src/RioJoy.Core/Profiles/AppConfig.cs index 2c6ec5d..bad2029 100644 --- a/src/RioJoy.Core/Profiles/AppConfig.cs +++ b/src/RioJoy.Core/Profiles/AppConfig.cs @@ -32,6 +32,13 @@ public sealed class AppConfig /// Start RIOJoy with Windows. public bool AutoStart { get; set; } + /// + /// Path to the cockpit overlay template (regions.json) used to generate + /// per-profile wallpapers (Phase 7). Null disables wallpaper generation. The + /// template's base image is resolved relative to this file's folder. + /// + public string? OverlayTemplatePath { get; set; } + /// Find a profile by name (case-insensitive), or null. public RioProfile? FindProfile(string? name) => name is null diff --git a/src/RioJoy.Core/Profiles/RioProfile.cs b/src/RioJoy.Core/Profiles/RioProfile.cs index 728ff10..f2ce1a3 100644 --- a/src/RioJoy.Core/Profiles/RioProfile.cs +++ b/src/RioJoy.Core/Profiles/RioProfile.cs @@ -35,6 +35,14 @@ public sealed class RioProfile /// Cockpit wallpaper image path (generated in Phase 7). public string? WallpaperPath { get; set; } + /// + /// Overlay label text per template region (region/layer name → display text, + /// e.g. "b-00" → "FIRE"). Drives the Phase 7 wallpaper generator + /// (); empty/absent entries render + /// nothing. The region geometry itself lives in the shared overlay template. + /// + public Dictionary OverlayLabels { get; set; } = new(); + /// /// Executable names that select this profile when they are the foreground app /// (matched case-insensitively, with or without ".exe"). diff --git a/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs b/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs new file mode 100644 index 0000000..7491c53 --- /dev/null +++ b/src/RioJoy.Overlay/ProfileWallpaperGenerator.cs @@ -0,0 +1,56 @@ +using RioJoy.Core.Overlay; +using SkiaSharp; + +namespace RioJoy.Overlay; + +/// +/// Generates a profile's cockpit wallpaper: renders a label set onto the overlay +/// template's base image and writes a PNG. Resolves the template's +/// relative to the template file's +/// directory (so a regions.json + sibling riojoy.png work as a unit). +/// The runtime calls this on profile activation; an editor can call it for preview. +/// +public sealed class ProfileWallpaperGenerator +{ + private readonly SkiaOverlayRenderer _renderer; + + public ProfileWallpaperGenerator(SkiaOverlayRenderer? renderer = null) => + _renderer = renderer ?? new SkiaOverlayRenderer(); + + /// + /// Render onto the template's base image and write a + /// PNG to ; returns the output path. + /// + /// + /// Directory the template's relative + /// is resolved against (normally the folder holding the regions.json). + /// + public string Generate( + OverlayTemplate template, + string templateDirectory, + IReadOnlyDictionary labels, + string outputPath, + OverlayLayoutOptions? options = null) + { + ArgumentNullException.ThrowIfNull(template); + ArgumentNullException.ThrowIfNull(labels); + ArgumentException.ThrowIfNullOrWhiteSpace(outputPath); + if (string.IsNullOrWhiteSpace(template.BaseImagePath)) + throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set."); + + string basePath = Path.IsPathRooted(template.BaseImagePath) + ? template.BaseImagePath + : Path.Combine(templateDirectory, template.BaseImagePath); + + using SKBitmap baseImage = SKBitmap.Decode(basePath) + ?? throw new InvalidOperationException($"Could not decode base image: {basePath}"); + + byte[] png = _renderer.RenderToPng(template, labels, baseImage, options); + + string? dir = Path.GetDirectoryName(Path.GetFullPath(outputPath)); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + File.WriteAllBytes(outputPath, png); + return outputPath; + } +} diff --git a/src/RioJoy.Overlay/RioJoy.Overlay.csproj b/src/RioJoy.Overlay/RioJoy.Overlay.csproj new file mode 100644 index 0000000..90aa8c6 --- /dev/null +++ b/src/RioJoy.Overlay/RioJoy.Overlay.csproj @@ -0,0 +1,21 @@ + + + + net8.0-windows + x64 + enable + enable + latest + + + + + + + + + + + + + diff --git a/src/RioJoy.Overlay/SkiaOverlayRenderer.cs b/src/RioJoy.Overlay/SkiaOverlayRenderer.cs new file mode 100644 index 0000000..0cc07fc --- /dev/null +++ b/src/RioJoy.Overlay/SkiaOverlayRenderer.cs @@ -0,0 +1,132 @@ +using RioJoy.Core.Overlay; +using SkiaSharp; + +namespace RioJoy.Overlay; + +/// +/// Renders a cockpit wallpaper: lays out per-region label text with +/// , then draws each label onto the base image +/// with SkiaSharp. The modern replacement for the legacy GIMP/Script-Fu pipeline +/// (docs/reference/customBackground/sg-goobie-MFD.scm). Because it shares its +/// with the layout engine, drawn positions match the +/// measured extents exactly. +/// +public sealed class SkiaOverlayRenderer +{ + private readonly SkiaTextMeasurer _measurer; + private readonly OverlayLayoutEngine _engine; + private readonly SKColor _defaultColor; + + /// Label color when a region sets none (default white). + public SkiaOverlayRenderer(string defaultColorHex = "#FFFFFF") + { + _measurer = new SkiaTextMeasurer(); + _engine = new OverlayLayoutEngine(_measurer); + _defaultColor = SKColor.Parse(defaultColorHex); + } + + /// + /// Render labels onto and return the result as a + /// new bitmap (the input is not modified). Caller owns the returned bitmap. + /// + public SKBitmap Render( + OverlayTemplate template, + IReadOnlyDictionary labels, + SKBitmap baseImage, + OverlayLayoutOptions? options = null) + { + ArgumentNullException.ThrowIfNull(template); + ArgumentNullException.ThrowIfNull(labels); + ArgumentNullException.ThrowIfNull(baseImage); + options ??= new OverlayLayoutOptions(); + + var output = baseImage.Copy(); + using var canvas = new SKCanvas(output); + + foreach (PlacedLabel label in _engine.Layout(template, labels, options)) + { + OverlayRegion? region = template.FindRegion(label.RegionName); + SKColor color = region?.ColorHex is { } hex ? SKColor.Parse(hex) : _defaultColor; + + using var paint = new SKPaint + { + Typeface = _measurer.Typeface(label.FontFamily), + TextSize = (float)label.FontSizePx, + Color = color, + IsAntialias = true, + }; + + // PlacedLabel.Y is the top of the text box; Skia draws from the baseline. + float baseline = (float)label.Y - paint.FontMetrics.Ascent; // ascent is negative + + // Rotated labels: turn the canvas about the cell center (the engine + // centered the text box there). RotationDegrees is counter-clockwise; + // Skia's RotateDegrees is clockwise-positive, so negate. + if (label.RotationDegrees != 0 && region is not null) + { + float cx = (float)(region.X + region.Width / 2); + float cy = (float)(region.Y + region.Height / 2); + canvas.Save(); + canvas.RotateDegrees(-(float)label.RotationDegrees, cx, cy); + canvas.DrawText(label.Text, (float)label.X, baseline, paint); + canvas.Restore(); + continue; + } + + bool clip = options.SizeMode == OverlaySizeMode.Crop && region is not null; + if (clip) + { + canvas.Save(); + canvas.ClipRect(new SKRect( + (float)region!.X, (float)region.Y, + (float)(region.X + region.Width), (float)(region.Y + region.Height))); + } + + canvas.DrawText(label.Text, (float)label.X, baseline, paint); + + if (clip) + canvas.Restore(); + } + + canvas.Flush(); + return output; + } + + /// Render and encode to PNG bytes. + public byte[] RenderToPng( + OverlayTemplate template, + IReadOnlyDictionary labels, + SKBitmap baseImage, + OverlayLayoutOptions? options = null) + { + using SKBitmap bmp = Render(template, labels, baseImage, options); + using SKData data = bmp.Encode(SKEncodedImageFormat.Png, 100); + return data.ToArray(); + } + + /// + /// Render using the template's and + /// write a PNG to . + /// + public void RenderToFile( + OverlayTemplate template, + IReadOnlyDictionary labels, + string outputPath, + OverlayLayoutOptions? options = null) + { + ArgumentNullException.ThrowIfNull(template); + ArgumentException.ThrowIfNullOrWhiteSpace(outputPath); + if (string.IsNullOrWhiteSpace(template.BaseImagePath)) + throw new InvalidOperationException("OverlayTemplate.BaseImagePath is not set."); + + using SKBitmap baseImage = SKBitmap.Decode(template.BaseImagePath) + ?? throw new InvalidOperationException($"Could not decode base image: {template.BaseImagePath}"); + + byte[] png = RenderToPng(template, labels, baseImage, options); + + string? dir = Path.GetDirectoryName(Path.GetFullPath(outputPath)); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + File.WriteAllBytes(outputPath, png); + } +} diff --git a/src/RioJoy.Overlay/SkiaTextMeasurer.cs b/src/RioJoy.Overlay/SkiaTextMeasurer.cs new file mode 100644 index 0000000..095ba0d --- /dev/null +++ b/src/RioJoy.Overlay/SkiaTextMeasurer.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; +using RioJoy.Core.Overlay; +using SkiaSharp; + +namespace RioJoy.Overlay; + +/// +/// backed by SkiaSharp — the real metrics the +/// draws with, so measured layout and rendered +/// output agree. Width is the glyph advance; height is the logical line height +/// (descent − ascent), which keeps vertical centering stable across labels +/// regardless of which glyphs each one happens to contain. +/// +public sealed class SkiaTextMeasurer : ITextMeasurer +{ + private readonly ConcurrentDictionary _typefaces = new(); + + internal SKTypeface Typeface(string fontFamily) => + _typefaces.GetOrAdd(fontFamily, f => SKTypeface.FromFamilyName(f) ?? SKTypeface.Default); + + public TextExtent Measure(string text, string fontFamily, double fontSizePx) + { + using var paint = new SKPaint + { + Typeface = Typeface(fontFamily), + TextSize = (float)fontSizePx, + IsAntialias = true, + }; + + double width = paint.MeasureText(text); + SKFontMetrics m = paint.FontMetrics; + double height = m.Descent - m.Ascent; // ascent is negative + return new TextExtent(width, height); + } +} diff --git a/src/RioJoy.Tray/Editor/ProfileEditorForm.cs b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs new file mode 100644 index 0000000..c0c65e5 --- /dev/null +++ b/src/RioJoy.Tray/Editor/ProfileEditorForm.cs @@ -0,0 +1,255 @@ +using System.Runtime.Versioning; +using RioJoy.Core.Editing; +using RioJoy.Core.Hid; +using RioJoy.Core.Mapping; +using RioJoy.Core.Profiles; + +namespace RioJoy.Tray.Editor; + +/// +/// Profile editor: shows the cockpit authoring sheet (its logical layout, from +/// ) as a scrollable, clickable grid and edits the +/// selected address cell's label and action/modifiers/lamp. Writes back to the +/// 's (keyed by the +/// wallpaper region b-XX) and (the iRIO +/// word); Save raises . +/// +[SupportedOSPlatform("windows")] +public sealed class ProfileEditorForm : Form +{ + private readonly RioProfile _profile; + private readonly IReadOnlyList _cells; + private readonly SheetCanvas _canvas = new(); + + private readonly Label _info = new() { AutoSize = true, Location = new Point(12, 12), MaximumSize = new Size(310, 0) }; + private readonly TextBox _labelBox = new() { Location = new Point(70, 44), Width = 230 }; + private readonly ComboBox _kindBox = new() { Location = new Point(70, 78), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList }; + private readonly Label _valueLabel = new() { Text = "Key:", Location = new Point(12, 115), AutoSize = true }; + private readonly ComboBox _valueCombo = new() { Location = new Point(70, 112), Width = 230, DropDownStyle = ComboBoxStyle.DropDownList }; + private readonly CheckBox _shift = new() { Text = "Shift", Location = new Point(70, 144), AutoSize = true }; + private readonly CheckBox _ctrl = new() { Text = "Ctrl", Location = new Point(140, 144), AutoSize = true }; + private readonly CheckBox _alt = new() { Text = "Alt", Location = new Point(200, 144), AutoSize = true }; + private readonly CheckBox _ext = new() { Text = "Ext", Location = new Point(250, 144), AutoSize = true }; + private readonly CheckBox _lit = new() { Text = "Lit", Location = new Point(70, 172), AutoSize = true }; + private readonly Button _apply = new() { Text = "Apply to cell", Location = new Point(70, 206), Width = 110 }; + private readonly Button _save = new() { Text = "Save profile", Location = new Point(70, 240), Width = 110 }; + private readonly Button _close = new() { Text = "Close", Location = new Point(190, 240), Width = 80 }; + + private int? _address; + private bool _loading; + + /// A pick-list entry: what the user sees and the byte it maps to. + private sealed record ValueItem(string Display, byte Value) + { + public override string ToString() => Display; + } + + /// Raised when the user saves; the argument is the edited profile. + public event Action? Saved; + + public ProfileEditorForm(IReadOnlyList cells, RioProfile profile) + { + _cells = cells ?? throw new ArgumentNullException(nameof(cells)); + _profile = profile ?? throw new ArgumentNullException(nameof(profile)); + + Text = $"RIOJoy — Edit profile: {profile.Name}"; + ClientSize = new Size(1500, 820); + StartPosition = FormStartPosition.CenterScreen; + MinimumSize = new Size(900, 500); + + _kindBox.Items.AddRange(Enum.GetNames()); + _kindBox.SelectedIndexChanged += OnKindChanged; + _canvas.SetCells(cells); + _canvas.AddressSelected += OnAddressSelected; + + var scroller = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.FromArgb(28, 28, 28) }; + scroller.Controls.Add(_canvas); + + Panel editPanel = BuildEditPanel(); + + Controls.Add(scroller); + Controls.Add(editPanel); + + _apply.Click += (_, _) => ApplyToCell(); + _save.Click += (_, _) => { ApplyToCell(); Saved?.Invoke(_profile); }; + _close.Click += (_, _) => Close(); + + SetEditingEnabled(false); + _info.Text = "Click a coloured address cell to edit it."; + } + + /// The selected RIO address (for tests / offline rendering). + public int? SelectedAddress => _address; + + private Panel BuildEditPanel() + { + var panel = new Panel { Dock = DockStyle.Right, Width = 330, Padding = new Padding(8), BorderStyle = BorderStyle.FixedSingle }; + + panel.Controls.Add(_info); + panel.Controls.Add(new Label { Text = "Label:", Location = new Point(12, 47), AutoSize = true }); + panel.Controls.Add(_labelBox); + panel.Controls.Add(new Label { Text = "Action:", Location = new Point(12, 81), AutoSize = true }); + panel.Controls.Add(_kindBox); + panel.Controls.Add(_valueLabel); + panel.Controls.Add(_valueCombo); + panel.Controls.AddRange(new Control[] { _shift, _ctrl, _alt, _ext, _lit, _apply, _save, _close }); + + return panel; + } + + private void OnAddressSelected(SheetCell cell) + { + if (cell.Address is not { } addr) { SetEditingEnabled(false); return; } + + _address = addr; + string region = RegionFor(addr); + _info.Text = $"Cell {cell.Text} Address 0x{addr:X2} {cell.Section} (wallpaper {region})"; + + _labelBox.Text = _profile.OverlayLabels.TryGetValue(region, out string? label) ? label : string.Empty; + + ButtonBinding b = ButtonBinding.FromWord( + _profile.Buttons.TryGetValue(addr, out int w) ? (ushort)w : (ushort)0); + + _loading = true; + _kindBox.SelectedItem = b.Kind.ToString(); + PopulateValues(b.Kind); + SelectValue(b.Value); + _shift.Checked = b.Shift; + _ctrl.Checked = b.Ctrl; + _alt.Checked = b.Alt; + _ext.Checked = b.Extended; + _lit.Checked = b.IsLit; + _loading = false; + + SetEditingEnabled(true); + } + + private void OnKindChanged(object? sender, EventArgs e) + { + if (_loading) return; + PopulateValues(SelectedKind()); + if (_valueCombo.Items.Count > 0) _valueCombo.SelectedIndex = 0; + UpdateModifierAvailability(); + } + + // Fill the value picker with friendly choices for the chosen routing kind. + private void PopulateValues(RioRouteKind kind) + { + _valueLabel.Text = kind switch + { + RioRouteKind.Keyboard => "Key:", + RioRouteKind.Joystick => "Button:", + RioRouteKind.Hat => "Direction:", + RioRouteKind.Mouse => "Action:", + RioRouteKind.RioCommand => "Command:", + _ => "Value:", + }; + + _valueCombo.BeginUpdate(); + _valueCombo.Items.Clear(); + + switch (kind) + { + case RioRouteKind.Keyboard: + foreach (KeyName k in KeyCatalog.Entries) + _valueCombo.Items.Add(new ValueItem(k.Name, k.Value)); + break; + case RioRouteKind.Joystick: + for (int btn = 1; btn <= RioHidReport.ButtonCount; btn++) + _valueCombo.Items.Add(new ValueItem($"Button {btn}", (byte)btn)); + break; + case RioRouteKind.Hat: + foreach (RioHat h in new[] { RioHat.Up, RioHat.Right, RioHat.Down, RioHat.Left }) + _valueCombo.Items.Add(new ValueItem(h.ToString(), (byte)(int)h)); + break; + case RioRouteKind.Mouse: + foreach (MouseAction m in Enum.GetValues()) + _valueCombo.Items.Add(new ValueItem(m.ToString(), (byte)m)); + break; + case RioRouteKind.RioCommand: + foreach (RioCommandCode c in Enum.GetValues()) + _valueCombo.Items.Add(new ValueItem(c.ToString(), (byte)c)); + break; + } + + _valueCombo.EndUpdate(); + UpdateModifierAvailability(); + } + + private void SelectValue(byte value) + { + for (int i = 0; i < _valueCombo.Items.Count; i++) + { + if (_valueCombo.Items[i] is ValueItem v && v.Value == value) + { + _valueCombo.SelectedIndex = i; + return; + } + } + // Not in the list (e.g. an uncatalogued VK): add a raw fallback entry. + int idx = _valueCombo.Items.Add(new ValueItem($"0x{value:X2}", value)); + _valueCombo.SelectedIndex = idx; + } + + // Modifiers/extended only make sense for keyboard actions. + private void UpdateModifierAvailability() + { + bool keyboard = SelectedKind() == RioRouteKind.Keyboard; + foreach (Control c in new Control[] { _shift, _ctrl, _alt, _ext }) + c.Enabled = keyboard; + } + + private RioRouteKind SelectedKind() => + Enum.TryParse(_kindBox.SelectedItem?.ToString(), out RioRouteKind k) ? k : RioRouteKind.Keyboard; + + private void ApplyToCell() + { + if (_address is not { } addr) return; + + string region = RegionFor(addr); + string label = _labelBox.Text.Trim(); + if (string.IsNullOrEmpty(label)) + _profile.OverlayLabels.Remove(region); + else + _profile.OverlayLabels[region] = label; + + RioRouteKind kind = SelectedKind(); + bool keyboard = kind == RioRouteKind.Keyboard; + var binding = new ButtonBinding + { + Kind = kind, + Value = _valueCombo.SelectedItem is ValueItem vi ? vi.Value : (byte)0, + Shift = keyboard && _shift.Checked, + Ctrl = keyboard && _ctrl.Checked, + Alt = keyboard && _alt.Checked, + Extended = keyboard && _ext.Checked, + IsLit = _lit.Checked, + }; + + ushort word = binding.ToWord(); + if (word == 0) + _profile.Buttons.Remove(addr); + else + _profile.Buttons[addr] = word; + } + + private void SetEditingEnabled(bool enabled) + { + foreach (Control c in new Control[] { _labelBox, _kindBox, _valueCombo, _shift, _ctrl, _alt, _ext, _lit, _apply }) + c.Enabled = enabled; + if (enabled) UpdateModifierAvailability(); + } + + private static string RegionFor(int address) => $"b-{address:X2}"; + + /// Select an address programmatically (for tests / offline preview). + public void SelectAddress(int address) + { + SheetCell? cell = _cells.FirstOrDefault(c => c.Address == address); + if (cell is not null) + { + _canvas.Select(address); + OnAddressSelected(cell); + } + } +} diff --git a/src/RioJoy.Tray/Editor/SheetCanvas.cs b/src/RioJoy.Tray/Editor/SheetCanvas.cs new file mode 100644 index 0000000..0a070f8 --- /dev/null +++ b/src/RioJoy.Tray/Editor/SheetCanvas.cs @@ -0,0 +1,57 @@ +using System.Runtime.Versioning; +using RioJoy.Core.Editing; + +namespace RioJoy.Tray.Editor; + +/// +/// Scrollable cockpit-sheet grid: draws the cells (via ) at a +/// fixed cell size and raises when an address cell is +/// clicked. Sized to the full grid so the host panel scrolls it. +/// +[SupportedOSPlatform("windows")] +internal sealed class SheetCanvas : Control +{ + private const int CellW = 62; + private const int CellH = 22; + + private IReadOnlyList _cells = Array.Empty(); + + public SheetCanvas() + { + DoubleBuffered = true; + BackColor = Color.FromArgb(28, 28, 28); + } + + /// The RIO address currently highlighted, or null. + public int? SelectedAddress { get; private set; } + + public event Action? AddressSelected; + + public void SetCells(IReadOnlyList cells) + { + _cells = cells; + Size = SheetView.GridSize(cells, CellW, CellH); + Invalidate(); + } + + public void Select(int? address) + { + SelectedAddress = address; + Invalidate(); + } + + protected override void OnPaint(PaintEventArgs e) => + SheetView.Paint(e.Graphics, _cells, CellW, CellH, SelectedAddress); + + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + SheetCell? hit = SheetView.HitTest(_cells, e.Location, CellW, CellH); + if (hit?.Address is { } addr) + { + SelectedAddress = addr; + AddressSelected?.Invoke(hit); + Invalidate(); + } + } +} diff --git a/src/RioJoy.Tray/Editor/SheetView.cs b/src/RioJoy.Tray/Editor/SheetView.cs new file mode 100644 index 0000000..5c80ca9 --- /dev/null +++ b/src/RioJoy.Tray/Editor/SheetView.cs @@ -0,0 +1,83 @@ +using System.Runtime.Versioning; +using RioJoy.Core.Editing; + +namespace RioJoy.Tray.Editor; + +/// +/// Draws the cockpit authoring sheet as a grid of cells at their spreadsheet +/// (row, col) positions — the cockpit's logical layout. Address cells are filled by +/// section and clickable; scaffolding/label cells are drawn as context text. Shared +/// by and offline bitmap rendering. +/// +[SupportedOSPlatform("windows")] +public static class SheetView +{ + public static Color SectionColor(SheetSection section) => section switch + { + SheetSection.Contacts or SheetSection.Letters => Color.FromArgb(232, 150, 150), // red + SheetSection.Secondary or SheetSection.Screen => Color.FromArgb(245, 224, 150), // yellow + SheetSection.Throttle or SheetSection.Joystick => Color.FromArgb(150, 182, 226), // blue + SheetSection.Keypad or SheetSection.ExtKeypad => Color.FromArgb(165, 212, 165), // green + _ => Color.Gainsboro, + }; + + public static Size GridSize(IReadOnlyList cells, int cellW, int cellH) + { + int maxCol = 0, maxRow = 0; + foreach (SheetCell c in cells) + { + if (c.Col > maxCol) maxCol = c.Col; + if (c.Row > maxRow) maxRow = c.Row; + } + return new Size((maxCol + 1) * cellW, (maxRow + 1) * cellH); + } + + public static void Paint( + Graphics g, + IReadOnlyList cells, + int cellW, + int cellH, + int? selectedAddress) + { + g.Clear(Color.FromArgb(28, 28, 28)); + + using var addrFont = new Font("Segoe UI", 7.5f, FontStyle.Bold); + using var textFont = new Font("Segoe UI", 7f); + using var border = new Pen(Color.FromArgb(70, 70, 70)); + + foreach (SheetCell cell in cells) + { + var r = new Rectangle(cell.Col * cellW, cell.Row * cellH, cellW, cellH); + + if (cell.Address is { } addr) + { + using var fill = new SolidBrush(SectionColor(cell.Section)); + g.FillRectangle(fill, r); + g.DrawRectangle(border, r); + TextRenderer.DrawText(g, cell.Text, addrFont, r, Color.Black, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix); + + if (addr == selectedAddress) + { + using var hi = new Pen(Color.Yellow, 2f); + g.DrawRectangle(hi, r.X + 1, r.Y + 1, r.Width - 2, r.Height - 2); + } + } + else + { + // Scaffolding / KEY / LABEL / section headers — context only. + TextRenderer.DrawText(g, cell.Text, textFont, r, Color.Silver, + TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | + TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix); + } + } + } + + /// The address cell at a pixel point, or null. + public static SheetCell? HitTest(IReadOnlyList cells, Point p, int cellW, int cellH) + { + int col = p.X / cellW; + int row = p.Y / cellH; + return cells.FirstOrDefault(c => c.Row == row && c.Col == col && c.Address is not null); + } +} diff --git a/src/RioJoy.Tray/RioCoordinator.cs b/src/RioJoy.Tray/RioCoordinator.cs index bc3c8fe..a6e4613 100644 --- a/src/RioJoy.Tray/RioCoordinator.cs +++ b/src/RioJoy.Tray/RioCoordinator.cs @@ -3,8 +3,10 @@ using RioJoy.Core; using RioJoy.Core.Calibration; using RioJoy.Core.Mapping; using RioJoy.Core.Output; +using RioJoy.Core.Overlay; using RioJoy.Core.Profiles; using RioJoy.Core.Serial; +using RioJoy.Overlay; namespace RioJoy.Tray; @@ -141,7 +143,48 @@ public sealed class RioCoordinator : IDisposable { Teardown(); SetStatus($"Error opening {port}: {ex.Message}"); + return; } + + ApplyWallpaper(profile); + } + + /// + /// Generate the profile's cockpit wallpaper from the configured overlay template + /// and apply it. Best-effort and opt-in (only when + /// is set), so a missing template or render failure never breaks activation. + /// + private void ApplyWallpaper(RioProfile profile) + { + string? templatePath = _config().OverlayTemplatePath; + if (string.IsNullOrWhiteSpace(templatePath) || !File.Exists(templatePath)) + return; + + try + { + OverlayTemplate template = OverlayTemplateStore.Load(templatePath); + string templateDir = Path.GetDirectoryName(Path.GetFullPath(templatePath)) ?? "."; + + string outDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "RIOJoy", "wallpapers"); + string outPath = Path.Combine(outDir, SafeFileName(profile.Name) + ".png"); + + new ProfileWallpaperGenerator().Generate(template, templateDir, profile.OverlayLabels, outPath); + profile.WallpaperPath = outPath; + WallpaperApplier.Apply(outPath); + } + catch (Exception ex) + { + SetStatus($"{Status} [wallpaper: {ex.Message}]"); + } + } + + private static string SafeFileName(string name) + { + foreach (char c in Path.GetInvalidFileNameChars()) + name = name.Replace(c, '_'); + return name; } private void GoDormant(string status) diff --git a/src/RioJoy.Tray/RioJoy.Tray.csproj b/src/RioJoy.Tray/RioJoy.Tray.csproj index 65b4e9f..a5f1f5c 100644 --- a/src/RioJoy.Tray/RioJoy.Tray.csproj +++ b/src/RioJoy.Tray/RioJoy.Tray.csproj @@ -13,6 +13,7 @@ + diff --git a/src/RioJoy.Tray/TrayApplicationContext.cs b/src/RioJoy.Tray/TrayApplicationContext.cs index bf526bc..53d466d 100644 --- a/src/RioJoy.Tray/TrayApplicationContext.cs +++ b/src/RioJoy.Tray/TrayApplicationContext.cs @@ -1,6 +1,8 @@ using System.Runtime.Versioning; +using RioJoy.Core.Editing; using RioJoy.Core.Mapping; using RioJoy.Core.Profiles; +using RioJoy.Tray.Editor; using RioJoy.Tray.Os; namespace RioJoy.Tray; @@ -67,6 +69,8 @@ internal sealed class TrayApplicationContext : ApplicationContext RebuildProfileMenu(profileMenu); menu.Items.Add(profileMenu); + menu.Items.Add("Edit profile…", null, (_, _) => OpenEditor()); + // RIO commands mirroring the legacy console menu. var commands = new ToolStripMenuItem("RIO commands"); AddCommand(commands, "Reset all analog axes", RioCommandCode.ResetAllAxes); @@ -129,6 +133,48 @@ internal sealed class TrayApplicationContext : ApplicationContext profileMenu.DropDownItems.Add(dormant); } + private void OpenEditor() + { + // The cockpit sheet (config-sheet.csv) sits beside the overlay template. + string? templatePath = _config.OverlayTemplatePath; + string? sheetPath = string.IsNullOrWhiteSpace(templatePath) + ? null + : Path.Combine(Path.GetDirectoryName(Path.GetFullPath(templatePath)) ?? ".", "config-sheet.csv"); + + if (sheetPath is null || !File.Exists(sheetPath)) + { + MessageBox.Show( + "Set 'OverlayTemplatePath' in the config and place 'config-sheet.csv' beside it to use the editor.", + "RIOJoy", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + IReadOnlyList cells; + try + { + cells = SheetLayout.Load(sheetPath); + } + catch (Exception ex) + { + MessageBox.Show($"Could not load the cockpit sheet:\n{ex.Message}", "RIOJoy", + MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + RioProfile profile = _config.Profiles.FirstOrDefault() ?? AddBlankProfile(); + + var editor = new ProfileEditorForm(cells, profile); + editor.Saved += _ => ConfigStore.Save(_config, ConfigPath); + editor.Show(); + } + + private RioProfile AddBlankProfile() + { + var profile = new RioProfile { Name = "New profile" }; + _config.Profiles.Add(profile); + return profile; + } + private void AddCommand(ToolStripMenuItem parent, string text, RioCommandCode code) { var item = new ToolStripMenuItem(text); diff --git a/src/RioJoy.Tray/WallpaperApplier.cs b/src/RioJoy.Tray/WallpaperApplier.cs new file mode 100644 index 0000000..024f9fb --- /dev/null +++ b/src/RioJoy.Tray/WallpaperApplier.cs @@ -0,0 +1,42 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace RioJoy.Tray; + +/// +/// Sets the Windows desktop wallpaper to a generated cockpit overlay +/// (Phase 7 — the modern replacement for manually setting the GIMP-exported +/// background). Wraps SystemParametersInfo(SPI_SETDESKWALLPAPER). +/// +/// Windows requires a BMP file path for reliable results on +/// all versions; PNGs are accepted on modern Windows but not guaranteed, so callers +/// should hand this a path the overlay renderer wrote. Applying a wallpaper changes +/// a user-visible system setting, so it is only invoked on an explicit profile +/// activation, never speculatively. +/// +[SupportedOSPlatform("windows")] +public static class WallpaperApplier +{ + private const int SPI_SETDESKWALLPAPER = 0x0014; + private const int SPIF_UPDATEINIFILE = 0x01; // persist across logon + private const int SPIF_SENDCHANGE = 0x02; // broadcast WM_SETTINGCHANGE + + /// + /// Apply as the desktop wallpaper. Returns true on + /// success. Throws if the file does not exist. + /// + public static bool Apply(string imagePath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(imagePath); + string full = Path.GetFullPath(imagePath); + if (!File.Exists(full)) + throw new FileNotFoundException("Wallpaper image not found.", full); + + return SystemParametersInfo( + SPI_SETDESKWALLPAPER, 0, full, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + } + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); +} diff --git a/tests/RioJoy.Core.Tests/Editing/SheetLayoutTests.cs b/tests/RioJoy.Core.Tests/Editing/SheetLayoutTests.cs new file mode 100644 index 0000000..a025a27 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Editing/SheetLayoutTests.cs @@ -0,0 +1,78 @@ +using RioJoy.Core.Editing; +using Xunit; + +namespace RioJoy.Core.Tests.Editing; + +public class SheetLayoutTests +{ + [Theory] + [InlineData("0F", 0x0F)] + [InlineData("50", 0x50)] + [InlineData("6F", 0x6F)] + [InlineData("00", 0x00)] + public void AddressFromText_ParsesHexCells(string text, int expected) + { + Assert.Equal(expected, SheetLayout.AddressFromText(text)); + } + + [Theory] + [InlineData("HOME")] + [InlineData("RIO_0")] + [InlineData("70")] // > 0x6F + [InlineData("M U")] + [InlineData("0")] // single digit + public void AddressFromText_RejectsNonAddresses(string text) + { + Assert.Null(SheetLayout.AddressFromText(text)); + } + + [Theory] + [InlineData(0x00, SheetSection.Contacts)] + [InlineData(0x10, SheetSection.Secondary)] + [InlineData(0x18, SheetSection.Screen)] + [InlineData(0x2F, SheetSection.Letters)] + [InlineData(0x38, SheetSection.Throttle)] + [InlineData(0x40, SheetSection.Joystick)] + [InlineData(0x50, SheetSection.Keypad)] + [InlineData(0x6F, SheetSection.ExtKeypad)] + public void SectionForAddress_Classifies(int address, SheetSection expected) + { + Assert.Equal(expected, SheetLayout.SectionForAddress(address)); + } + + [Fact] + public void Parse_BuildsCellsWithGridPositions() + { + const string csv = + "\"\",\"IsLit\",\"\",\"\",\"0F\"\n" + + "\"\",\"\",\"\",\"\",\"HOME\""; + + IReadOnlyList cells = SheetLayout.Parse(csv); + + SheetCell addr = Assert.Single(cells, c => c.Text == "0F"); + Assert.Equal(0x0F, addr.Address); + Assert.Equal(SheetSection.Contacts, addr.Section); + Assert.Equal(0, addr.Row); + Assert.Equal(4, addr.Col); + + SheetCell key = Assert.Single(cells, c => c.Text == "HOME"); + Assert.Null(key.Address); + Assert.Equal(1, key.Row); + Assert.Equal(4, key.Col); + } + + [Fact] + public void Parse_RealSheet_HasFullAddressMap() + { + IReadOnlyList cells = SheetLayout.Load( + TestRepo.CustomBackground("config-sheet.csv")); + + // Every RIO address 0x00..0x6F that exists should appear exactly once as a cell. + var addresses = cells.Where(c => c.Address is not null).Select(c => c.Address!.Value).ToHashSet(); + Assert.Contains(0x00, addresses); + Assert.Contains(0x2F, addresses); // a letter button + Assert.Contains(0x50, addresses); // keypad + Assert.Contains(0x6F, addresses); // ext keypad + Assert.True(addresses.Count >= 100, $"expected most of the address map, got {addresses.Count}"); + } +} diff --git a/tests/RioJoy.Core.Tests/Mapping/ButtonBindingTests.cs b/tests/RioJoy.Core.Tests/Mapping/ButtonBindingTests.cs new file mode 100644 index 0000000..5d67faa --- /dev/null +++ b/tests/RioJoy.Core.Tests/Mapping/ButtonBindingTests.cs @@ -0,0 +1,84 @@ +using RioJoy.Core.Mapping; +using Xunit; + +namespace RioJoy.Core.Tests.Mapping; + +public class ButtonBindingTests +{ + [Theory] + [InlineData(RioRouteKind.Keyboard, 0x47, 0x0047)] + [InlineData(RioRouteKind.Joystick, 0x58, 0x1058)] + [InlineData(RioRouteKind.Hat, 0x01, 0x2001)] + [InlineData(RioRouteKind.Mouse, 0x10, 0x4010)] + [InlineData(RioRouteKind.RioCommand, 0x60, 0x7060)] + public void Create_PacksRoutingFlagsAndValue(RioRouteKind kind, byte value, ushort expected) + { + RioMapEntry e = RioMapEntry.Create(kind, value); + Assert.Equal(expected, e.Raw); + Assert.Equal(kind, e.Kind); + Assert.Equal(value, e.Value); + } + + [Fact] + public void Create_SetsHighByteBits() + { + RioMapEntry e = RioMapEntry.Create( + RioRouteKind.Keyboard, 0x41, lit: true, shift: true, ctrl: true, alt: true, extended: true); + + Assert.True(e.HasLamp); + Assert.True(e.Shift); + Assert.True(e.Ctrl); + Assert.True(e.Alt); + Assert.True(e.Extended); + Assert.Equal(0x41, e.Value); + // 0x8000|0x0800|0x0400|0x0200|0x0100|0x41 + Assert.Equal(0x8F41, e.Raw); + } + + [Theory] + [InlineData(RioRouteKind.Keyboard)] + [InlineData(RioRouteKind.Joystick)] + [InlineData(RioRouteKind.Hat)] + [InlineData(RioRouteKind.Mouse)] + [InlineData(RioRouteKind.RioCommand)] + public void Binding_RoundTripsThroughWord(RioRouteKind kind) + { + var b = new ButtonBinding + { + Kind = kind, + Value = 0x2A, + IsLit = true, + Shift = true, + Alt = true, + }; + + ushort word = b.ToWord(); + ButtonBinding back = ButtonBinding.FromWord(word); + + Assert.Equal(b.Kind, back.Kind); + Assert.Equal(b.Value, back.Value); + Assert.Equal(b.IsLit, back.IsLit); + Assert.Equal(b.Shift, back.Shift); + Assert.Equal(b.Ctrl, back.Ctrl); + Assert.Equal(b.Alt, back.Alt); + Assert.Equal(b.Extended, back.Extended); + Assert.Equal(word, back.ToWord()); + } + + [Fact] + public void Binding_FromWord_DecodesLegacyValue() + { + // 0x8F41 from the encode test -> lit keyboard 'A' with all modifiers. + ButtonBinding b = ButtonBinding.FromWord(0x8F41); + Assert.Equal(RioRouteKind.Keyboard, b.Kind); + Assert.Equal(0x41, b.Value); + Assert.True(b.IsLit && b.Shift && b.Ctrl && b.Alt && b.Extended); + } + + [Fact] + public void Binding_Default_IsUnmapped() + { + Assert.True(new ButtonBinding().IsUnmapped); + Assert.False(new ButtonBinding { Value = 0x41 }.IsUnmapped); + } +} diff --git a/tests/RioJoy.Core.Tests/Mapping/KeyCatalogTests.cs b/tests/RioJoy.Core.Tests/Mapping/KeyCatalogTests.cs new file mode 100644 index 0000000..7255904 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Mapping/KeyCatalogTests.cs @@ -0,0 +1,42 @@ +using RioJoy.Core.Mapping; +using Xunit; + +namespace RioJoy.Core.Tests.Mapping; + +public class KeyCatalogTests +{ + [Theory] + [InlineData("A", 0x41)] + [InlineData("Z", 0x5A)] + [InlineData("0", 0x30)] + [InlineData("F1", 0x70)] + [InlineData("F12", 0x7B)] + [InlineData("Enter", 0x0D)] + [InlineData("Space", 0x20)] + [InlineData("Left", 0x25)] + [InlineData("NumPad 5", 0x65)] + public void NameAndValue_RoundTrip(string name, int vk) + { + Assert.True(KeyCatalog.TryGetValue(name, out byte v)); + Assert.Equal((byte)vk, v); + Assert.Equal(name, KeyCatalog.NameFor((byte)vk)); + } + + [Fact] + public void NameFor_UnknownVk_ReturnsHex() + { + Assert.Equal("0xFF", KeyCatalog.NameFor(0xFF)); + } + + [Fact] + public void TryGetValue_Unknown_ReturnsFalse() + { + Assert.False(KeyCatalog.TryGetValue("NotAKey", out _)); + } + + [Fact] + public void Entries_AreUniqueByName() + { + Assert.Equal(KeyCatalog.Entries.Count, KeyCatalog.Entries.Select(e => e.Name).Distinct().Count()); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/CockpitRegionsTests.cs b/tests/RioJoy.Core.Tests/Overlay/CockpitRegionsTests.cs new file mode 100644 index 0000000..26d7e5e --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/CockpitRegionsTests.cs @@ -0,0 +1,68 @@ +using RioJoy.Core.Overlay; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +/// +/// Regression tests over the committed cockpit regions.json extracted from +/// riojoy.xcf by tools/XcfRegionExtract. Anchors the extracted geometry and proves +/// the asset loads through . +/// +public class CockpitRegionsTests +{ + private static OverlayTemplate Load() => + OverlayTemplateStore.Load(TestRepo.CustomBackground("regions.json")); + + [Fact] + public void Loads_WithExpectedCanvasAndCount() + { + OverlayTemplate t = Load(); + Assert.Equal(2720, t.Width); + Assert.Equal(600, t.Height); + Assert.Equal(119, t.Regions.Count); + } + + [Fact] + public void HasExpectedButtonCell() + { + OverlayRegion? b00 = Load().FindRegion("b-00"); + Assert.NotNull(b00); + Assert.Equal(2558, b00!.X, 6); + Assert.Equal(428, b00.Y, 6); + Assert.Equal(153, b00.Width, 6); + Assert.Equal(53, b00.Height, 6); + Assert.Equal(45, b00.FontSizePx, 6); + Assert.Equal("Sans", b00.FontFamily); + } + + [Fact] + public void IncludesNamedControlLabels() + { + OverlayTemplate t = Load(); + Assert.NotNull(t.FindRegion("Throttle")); + Assert.NotNull(t.FindRegion("Trigger")); + Assert.NotNull(t.FindRegion("HatUp")); + } + + [Fact] + public void VerticalCells_AreTaggedCcw90() + { + OverlayTemplate t = Load(); + Assert.Equal(90.0, t.FindRegion("a-00")!.RotationDegrees); + Assert.Equal(90.0, t.FindRegion("b-10")!.RotationDegrees); + Assert.Equal(90.0, t.FindRegion("b-1F")!.RotationDegrees); + Assert.Null(t.FindRegion("b-00")!.RotationDegrees); // a normal upright cell + } + + [Fact] + public void AllRegionsLieWithinCanvas() + { + OverlayTemplate t = Load(); + Assert.All(t.Regions, r => + { + Assert.True(r.X >= 0 && r.Y >= 0, $"{r.Name} has negative offset"); + Assert.True(r.X + r.Width <= t.Width, $"{r.Name} overflows width"); + Assert.True(r.Y + r.Height <= t.Height, $"{r.Name} overflows height"); + }); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/FontFitterTests.cs b/tests/RioJoy.Core.Tests/Overlay/FontFitterTests.cs new file mode 100644 index 0000000..a3a0d32 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/FontFitterTests.cs @@ -0,0 +1,75 @@ +using RioJoy.Core.Overlay; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +public class FontFitterTests +{ + // Brute-force oracle: the largest integer size whose text fits, mirroring the + // routine's truncate-to-integer behavior and MinFontSize floor. + private static double BruteForceBestFit( + string text, ITextMeasurer m, double width, double height, double cap = 1000) + { + double best = FontFitter.MinFontSize; + for (double s = FontFitter.MinFontSize; s <= cap; s++) + { + TextExtent e = m.Measure(text, "f", s); + if (e.Width <= width && e.Height <= height) + best = s; + } + return best; + } + + [Theory] + [InlineData("FIRE", 100, 20)] + [InlineData("GEAR UP", 120, 24)] + [InlineData("X", 50, 50)] + [InlineData("LONGER LABEL TEXT", 80, 16)] + [InlineData("WW", 10, 40)] // width-bound, tiny cell + public void BestFitSize_MatchesBruteForceOracle(string text, double w, double h) + { + var m = new LinearTextMeasurer(); + + double fit = FontFitter.BestFitSize(text, "f", w, h, m); + double oracle = BruteForceBestFit(text, m, w, h); + + Assert.Equal(oracle, fit); + } + + [Fact] + public void BestFitSize_ResultActuallyFits() + { + var m = new LinearTextMeasurer(); + double size = FontFitter.BestFitSize("THROTTLE", "f", 90, 18, m); + + TextExtent e = m.Measure("THROTTLE", "f", size); + Assert.True(e.Width <= 90 && e.Height <= 18, $"size {size} -> {e.Width}x{e.Height} should fit 90x18"); + } + + [Fact] + public void BestFitSize_NeverBelowMinimum() + { + var m = new LinearTextMeasurer(); + // A cell far too small for any text still yields the floor, not 0/negative. + double size = FontFitter.BestFitSize("IMPOSSIBLE", "f", 1, 1, m); + Assert.Equal(FontFitter.MinFontSize, size); + } + + [Fact] + public void BestFitSize_EmptyText_Terminates() + { + // Empty text has zero width; the loop must still converge (via repeated + // extents) rather than grow forever. + double size = FontFitter.BestFitSize("", "f", 100, 100, new LinearTextMeasurer()); + Assert.True(size >= FontFitter.MinFontSize); + } + + [Fact] + public void BestFitSize_LargerCell_AllowsLargerFont() + { + var m = new LinearTextMeasurer(); + double small = FontFitter.BestFitSize("ABC", "f", 60, 12, m); + double big = FontFitter.BestFitSize("ABC", "f", 600, 120, m); + Assert.True(big > small); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/GoobieDataImporterTests.cs b/tests/RioJoy.Core.Tests/Overlay/GoobieDataImporterTests.cs new file mode 100644 index 0000000..547f154 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/GoobieDataImporterTests.cs @@ -0,0 +1,57 @@ +using RioJoy.Core.Overlay; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +public class GoobieDataImporterTests +{ + private const string Sample = """ + ; a comment, ignored + ( a-00 b-00 b-01 b-02 ) + ( "tag" "FIRE" "" "GEAR UP" ) + """; + + [Fact] + public void Parse_ReadsFieldsAndRow() + { + GoobieDataImporter.Sheet sheet = GoobieDataImporter.Parse(Sample); + + Assert.Equal(new[] { "a-00", "b-00", "b-01", "b-02" }, sheet.Fields); + Assert.Single(sheet.Rows); + Assert.Equal("tag", sheet.Rows[0]["a-00"]); + Assert.Equal("FIRE", sheet.Rows[0]["b-00"]); + Assert.Equal("GEAR UP", sheet.Rows[0]["b-02"]); // quoted strings keep spaces + } + + [Fact] + public void LabelsForRow_DropsEmptyValues() + { + IReadOnlyDictionary labels = + GoobieDataImporter.LabelsForRow(GoobieDataImporter.Parse(Sample)); + + Assert.True(labels.ContainsKey("b-00")); + Assert.False(labels.ContainsKey("b-01")); // empty string dropped + Assert.Equal(3, labels.Count); // a-00, b-00, b-02 + } + + [Fact] + public void Parse_Field0_IsTheRowFileTag() + { + GoobieDataImporter.Sheet sheet = GoobieDataImporter.Parse(Sample); + Assert.Equal("a-00", sheet.Fields[0]); + Assert.Equal("tag", sheet.Rows[0][sheet.Fields[0]]); + } + + [Fact] + public void Loads_RealTestDataFile() + { + GoobieDataImporter.Sheet sheet = GoobieDataImporter.Load(TestRepo.CustomBackground("TEST.data")); + + Assert.Equal("a-00", sheet.Fields[0]); + Assert.NotEmpty(sheet.Rows); + // Header: ( a-00 b-00 b-01 ... ) Row0: ( "defalt" "LSDI" "ZOOM" ... ) + Assert.Equal("defalt", sheet.Rows[0]["a-00"]); + Assert.Equal("LSDI", sheet.Rows[0]["b-00"]); + Assert.Equal("ZOOM", sheet.Rows[0]["b-01"]); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/LinearTextMeasurer.cs b/tests/RioJoy.Core.Tests/Overlay/LinearTextMeasurer.cs new file mode 100644 index 0000000..c154578 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/LinearTextMeasurer.cs @@ -0,0 +1,23 @@ +using RioJoy.Core.Overlay; + +namespace RioJoy.Core.Tests.Overlay; + +/// +/// Deterministic fake for layout tests: width grows +/// linearly with character count and font size, height with font size. Lets the +/// font-fit and placement math be asserted exactly without a real font/rasterizer. +/// +internal sealed class LinearTextMeasurer : ITextMeasurer +{ + private readonly double _charWidthPerPx; + private readonly double _heightPerPx; + + public LinearTextMeasurer(double charWidthPerPx = 0.6, double heightPerPx = 1.2) + { + _charWidthPerPx = charWidthPerPx; + _heightPerPx = heightPerPx; + } + + public TextExtent Measure(string text, string fontFamily, double fontSizePx) => + new(text.Length * fontSizePx * _charWidthPerPx, fontSizePx * _heightPerPx); +} diff --git a/tests/RioJoy.Core.Tests/Overlay/OverlayLayoutEngineTests.cs b/tests/RioJoy.Core.Tests/Overlay/OverlayLayoutEngineTests.cs new file mode 100644 index 0000000..0c66422 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/OverlayLayoutEngineTests.cs @@ -0,0 +1,143 @@ +using RioJoy.Core.Overlay; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +public class OverlayLayoutEngineTests +{ + // LinearTextMeasurer(0.6, 1.2): width = len*size*0.6, height = size*1.2. + private static readonly OverlayLayoutEngine Engine = new(new LinearTextMeasurer()); + + private static OverlayRegion Region( + string name = "b-00", double x = 0, double y = 0, double w = 200, double h = 40, double size = 10) => + new() { Name = name, X = x, Y = y, Width = w, Height = h, FontSizePx = size, FontFamily = "Sans" }; + + [Fact] + public void Place_TextFits_KeepsBaseSize_AndCenters() + { + // "FIRE" at size 10 -> 24 x 12, well inside 200 x 40. + PlacedLabel p = Engine.Place(Region(x: 10, y: 20), "FIRE"); + + Assert.Equal(10, p.FontSizePx, 6); + Assert.Equal(24, p.Width, 6); + Assert.Equal(12, p.Height, 6); + Assert.Equal(10 + (200 - 24) / 2, p.X, 6); // 98 + Assert.Equal(20 + (40 - 12) / 2, p.Y, 6); // 34 + } + + [Fact] + public void Place_Overflow_Fit_ShrinksFont() + { + // Narrow cell: "FIRE" at base 10 is 24 wide > 20 -> auto-fit shrinks it. + PlacedLabel p = Engine.Place(Region(w: 20, h: 40), "FIRE"); + + Assert.True(p.FontSizePx < 10); + Assert.True(p.Width <= 20, $"fitted width {p.Width} must be <= 20"); + } + + [Fact] + public void Place_Overflow_Crop_KeepsBaseSize() + { + var opts = new OverlayLayoutOptions { SizeMode = OverlaySizeMode.Crop }; + PlacedLabel p = Engine.Place(Region(w: 20, h: 40), "FIRE", opts); + + Assert.Equal(10, p.FontSizePx, 6); // not shrunk + Assert.Equal(24, p.Width, 6); // overflows the 20-wide cell + } + + [Theory] + [InlineData(OverlayHJust.Left, 0)] + [InlineData(OverlayHJust.Center, 44)] // (100-12)/2 + [InlineData(OverlayHJust.Right, 88)] // 100-12 + public void Place_HorizontalJustification(OverlayHJust hjust, double expectedX) + { + // "AB" at size 10 -> 12 x 12 inside a 100 x 20 cell at origin. + var opts = new OverlayLayoutOptions { HJust = hjust }; + PlacedLabel p = Engine.Place(Region(w: 100, h: 20), "AB", opts); + Assert.Equal(expectedX, p.X, 6); + } + + [Theory] + [InlineData(OverlayVJust.Top, 0)] + [InlineData(OverlayVJust.Center, 4)] // (20-12)/2 + [InlineData(OverlayVJust.Bottom, 8)] // 20-12 + public void Place_VerticalJustification(OverlayVJust vjust, double expectedY) + { + var opts = new OverlayLayoutOptions { VJust = vjust }; + PlacedLabel p = Engine.Place(Region(w: 100, h: 20), "AB", opts); + Assert.Equal(expectedY, p.Y, 6); + } + + [Fact] + public void Place_RegionOverride_BeatsEngineDefault() + { + OverlayRegion region = Region(w: 100, h: 20); + region.HJust = OverlayHJust.Right; + region.VJust = OverlayVJust.Top; + + var opts = new OverlayLayoutOptions { HJust = OverlayHJust.Left, VJust = OverlayVJust.Bottom }; + PlacedLabel p = Engine.Place(region, "AB", opts); + + Assert.Equal(88, p.X, 6); // Right wins over Left + Assert.Equal(0, p.Y, 6); // Top wins over Bottom + } + + [Fact] + public void Place_Rotated_CentersInCell_AndCarriesAngle() + { + // "HELLO" at size 10 -> 30 x 12; fits the swapped 200 x 50 envelope, so size holds. + OverlayRegion region = Region(w: 50, h: 200, size: 10); + region.RotationDegrees = 90; + + PlacedLabel p = Engine.Place(region, "HELLO"); + + Assert.Equal(90, p.RotationDegrees, 6); + Assert.Equal(10, p.FontSizePx, 6); + Assert.Equal((50 - 30) / 2.0, p.X, 6); // centered horizontally -> 10 + Assert.Equal((200 - 12) / 2.0, p.Y, 6); // centered vertically -> 94 + } + + [Fact] + public void Place_Rotated_FitsAgainstSwappedDimensions() + { + // A wide, short cell: upright the text fits height-bound; rotated it must fit + // the (swapped) narrow height, forcing a smaller font. + OverlayRegion upright = Region(w: 200, h: 20, size: 30); + OverlayRegion rotated = Region(w: 200, h: 20, size: 30); + rotated.RotationDegrees = 90; + + double uprightSize = Engine.Place(upright, "HELLO").FontSizePx; + double rotatedSize = Engine.Place(rotated, "HELLO").FontSizePx; + + Assert.True(rotatedSize < uprightSize, + $"rotated {rotatedSize} should be smaller than upright {uprightSize} (fit uses swapped axes)"); + } + + [Fact] + public void Layout_SkipsEmptyAndMissingLabels_PreservesRegionOrder() + { + var template = new OverlayTemplate + { + Regions = + { + Region("b-00"), + Region("b-01"), + Region("b-02"), + }, + }; + var labels = new Dictionary + { + ["b-00"] = "FIRE", + ["b-01"] = "", // empty -> skipped + ["b-02"] = "GEAR", + ["b-99"] = "ORPHAN", // no such region -> ignored + }; + + IReadOnlyList placed = Engine.Layout(template, labels); + + Assert.Equal(2, placed.Count); + Assert.Equal("b-00", placed[0].RegionName); + Assert.Equal("b-02", placed[1].RegionName); + Assert.Equal("GEAR", placed[1].Text); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/OverlayRenderIntegrationTests.cs b/tests/RioJoy.Core.Tests/Overlay/OverlayRenderIntegrationTests.cs new file mode 100644 index 0000000..c43e622 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/OverlayRenderIntegrationTests.cs @@ -0,0 +1,50 @@ +using RioJoy.Core.Overlay; +using RioJoy.Overlay; +using SkiaSharp; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +/// +/// Full Phase-7 pipeline on the real cockpit assets: extracted regions.json + the +/// exported riojoy.png + the legacy TEST.data labels → a rendered wallpaper. Proves +/// the extract → import → lay out → rasterize chain end to end (headless). +/// +public class OverlayRenderIntegrationTests +{ + private static bool RegionDiffers(SKBitmap a, SKBitmap b, OverlayRegion r) + { + int x0 = (int)r.X, y0 = (int)r.Y; + int x1 = Math.Min(a.Width, (int)(r.X + r.Width)); + int y1 = Math.Min(a.Height, (int)(r.Y + r.Height)); + for (int y = y0; y < y1; y++) + for (int x = x0; x < x1; x++) + if (a.GetPixel(x, y) != b.GetPixel(x, y)) + return true; + return false; + } + + [Fact] + public void RendersCockpitWallpaper_FromRealAssets() + { + OverlayTemplate template = OverlayTemplateStore.Load(TestRepo.CustomBackground("regions.json")); + Assert.False(string.IsNullOrWhiteSpace(template.BaseImagePath)); + + using SKBitmap baseImg = SKBitmap.Decode(TestRepo.CustomBackground(template.BaseImagePath!)); + Assert.NotNull(baseImg); + Assert.Equal(template.Width, baseImg.Width); + Assert.Equal(template.Height, baseImg.Height); + + IReadOnlyDictionary labels = + GoobieDataImporter.LabelsForRow(GoobieDataImporter.Load(TestRepo.CustomBackground("TEST.data"))); + + using SKBitmap rendered = new SkiaOverlayRenderer().Render(template, labels, baseImg); + + Assert.Equal(2720, rendered.Width); + Assert.Equal(600, rendered.Height); + + // A labeled cell ("b-00" -> "LSDI") must have changed from the base. + OverlayRegion b00 = template.FindRegion("b-00")!; + Assert.True(RegionDiffers(baseImg, rendered, b00), "labelled region b-00 should differ from the base image"); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/OverlayTemplateStoreTests.cs b/tests/RioJoy.Core.Tests/Overlay/OverlayTemplateStoreTests.cs new file mode 100644 index 0000000..d98b02d --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/OverlayTemplateStoreTests.cs @@ -0,0 +1,53 @@ +using RioJoy.Core.Overlay; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +public class OverlayTemplateStoreTests +{ + [Fact] + public void RoundTrips_GeometryAndOverrides() + { + var template = new OverlayTemplate + { + BaseImagePath = "cockpit.png", + Width = 1920, + Height = 1080, + Regions = + { + new OverlayRegion { Name = "b-00", X = 10, Y = 20, Width = 80, Height = 24, FontFamily = "Consolas", FontSizePx = 18 }, + new OverlayRegion { Name = "b-41", X = 5, Y = 7, Width = 60, Height = 30, HJust = OverlayHJust.Right, VJust = OverlayVJust.Bottom }, + }, + }; + + OverlayTemplate back = OverlayTemplateStore.Deserialize(OverlayTemplateStore.Serialize(template)); + + Assert.Equal(1920, back.Width); + Assert.Equal(2, back.Regions.Count); + + OverlayRegion? r0 = back.FindRegion("b-00"); + Assert.NotNull(r0); + Assert.Equal(80, r0!.Width, 6); + Assert.Equal("Consolas", r0.FontFamily); + Assert.Null(r0.HJust); + + OverlayRegion? r1 = back.FindRegion("B-41"); // case-insensitive lookup + Assert.NotNull(r1); + Assert.Equal(OverlayHJust.Right, r1!.HJust); + Assert.Equal(OverlayVJust.Bottom, r1.VJust); + } + + [Fact] + public void Serialize_WritesEnumsAsStrings() + { + var template = new OverlayTemplate + { + Regions = { new OverlayRegion { Name = "x", HJust = OverlayHJust.Center } }, + }; + + string json = OverlayTemplateStore.Serialize(template); + + Assert.Contains("\"Center\"", json); + Assert.DoesNotContain("\"HJust\": 1", json); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/ProfileWallpaperGeneratorTests.cs b/tests/RioJoy.Core.Tests/Overlay/ProfileWallpaperGeneratorTests.cs new file mode 100644 index 0000000..96cfa88 --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/ProfileWallpaperGeneratorTests.cs @@ -0,0 +1,46 @@ +using RioJoy.Core.Overlay; +using RioJoy.Overlay; +using SkiaSharp; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +public class ProfileWallpaperGeneratorTests +{ + [Fact] + public void Generate_WritesWallpaperPng_AtCanvasSize() + { + string templatePath = TestRepo.CustomBackground("regions.json"); + OverlayTemplate template = OverlayTemplateStore.Load(templatePath); + string templateDir = Path.GetDirectoryName(templatePath)!; + + var labels = new Dictionary { ["a-00"] = "DEMO", ["b-00"] = "FIRE" }; + string outPath = Path.Combine(Path.GetTempPath(), $"riojoy-wp-{Guid.NewGuid():N}.png"); + + try + { + string written = new ProfileWallpaperGenerator().Generate(template, templateDir, labels, outPath); + + Assert.Equal(outPath, written); + Assert.True(File.Exists(outPath)); + + using SKBitmap bmp = SKBitmap.Decode(outPath); + Assert.Equal(template.Width, bmp.Width); + Assert.Equal(template.Height, bmp.Height); + } + finally + { + if (File.Exists(outPath)) File.Delete(outPath); + } + } + + [Fact] + public void Generate_WithoutBaseImagePath_Throws() + { + var template = new OverlayTemplate { Width = 10, Height = 10 }; // BaseImagePath null + Assert.Throws(() => + new ProfileWallpaperGenerator().Generate( + template, ".", new Dictionary(), + Path.Combine(Path.GetTempPath(), $"x-{Guid.NewGuid():N}.png"))); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs b/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs new file mode 100644 index 0000000..89db36b --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/SkiaOverlayRendererTests.cs @@ -0,0 +1,82 @@ +using RioJoy.Core.Overlay; +using RioJoy.Overlay; +using SkiaSharp; +using Xunit; + +namespace RioJoy.Core.Tests.Overlay; + +public class SkiaOverlayRendererTests +{ + private static SKBitmap SolidBase(int w, int h, SKColor color) + { + var bmp = new SKBitmap(w, h); + using var canvas = new SKCanvas(bmp); + canvas.Clear(color); + canvas.Flush(); + return bmp; + } + + private static bool HasLightPixel(SKBitmap bmp, int x, int y, int w, int h) + { + for (int yy = y; yy < y + h; yy++) + for (int xx = x; xx < x + w; xx++) + { + SKColor p = bmp.GetPixel(xx, yy); + if (p.Red > 40 || p.Green > 40 || p.Blue > 40) + return true; + } + return false; + } + + private static OverlayRegion Region(string name, int x, int y, int w, int h) => + new() { Name = name, X = x, Y = y, Width = w, Height = h, FontFamily = "Arial", FontSizePx = 20, ColorHex = "#FFFFFF" }; + + [Fact] + public void Render_DrawsLabel_AndPreservesDimensions() + { + using SKBitmap baseImg = SolidBase(200, 100, SKColors.Black); + var template = new OverlayTemplate { Width = 200, Height = 100, Regions = { Region("b-00", 10, 10, 150, 50) } }; + var labels = new Dictionary { ["b-00"] = "FIRE" }; + + using SKBitmap output = new SkiaOverlayRenderer().Render(template, labels, baseImg); + + Assert.Equal(200, output.Width); + Assert.Equal(100, output.Height); + Assert.True(HasLightPixel(output, 10, 10, 150, 50), "white label text should appear inside the region"); + } + + [Fact] + public void Render_DoesNotMutateBaseImage() + { + using SKBitmap baseImg = SolidBase(120, 60, SKColors.Black); + var template = new OverlayTemplate { Regions = { Region("x", 5, 5, 100, 40) } }; + + using SKBitmap _ = new SkiaOverlayRenderer().Render(template, new Dictionary { ["x"] = "HI" }, baseImg); + + Assert.False(HasLightPixel(baseImg, 0, 0, 120, 60), "the source bitmap must be left untouched"); + } + + [Fact] + public void Render_RegionWithoutLabel_StaysBackground() + { + using SKBitmap baseImg = SolidBase(120, 60, SKColors.Black); + var template = new OverlayTemplate { Regions = { Region("x", 5, 5, 100, 40) } }; + + // No entry for "x" -> nothing drawn. + using SKBitmap output = new SkiaOverlayRenderer().Render(template, new Dictionary(), baseImg); + + Assert.False(HasLightPixel(output, 0, 0, 120, 60)); + } + + [Fact] + public void RenderToPng_ProducesValidPngSignature() + { + using SKBitmap baseImg = SolidBase(64, 64, SKColors.Black); + var template = new OverlayTemplate { Regions = { Region("x", 2, 2, 60, 30) } }; + + byte[] png = new SkiaOverlayRenderer().RenderToPng(template, new Dictionary { ["x"] = "HI" }, baseImg); + + Assert.True(png.Length > 8); + Assert.Equal(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, png[..8]); + } +} diff --git a/tests/RioJoy.Core.Tests/Overlay/TestRepo.cs b/tests/RioJoy.Core.Tests/Overlay/TestRepo.cs new file mode 100644 index 0000000..23ff62e --- /dev/null +++ b/tests/RioJoy.Core.Tests/Overlay/TestRepo.cs @@ -0,0 +1,16 @@ +namespace RioJoy.Core.Tests; + +/// Locates committed reference assets relative to the repo root. +internal static class TestRepo +{ + public static string Root() + { + DirectoryInfo? dir = new(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "RioJoy.sln"))) + dir = dir.Parent; + return dir?.FullName ?? throw new DirectoryNotFoundException("Could not locate repo root (RioJoy.sln)."); + } + + public static string CustomBackground(string file) => + Path.Combine(Root(), "docs", "reference", "customBackground", file); +} diff --git a/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj b/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj index 58ada06..650566e 100644 --- a/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj +++ b/tests/RioJoy.Core.Tests/RioJoy.Core.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/tools/XcfRegionExtract/Program.cs b/tools/XcfRegionExtract/Program.cs new file mode 100644 index 0000000..26b84c3 --- /dev/null +++ b/tools/XcfRegionExtract/Program.cs @@ -0,0 +1,133 @@ +using System.Globalization; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +// Extracts the cockpit label cells from the GIMP source art (riojoy.xcf) into a +// regions.json in OverlayTemplate shape: each named text layer becomes a region +// (Name = layer name, geometry = layer box, font/size from the gimp-text-layer +// parasite). Replaces eyeballing coordinates out of GIMP by hand. +// +// dotnet run --project tools/XcfRegionExtract [-- ] +// +// Defaults assume it is run from the repo root. XCF is big-endian; pointers are +// 32-bit for versions < 11. Only layer metadata is read (no tile/pixel data), so +// tile compression does not matter. + +string inPath = args.Length > 0 ? args[0] : "docs/reference/customBackground/riojoy.xcf"; +string outPath = args.Length > 1 ? args[1] : "docs/reference/customBackground/regions.json"; +// Base cockpit image (the wallpaper background), a sibling of regions.json. +string baseImage = args.Length > 2 ? args[2] : "riojoy.png"; + +byte[] d = File.ReadAllBytes(inPath); +int pos = 0; + +uint U32() { uint v = (uint)((d[pos] << 24) | (d[pos + 1] << 16) | (d[pos + 2] << 8) | d[pos + 3]); pos += 4; return v; } +int I32() => (int)U32(); +ulong U64() { ulong v = 0; for (int i = 0; i < 8; i++) v = (v << 8) | d[pos++]; return v; } + +string magic = Encoding.ASCII.GetString(d, 0, 9); // "gimp xcf " +string ver = Encoding.ASCII.GetString(d, 9, 4); // "file" (=v0) or "vNNN" +if (magic != "gimp xcf ") { Console.Error.WriteLine($"Not an XCF file: '{magic}'"); return 1; } +bool ptr64 = ver[0] == 'v' && int.TryParse(ver.AsSpan(1, 3), out int vn) && vn >= 11; +long Ptr() => ptr64 ? (long)U64() : U32(); + +pos = 14; +int width = (int)U32(), height = (int)U32(); +_ = U32(); // base type +Console.WriteLine($"xcf ver='{ver}' canvas={width}x{height}"); + +string XcfString() +{ + uint len = U32(); + if (len == 0) return ""; + string s = Encoding.UTF8.GetString(d, pos, (int)len - 1); // length includes trailing NUL + pos += (int)len; + return s; +} + +void SkipProps() +{ + while (true) { uint t = U32(); uint l = U32(); if (t == 0) break; pos += (int)l; } +} + +pos = 26; // header(14) + width(4) + height(4) + base(4) +SkipProps(); // image property list +var layerPtrs = new List(); +while (true) { long p = Ptr(); if (p == 0) break; layerPtrs.Add(p); } + +var fontRx = new Regex("\\(font \"([^\"]*)\"\\)"); +var sizeRx = new Regex("\\(font-size ([0-9.]+)"); +var colorRx = new Regex("\\(color \\(color-rgb ([0-9.]+) ([0-9.]+) ([0-9.]+)\\)\\)"); + +static string? ToHex(Match m) +{ + if (!m.Success) return null; + int Byte(int g) => (int)Math.Round(double.Parse(m.Groups[g].Value, CultureInfo.InvariantCulture) * 255); + return $"#{Byte(1):X2}{Byte(2):X2}{Byte(3):X2}"; +} + +// Cockpit-specific orientation: these cells' labels read 90° counter-clockwise +// (the vertical button column b-10..b-1F and the center title a-00). GIMP doesn't +// record a simple text-layer rotation, so it is applied here by name. +var rotateCcw90 = new HashSet(StringComparer.OrdinalIgnoreCase) { "a-00" }; +for (int a = 0x10; a <= 0x1F; a++) rotateCcw90.Add($"b-{a:X2}"); + +var regions = new List(); +foreach (long lp in layerPtrs) +{ + pos = (int)lp; + uint lw = U32(), lh = U32(); _ = U32(); // width, height, type + string name = XcfString(); + int dx = 0, dy = 0; string? parasite = null; + + while (true) + { + uint t = U32(); uint l = U32(); + if (t == 0) break; + int next = pos + (int)l; + if (t == 15) { dx = I32(); dy = I32(); } // PROP_OFFSETS + else if (t == 21) // PROP_PARASITES + { + while (pos < next) + { + string pname = XcfString(); + _ = U32(); // flags + uint psize = U32(); + string pdata = Encoding.UTF8.GetString(d, pos, (int)psize); pos += (int)psize; + if (pname == "gimp-text-layer") parasite = pdata; + } + } + pos = next; + } + + if (parasite is null) continue; // only text layers are label cells + string font = fontRx.Match(parasite) is { Success: true } fm ? fm.Groups[1].Value : "Sans"; + string sizeStr = sizeRx.Match(parasite) is { Success: true } sm ? sm.Groups[1].Value : "12"; + double sizePx = double.TryParse(sizeStr, NumberStyles.Float, CultureInfo.InvariantCulture, out double sp) ? sp : 12; + string? colorHex = ToHex(colorRx.Match(parasite)); + + regions.Add(new + { + Name = name, + X = dx, + Y = dy, + Width = (int)lw, + Height = (int)lh, + FontFamily = string.IsNullOrEmpty(font) ? "Sans" : font, + FontSizePx = sizePx, + ColorHex = colorHex, + RotationDegrees = rotateCcw90.Contains(name) ? 90.0 : (double?)null, + }); +} + +var template = new { BaseImagePath = baseImage, Width = width, Height = height, Regions = regions }; +var opts = new JsonSerializerOptions +{ + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, +}; +File.WriteAllText(outPath, JsonSerializer.Serialize(template, opts)); +Console.WriteLine($"Wrote {regions.Count} regions -> {outPath}"); +return 0; diff --git a/tools/XcfRegionExtract/README.md b/tools/XcfRegionExtract/README.md new file mode 100644 index 0000000..dcee43c --- /dev/null +++ b/tools/XcfRegionExtract/README.md @@ -0,0 +1,31 @@ +# XcfRegionExtract + +Extracts the cockpit label cells from the GIMP source art +([`docs/reference/customBackground/riojoy.xcf`](../../docs/reference/customBackground/riojoy.xcf)) +into a `regions.json` in `OverlayTemplate` shape — the geometry the Phase 7 +overlay generator (`RioJoy.Core.Overlay`) lays text into. + +Each **named text layer** becomes a region: `Name` = layer name (e.g. `b-00`, +`Throttle`), geometry = the layer's box (`PROP_OFFSETS` + width/height), and +`FontFamily`/`FontSizePx` from the layer's `gimp-text-layer` parasite. Only layer +metadata is read (no tile/pixel data), so XCF tile compression is irrelevant. + +## Run + +```cmd +dotnet run --project tools/XcfRegionExtract +``` + +Defaults read `docs/reference/customBackground/riojoy.xcf` and overwrite +`docs/reference/customBackground/regions.json` (run from the repo root). Override: + +```cmd +dotnet run --project tools/XcfRegionExtract -- path\to\in.xcf path\to\out.json +``` + +Re-run whenever the cockpit artwork changes. The committed `regions.json` is the +canonical extract; `OverlayTemplate.BaseImagePath` is left null and must point at +the exported base cockpit image used as the wallpaper background. + +> Supports XCF version 0 (`gimp xcf file`) through current; pointers are 64-bit at +> version ≥ 11, 32-bit otherwise. diff --git a/tools/XcfRegionExtract/XcfRegionExtract.csproj b/tools/XcfRegionExtract/XcfRegionExtract.csproj new file mode 100644 index 0000000..3553741 --- /dev/null +++ b/tools/XcfRegionExtract/XcfRegionExtract.csproj @@ -0,0 +1,10 @@ + + + Exe + net8.0 + enable + enable + XcfRegionExtract + + +