├── .editorconfig ├── .gitattributes ├── .gitignore ├── .yarnrc.yml ├── LICENSE ├── PackageDefinitions ├── spitice-ingamepanels-inputviewer.xml └── spitice-ingamepanels-inputviewer │ ├── ContentInfo │ └── Thumbnail.jpg │ └── ReleaseNotes.xml ├── PackageSources └── InGamePanels │ └── InGamePanel_InputViewer.xml ├── README.md ├── doc └── images │ ├── activating-input-viewer.jpg │ ├── auto-hide-title-bar.jpg │ ├── configuration-screen.jpg │ ├── input-viewer-main.jpg │ ├── input-viewer.jpg │ ├── number-display.jpg │ └── throttle-panel-only.jpg ├── input-viewer.xml ├── jest.config.js ├── package.json ├── src ├── InputViewer.scss ├── InputViewer.ts ├── inputViewer │ ├── __tests__ │ │ └── utils.test.ts │ ├── makePropMixToggleButton.ts │ ├── slice │ │ ├── epics.internal.ts │ │ ├── epics.ts │ │ ├── index.ts │ │ ├── models.ts │ │ └── slice.ts │ ├── uiElements.ts │ └── utils.ts ├── store.ts └── styles │ └── _utils.scss ├── static └── html_ui │ ├── InGamePanels │ └── InputViewer │ │ ├── InputViewer.html │ │ └── images │ │ ├── config.svg │ │ ├── mixture.svg │ │ └── propeller.svg │ └── icons │ └── toolbar │ └── ICON_TOOLBAR_INPUT_VIEWER.svg ├── test └── manual-testing.md ├── tsconfig.json ├── typings ├── _includeAll.d.ts └── msfs │ └── index.d.ts ├── webpack.config.js ├── webpack.dev.config.js ├── webpack.prod.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | 8 | end_of_line = crlf 9 | indent_style = space 10 | 11 | indent_size = 4 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .yarn 3 | .pnp.* 4 | node_modules 5 | dist 6 | _PackageInt 7 | 8 | Packages/* 9 | 10 | _copy-to-msfs.bat 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | yarnPath: ".yarn/releases/yarn-berry.cjs" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 spitice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PackageDefinitions/spitice-ingamepanels-inputviewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MISC 5 | Input Viewer 6 | 7 | Spitice 8 | 9 | 10 | false 11 | false 12 | 13 | 14 | 15 | ContentInfo 16 | 17 | false 18 | 19 | PackageDefinitions\spitice-ingamepanels-inputviewer\ContentInfo\ 20 | ContentInfo\spitice-ingamepanels-inputviewer\ 21 | 22 | 23 | Copy 24 | 25 | false 26 | 27 | dist\ 28 | html_ui\InGamePanels\InputViewer\ 29 | 30 | 31 | Copy 32 | 33 | false 34 | 35 | static\html_ui\ 36 | html_ui\ 37 | 38 | 39 | SPB 40 | 41 | false 42 | 43 | PackageSources\InGamePanels\ 44 | InGamePanels\ 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /PackageDefinitions/spitice-ingamepanels-inputviewer/ContentInfo/Thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/PackageDefinitions/spitice-ingamepanels-inputviewer/ContentInfo/Thumbnail.jpg -------------------------------------------------------------------------------- /PackageDefinitions/spitice-ingamepanels-inputviewer/ReleaseNotes.xml: -------------------------------------------------------------------------------- 1 | 2 | - Now compatible with MSFS 2024 3 | - Some performance improvements 4 | - Add support for helicopters added in Sim Update 11 5 | - Add throttle-only mode 6 | - Add auto-hide title bar 7 | - Zero values are now grayed out in Simple Number Display 8 | - Fix behavior on saving PropMix option 9 | - Fix to prevent quick-hide while panel is externalized 10 | - Now compatible with Sim Update 5 11 | - Improve performance on updating SimVars by using new API 12 | - Add config panel and auto-save config 13 | - Add simple and verbose number display 14 | - Add quick hide feature 15 | - Propeller/Mixture Bar will be automatically set for vanilla aircrafts in Standard edition 16 | - Lower the minimum size of the panel 17 | 18 | 19 | -------------------------------------------------------------------------------- /PackageSources/InGamePanels/InGamePanel_InputViewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | InGamePanel_InputViewer.spb 4 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Input Viewer - In-Game Panel addon for MSFS2020 3 | 4 | ![logo](doc/images/input-viewer.jpg) 5 | 6 | ---- 7 | 8 |

9 | Download Input Viewer on Flightsim.to 10 |

11 | 12 | ---- 13 | 14 | Do you want to inspect your controls without looking at internal 3D models like throttle levers? Or, you want to brag about how good you are at controlling aircraft in your recording videos? Well, here is an easy solution! 15 | 16 | This tiny addon adds an in-game panel that shows several axis inputs for MSFS. 17 | 18 | As some of you may know, this is a rip-off of **Controls Indicator** feature from DCS (Digital Combat Simulator). Perhaps, other flight sims might have a similar feature. 19 | 20 | This input viewer covers the following input axes: 21 | 22 | - Aileron input/trim position 23 | - Elevator input/trim position 24 | - Rudder input/trim position 25 | - Wheel brakes 26 | - Throttle #1/2/3/4 27 | - Propeller RPM #1 28 | - Mixture #1 29 | - (Prop RPM #2-4 and Mixture #2-4 are not included) 30 | 31 | for helicopters: 32 | 33 | - Cyclic input/trim position 34 | - Rudder (anti-torque pedals) position 35 | - Collective position 36 | - Throttle 37 | - Mixture 38 | - Rotor brake 39 | 40 | 41 | ## Installation 42 | 43 | Copy `spitice-ingamepanels-inputviewer` folder to community package folder. 44 | 45 | If you wonder what the community package folder is, googling for "msfs community package directory" might help. 46 | 47 | To uninstall, simply remove `spitice-ingamepanels-inputviewer` from your community package folder. 48 | 49 | 50 | ## Usage 51 | 52 | You should find a custom panel icon in your in-game toolbar. Click it to activate the panel. 53 | 54 | If it doesn't appear, access the list of in-game panels by clicking the gear icon, then toggle "INPUT VIEWER" on. It should add the panel icon to your toolbar. 55 | 56 | ![activation](doc/images/activating-input-viewer.jpg) 57 | 58 | The initial position of the panel might be the top right of your screen. Drag the header and the border to move and resize the panel as you want. 59 | 60 | ![main-screen](doc/images/input-viewer-main.jpg) 61 | 62 | ### Configuration 63 | 64 | Click the "gear" button located at the bottom left of the panel to open the configuration menu. 65 | 66 | ![configuration](doc/images/configuration-screen.jpg) 67 | 68 | 69 | ### Auto-hide Title Bar 70 | 71 | ***Default value: Disabled*** 72 | 73 | By enabling this feature, the title bar will automatically become invisible when your cursor goes outside of the panel. 74 | 75 | ![auto-hide title bar](doc/images/auto-hide-title-bar.jpg) 76 | 77 | ### Number Display 78 | 79 | ***Default value: None*** 80 | 81 | InputViewer contains two types of number display for each axis. Default is none, so you need to manually activate this feature via configuration. 82 | 83 | ![number display](doc/images/number-display.jpg) 84 | 85 | Perhaps, you will notice that trim numbers in the simple number display sometimes showing two different types of zero: "0" and "0.0". When you tweak those values via keys or buttons, not via axis, sometimes it causes a rounding error, in which the fraction digits would not be completely zeroed out thus it becomes a very small number. As a result, the simple number display shows "0" for the perfect zero value and "0.0" for those "almost zero" values. 86 | 87 | ### Show Panels 88 | 89 | ***Default value: All*** 90 | 91 | This feature is added for users who don't need all types of inputs to be visible. For now, you can select "Throttle" to hide the stick and rudder (right side of the panel) so your input viewer will be smaller. Use this feature in conjunction with "Auto-hide Title Bar" for more effectiveness. 92 | 93 | ![throttle panel only](doc/images/throttle-panel-only.jpg) 94 | 95 | ### Quick Hide 96 | 97 | ***Default duration: 2 seconds*** 98 | 99 | By double-clicking inside of the panel, you can temporarily hide it so it won't ruin your masterpiece when you take screenshots! The duration for quick hiding is two seconds by default. You can choose 1, 2, or 3 seconds as the duration or make the feature completely disabled in the configuration screen. 100 | 101 | ### Propeller/Mixture Bar 102 | 103 | ***Default value: Automatic for Aircrafts in Standard edition*** 104 | 105 | As of v1.1, you can tweak the visibility of the propeller/mixture bar in the configuration screen and the addon will remember which option is chosen for each aircraft model. By default, this option will be automatically set for those aircraft bundled with MSFS Standard edition. 106 | 107 | ## Repository 108 | 109 | Feel free to fork it and try to make your own input viewer. Any bug reports and suggestions are also welcome :) 110 | 111 | https://github.com/spitice/msfs-input-viewer 112 | 113 | 114 | ## Acknowledgements 115 | 116 | - [msfs2020-toolbar-window-template](https://github.com/bymaximus/msfs2020-toolbar-window-template) 117 | - [msfs-webui-devkit](https://github.com/dga711/msfs-webui-devkit) 118 | 119 | ## Changelogs 120 | 121 | ### v1.4.0 (November 21, 2024) 122 | 123 | - Now compatible with MSFS 2024 124 | - Some performance optimizations 125 | 126 | ### v1.3.0 (November 22, 2022) 127 | 128 | - Add support for helicopters added in Sim Update 11 129 | 130 | ### v1.2.1 (July 28, 2021) 131 | 132 | - Now compatible with Sim Update 5 133 | - Improve performance on updating SimVars by using new API 134 | 135 | ### v1.2.0 (July 4, 2021) 136 | 137 | - Add throttle-only mode 138 | - Add auto-hide title bar 139 | - Zero values are now grayed out in Simple Number Display 140 | - Fix behavior on saving PropMix option 141 | - Fix to prevent quick-hide while panel is externalized 142 | 143 | ### v1.1.0 (June 27, 2021) 144 | 145 | - Add config panel and auto-save config 146 | - Add simple and verbose number display 147 | - Add quick hide feature 148 | - Propeller/Mixture Bar will be automatically set for vanilla aircrafts in Standard edition 149 | - Lower the minimum size of the panel 150 | 151 | ### v1.0.0 (May 21, 2021) 152 | 153 | Initial release. 154 | -------------------------------------------------------------------------------- /doc/images/activating-input-viewer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/activating-input-viewer.jpg -------------------------------------------------------------------------------- /doc/images/auto-hide-title-bar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/auto-hide-title-bar.jpg -------------------------------------------------------------------------------- /doc/images/configuration-screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/configuration-screen.jpg -------------------------------------------------------------------------------- /doc/images/input-viewer-main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/input-viewer-main.jpg -------------------------------------------------------------------------------- /doc/images/input-viewer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/input-viewer.jpg -------------------------------------------------------------------------------- /doc/images/number-display.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/number-display.jpg -------------------------------------------------------------------------------- /doc/images/throttle-panel-only.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spitice/msfs-input-viewer/5f584da4d7c185c4c8e52cf622c3f53a81301406/doc/images/throttle-panel-only.jpg -------------------------------------------------------------------------------- /input-viewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | . 4 | _PackageInt 5 | _PublishingGroupInt 6 | 7 | PackageDefinitions\spitice-ingamepanels-inputviewer.xml 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 2 | const { pathsToModuleNameMapper } = require("ts-jest/utils"); 3 | const { compilerOptions } = require("./tsconfig.json"); 4 | 5 | /** @type {import("ts-jest/dist/types").InitialOptionsTsJest} */ 6 | module.exports = { 7 | preset: 'ts-jest', 8 | testEnvironment: 'node', 9 | 10 | // The glob patterns Jest uses to detect test files 11 | testMatch: [ 12 | "**/__tests__/*.test.+(ts|tsx|js)" 13 | ], 14 | 15 | moduleNameMapper: pathsToModuleNameMapper( 16 | compilerOptions.paths, { 17 | prefix: "/" 18 | } 19 | ), 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msfs-input-viewer", 3 | "private": true, 4 | "scripts": { 5 | "build": "run-s clean compile copy-static", 6 | "build-msfs": "\"$MSFS_SDK/Tools/bin/fspackagetool.exe\" input-viewer.xml -nomirroring", 7 | "clean": "rimraf _PackageInt dist Packages/spitice-ingamepanels-inputviewer", 8 | "compile": "webpack --config ./webpack.prod.config.js", 9 | "copy-static": "cpx 'static/**/*' 'Packages/spitice-ingamepanels-inputviewer'", 10 | "watch": "run-p watch-static watch-webpack", 11 | "watch-webpack": "webpack --watch --config ./webpack.dev.config.js", 12 | "watch-static": "cpx 'static/**/*' 'Packages/spitice-ingamepanels-inputviewer' --watch --no-initial --verbose" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^26.0.23", 16 | "@yarnpkg/pnpify": "^3.0.0-rc.3", 17 | "cpx": "^1.5.0", 18 | "css-loader": "^5.2.4", 19 | "filemanager-webpack-plugin": "^5.0.0", 20 | "jest": "^26.6.3", 21 | "mini-css-extract-plugin": "^1.6.0", 22 | "npm-run-all": "^4.1.5", 23 | "pnp-webpack-plugin": "^1.6.4", 24 | "rimraf": "^3.0.2", 25 | "sass": "^1.32.13", 26 | "sass-loader": "^11.1.1", 27 | "terser-webpack-plugin": "^5.1.2", 28 | "ts-jest": "^26.5.6", 29 | "ts-loader": "^9.1.2", 30 | "typescript": "^4.2.4", 31 | "webpack": "^5.37.0", 32 | "webpack-cli": "^4.7.0" 33 | }, 34 | "dependencies": { 35 | "@reduxjs/toolkit": "^1.6.0", 36 | "redux-observable": "next", 37 | "rxjs": "^7.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/InputViewer.scss: -------------------------------------------------------------------------------- 1 | 2 | @use "sass:selector"; 3 | @use "./styles/utils" as *; 4 | 5 | @function scaledWidget($px) { 6 | @return calc(var(--widgetScale) * #{$px}); 7 | } 8 | 9 | @mixin externalized() { 10 | @at-root body.extern & { 11 | @content; 12 | } 13 | } 14 | @mixin panelsToShow($value) { 15 | @at-root #InputViewer[data-panels=#{$value}] & { 16 | @content; 17 | } 18 | } 19 | @mixin throttlePanelMode($mode) { 20 | @at-root #InputViewer[data-throttle-panel-mode=#{$mode}] & { 21 | @content; 22 | } 23 | } 24 | @mixin numberDisplayType($type) { 25 | @at-root #InputViewer[data-number-display-type=#{$type}] & { 26 | @content; 27 | } 28 | } 29 | @mixin panelsAndNumberDisplayType($panels, $numberDisplayType) { 30 | @at-root #InputViewer[data-panels=#{$panels}][data-number-display-type=#{$numberDisplayType}] & { 31 | @content; 32 | } 33 | } 34 | @mixin forHelicopter() { 35 | @at-root #InputViewer[data-category="helicopter"] & { 36 | @content; 37 | } 38 | } 39 | 40 | @mixin fill { 41 | position: absolute; 42 | width: 100%; 43 | height: 100%; 44 | overflow: hidden; 45 | } 46 | 47 | 48 | html, body { 49 | margin: 0; 50 | padding: 0; 51 | box-sizing: border-box; 52 | } 53 | *, *:before, *:after { 54 | box-sizing: inherit; 55 | } 56 | 57 | #InputViewer_UIFrame { 58 | z-index: 0; 59 | height: 100vh !important; 60 | 61 | > ingame-ui-header { 62 | z-index: -1; 63 | position: relative; 64 | 65 | transition: opacity var(--animationTimeFast) var(--animationEffect); 66 | } 67 | 68 | > ui-element { 69 | z-index: -2; 70 | position: relative; 71 | } 72 | 73 | #InputViewer_UIFrame .ingameUiWrapper { 74 | position: relative; 75 | } 76 | } 77 | 78 | #InputViewer { 79 | --widgetScale: 1px; // placeholder 80 | 81 | --headerHeight: #{scaled(84)}; 82 | --margin: #{scaled(6)}; 83 | 84 | --wrapperWidth: calc(100vw - var(--margin) * 2); 85 | 86 | --wrapperHeightWithoutHeader: calc(100vh - var(--margin) * 2); 87 | --wrapperHeightWithHeader: calc(var(--wrapperHeightWithoutHeader) - var(--headerHeight) - var(--margin)); 88 | 89 | --wrapperHeight: var(--wrapperHeightWithHeader); 90 | --wrapperOffsetY: 0px; 91 | 92 | @include externalized() { 93 | --wrapperHeight: var(--wrapperHeightWithoutHeader); 94 | } 95 | &[data-auto-hide-header=true] { 96 | --wrapperHeight: var(--wrapperHeightWithoutHeader); 97 | 98 | @at-root body:not(.extern) & { 99 | --wrapperOffsetY: calc(0px - var(--headerHeight) - var(--margin)); 100 | } 101 | 102 | ingame-ui-header { 103 | opacity: 0; 104 | transition-delay: 0.2s; 105 | } 106 | ingame-ui.minimized { 107 | ingame-ui-header { 108 | opacity: 1; // When minimized, the header should always be visible 109 | } 110 | } 111 | @at-root #{selector.unify(&, "#InputViewer[data-config-opened=true]")} { 112 | ingame-ui-header { 113 | opacity: 1; // While the config panel is opened, the header should always be visible 114 | } 115 | } 116 | @at-root body:hover & { 117 | ingame-ui-header { 118 | opacity: 1; 119 | transition-delay: 0.0s; 120 | } 121 | } 122 | } 123 | 124 | #Wrapper { 125 | --panelGridWidth: var(--wrapperWidth); 126 | --panelGridHeight: var(--wrapperHeight); 127 | 128 | --popupWidth: var(--wrapperWidth); 129 | --popupHeight: calc(var(--wrapperHeight) + var(--wrapperOffsetY)); 130 | 131 | width: var(--wrapperWidth); 132 | height: var(--wrapperHeight); 133 | top: var(--wrapperOffsetY); 134 | 135 | #PanelGrid { 136 | width: var(--panelGridWidth); 137 | height: var(--panelGridHeight); 138 | 139 | --leftWidgetWidth: #{scaledWidget(60)}; 140 | --rightWidgetWidth: #{scaledWidget(220)}; 141 | --topWidgetHeight: #{scaledWidget(220)}; 142 | --bottomWidgetHeight: #{scaledWidget(40)}; 143 | } 144 | #PanelOverlay { 145 | width: var(--panelGridWidth); 146 | height: var(--panelGridHeight); 147 | } 148 | #Popup { 149 | width: var(--popupWidth); 150 | height: var(--popupHeight); 151 | top: calc(0px - var(--wrapperOffsetY)); 152 | } 153 | } 154 | } 155 | 156 | #Wrapper { 157 | position: absolute; 158 | z-index: 0; 159 | 160 | #PanelGrid { 161 | position: absolute; 162 | z-index: 1; 163 | } 164 | #PanelOverlay { 165 | position: absolute; 166 | z-index: 2; 167 | pointer-events: none; 168 | } 169 | #Popup { 170 | position: absolute; 171 | z-index: 3; 172 | pointer-events: none; 173 | } 174 | } 175 | 176 | #Wrapper { 177 | #PanelGrid { 178 | display: grid; 179 | grid-template-columns: var(--leftWidgetWidth) var(--rightWidgetWidth); 180 | grid-template-rows: var(--topWidgetHeight) var(--bottomWidgetHeight); 181 | grid-template-areas: 182 | "throttle stick" 183 | "misc rudder"; 184 | justify-content: space-evenly; // x-axis 185 | align-content: center; // y-axis 186 | 187 | background-color: rgba(0, 0, 0, 0.1); 188 | 189 | #StickPanel { 190 | grid-area: stick; 191 | } 192 | #RudderPanel { 193 | grid-area: rudder; 194 | } 195 | #ThrottlePanel { 196 | grid-area: throttle; 197 | } 198 | #MiscPanel { 199 | grid-area: misc; 200 | } 201 | 202 | #StickPanel #NumberDisp_Simple_Container > svg.throttleContainer { 203 | left: #{scaledWidget(15)}; 204 | } 205 | 206 | @include panelsToShow("throttle") { 207 | grid-template-columns: var(--leftWidgetWidth); 208 | grid-template-areas: 209 | "throttle" 210 | "misc"; 211 | 212 | #StickPanel, 213 | #RudderPanel { 214 | display: none; 215 | } 216 | } 217 | 218 | @include panelsAndNumberDisplayType("throttle", "simple") { 219 | grid-template-columns: var(--leftWidgetWidth) #{scaledWidget(60)}; 220 | grid-template-areas: 221 | "throttle stick" 222 | "misc misc"; 223 | 224 | #StickPanel { 225 | display: inline; 226 | 227 | > svg, 228 | #NumberDisp_Simple_Container > svg { 229 | display: none; 230 | } 231 | #NumberDisp_Simple_Container > svg.throttleContainer { 232 | display: inline; 233 | left: 0; 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | #DevOverlay { 241 | @include fill(); 242 | z-index: 999; 243 | pointer-events: none; 244 | 245 | > .error { 246 | background-color: crimson; 247 | } 248 | } 249 | 250 | #ConfigPopup_Container { 251 | @include fill(); 252 | z-index: 100; 253 | 254 | pointer-events: auto; 255 | 256 | background-color: rgba(0, 0, 0, 0.7); 257 | 258 | display: flex; 259 | flex-direction: column; 260 | 261 | @at-root #InputViewer:not([data-config-opened=true]) & { 262 | display: none; 263 | } 264 | 265 | font-weight: bold; 266 | 267 | #Config_ScrollCont { 268 | flex: 1 1 auto; 269 | } 270 | 271 | new-list-button { 272 | // Make it slightly slim 273 | --optionContentWidth: #{scaled(350)}; // 500 for .condensedPanel, 700 for default 274 | } 275 | 276 | .sectionTitle { 277 | padding: scaled(16) scaled(16) scaled(8); 278 | 279 | font-weight: normal; 280 | 281 | &:not(:first-child) { 282 | margin-top: scaled(16); 283 | } 284 | } 285 | 286 | .info { 287 | display: flex; 288 | flex-direction: row; 289 | justify-content: flex-start; 290 | background-color: var(--backgroundColorPanel); 291 | 292 | $size: 96; 293 | 294 | & > .icon { 295 | flex: 0 0 scaled($size); 296 | 297 | display: flex; 298 | flex-direction: row; 299 | align-items: center; 300 | 301 | background-color: var(--backgroundColorPanel); 302 | 303 | icon-element { 304 | --width: #{scaled($size)}; 305 | --height: #{scaled($size)}; 306 | } 307 | } 308 | 309 | & > .description { 310 | display: flex; 311 | justify-content: flex-start; 312 | align-items: center; 313 | 314 | overflow: hidden; 315 | padding: scaled(12); 316 | 317 | font-weight: normal; 318 | } 319 | } 320 | 321 | #Config_Close { 322 | margin: var(--halfMargin); 323 | } 324 | } 325 | 326 | #NumberDisp_Verbose_Container { 327 | @include fill(); 328 | 329 | display: flex; 330 | justify-content: center; 331 | align-items: center; 332 | 333 | z-index: 50; 334 | pointer-events: none; 335 | 336 | .numberDispGrid { 337 | display: grid; 338 | grid-template-columns: 1fr 2fr; 339 | 340 | div { 341 | padding: 0 scaled(16); 342 | 343 | font-size: calc(var(--fontSizeParagraph) * 0.75); 344 | 345 | background-color: var(--color-black-op20); 346 | 347 | &:nth-child(4n+1), 348 | &:nth-child(4n+2) { 349 | background-color: var(--color-black-op30); 350 | } 351 | } 352 | } 353 | } 354 | 355 | #NumberDisp_Simple_Container, 356 | #NumberDisp_Verbose_Container { 357 | display: none; 358 | } 359 | 360 | #NumberDisp_Simple_Container { 361 | @include numberDisplayType("simple") { 362 | display: inline; 363 | } 364 | } 365 | #NumberDisp_Verbose_Container { 366 | @include numberDisplayType("verbose") { 367 | display: flex; 368 | } 369 | } 370 | 371 | 372 | #StickPanel { 373 | > * { 374 | position: absolute; 375 | width: var(--rightWidgetWidth); 376 | height: var(--topWidgetHeight); 377 | } 378 | 379 | #NumberDisp_Simple_Container { 380 | > * { 381 | position: absolute; 382 | } 383 | } 384 | } 385 | 386 | #NumberDisp_Simple_Container { 387 | opacity: 0.8; 388 | 389 | .bg { 390 | .name { 391 | fill: white; 392 | } 393 | .primary { 394 | fill: #3E3E3E; 395 | } 396 | .secondary { 397 | fill: #818181; 398 | } 399 | .aileronTip { 400 | fill: #DA0000; 401 | } 402 | .elevatorTip { 403 | fill: #00C200; 404 | } 405 | .rudderTip { 406 | fill: #0000C2; 407 | } 408 | .brakeTip { 409 | fill: #EEB000; 410 | } 411 | .propellerPrimary { 412 | fill: #004DC7; 413 | @include forHelicopter() { 414 | fill: #818181; 415 | } 416 | } 417 | .mixturePrimary { 418 | fill: #B1352A; 419 | } 420 | } 421 | .label { 422 | text { 423 | font-size: 7px; 424 | text-anchor: middle; 425 | dominant-baseline: central; 426 | } 427 | .name { 428 | font-weight: 800; 429 | fill: rgba(0, 0, 0, 0.7); 430 | } 431 | .numberDisp { 432 | fill: white; 433 | &.zero { 434 | fill: rgba(255, 255, 255, 0.5); 435 | } 436 | } 437 | } 438 | 439 | .throttleContainer { 440 | > g { 441 | display: none; 442 | } 443 | } 444 | @include throttlePanelMode("SingleEngine") { 445 | .throttle1Container { 446 | display: inline; 447 | } 448 | } 449 | @include throttlePanelMode("TwinEngine") { 450 | .throttle1Container, 451 | .throttle2Container { 452 | display: inline; 453 | } 454 | } 455 | @include throttlePanelMode("ThreeEngine") { 456 | .throttle1Container, 457 | .throttle2Container, 458 | .throttle3Container { 459 | display: inline; 460 | } 461 | } 462 | @include throttlePanelMode("FourEngine") { 463 | .throttle1Container, 464 | .throttle2Container, 465 | .throttle3Container, 466 | .throttle4Container { 467 | display: inline; 468 | } 469 | } 470 | @include throttlePanelMode("PropMix") { 471 | .throttle1Container, 472 | .propeller1Container, 473 | .mixture1Container { 474 | display: inline; 475 | } 476 | } 477 | } 478 | 479 | toggle-button.togglePropMix { 480 | @function handleOffset($factor) { 481 | // More factor, more left 482 | @return var(--leftPos, #{scaled(72 - 12 * $factor - 4)}); 483 | } 484 | 485 | icon-element { 486 | position: absolute; 487 | 488 | top: scaled(2); 489 | width: scaled(48); 490 | height: scaled(48); 491 | 492 | transition: basic-transition(left, opacity); 493 | 494 | &#PropellerIcon { 495 | left: handleOffset(1.15); // Add 0.15 to move it to left slightly 496 | } 497 | &#MixtureIcon { 498 | left: handleOffset(0); 499 | } 500 | } 501 | 502 | .ToggleButton .toggleButtonWrapper .toggleButton .state { 503 | left: handleOffset(2); 504 | transition: basic-transition(left, background-color); 505 | } 506 | 507 | &:not(.off) { 508 | .ToggleButton .toggleButtonWrapper .toggleButton .state { 509 | background-color: #4F4F4F; 510 | } 511 | } 512 | 513 | &.off { 514 | icon-element { 515 | opacity: 0; 516 | } 517 | } 518 | } 519 | 520 | #MiscPanel { 521 | display: flex; 522 | justify-content: center; 523 | align-items: center; 524 | 525 | #OpenConfig { 526 | // Default = scaled(60) 527 | --width: #{scaled(90)}; 528 | --height: #{scaled(60)}; 529 | } 530 | } 531 | 532 | #PanelGrid { 533 | .grad1, .grad2, .grad3 { 534 | fill: none; 535 | stroke: var(--primaryColor); 536 | } 537 | .grad1 { 538 | stroke-width: 3; 539 | } 540 | .grad2 { 541 | stroke-width: 1; 542 | } 543 | .grad3 { 544 | stroke-width: 1; 545 | opacity: 0.5; 546 | } 547 | 548 | .inputPos { 549 | fill: none; 550 | stroke: white; 551 | stroke-width: 3; 552 | } 553 | .trimPos { 554 | stroke: white; 555 | stroke-width: 1; 556 | } 557 | 558 | #RudderPanel { 559 | .wheelBrakeBar { 560 | .fg { 561 | fill: orange; 562 | } 563 | .cap { 564 | stroke: var(--primaryColor); 565 | stroke-width: 1; 566 | } 567 | } 568 | } 569 | 570 | #ThrottlePanel { 571 | .throttleBar { 572 | .fg { 573 | fill: white; 574 | } 575 | .cap { 576 | stroke: var(--primaryColor); 577 | stroke-width: 0.5; // Compensates x2 scaling in y-axis 578 | } 579 | 580 | &.propeller { 581 | .fg { 582 | fill: skyblue; 583 | @include forHelicopter() { 584 | fill: #DDDDDD; 585 | } 586 | } 587 | } 588 | &.mixture { 589 | .fg { 590 | fill: salmon; 591 | } 592 | } 593 | } 594 | 595 | #ThrottleBars, 596 | #ThrottleBordersV, 597 | #ThrottleBordersH { 598 | & > * { 599 | display: none; 600 | } 601 | } 602 | 603 | @mixin showThrottleBars($n, $is-prop-mixture: false) { 604 | // Translate 605 | #ThrottleBars, 606 | #ThrottleBordersV { 607 | transform: translate($n * -5px, 0); 608 | } 609 | 610 | // Toggle visibility 611 | @for $i from 1 through $n { 612 | @if $is-prop-mixture { 613 | @if $i == 1 { 614 | #ThrottleBar_#{$i}, 615 | #PropellerBar_#{$i}, 616 | #MixtureBar_#{$i} { 617 | display: inline; 618 | } 619 | } 620 | } @else { 621 | #ThrottleBar_#{$i} { 622 | display: inline; 623 | } 624 | } 625 | 626 | #ThrottleBordersV > *:nth-child(#{$i}) { 627 | display: inline; 628 | } 629 | } 630 | #ThrottleBordersV > *:nth-child(#{$n + 1}) { 631 | display: inline; 632 | } 633 | #ThrottleBordersH > *:nth-child(#{$n}) { 634 | display: inline; 635 | } 636 | } 637 | 638 | @include throttlePanelMode("SingleEngine") { 639 | @include showThrottleBars(1); 640 | } 641 | @include throttlePanelMode("TwinEngine") { 642 | @include showThrottleBars(2); 643 | } 644 | @include throttlePanelMode("ThreeEngine") { 645 | @include showThrottleBars(3); 646 | } 647 | @include throttlePanelMode("FourEngine") { 648 | @include showThrottleBars(4); 649 | } 650 | @include throttlePanelMode("PropMix") { 651 | @include showThrottleBars(3, true); 652 | } 653 | } 654 | } 655 | -------------------------------------------------------------------------------- /src/InputViewer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { store } from "./store"; 3 | import { 4 | appendDebugMsg, 5 | debugMsg, 6 | } from "./inputViewer/utils"; 7 | import { makePropMixToggleButton } from "./inputViewer/makePropMixToggleButton"; 8 | import { inputViewerActions as A } from "./inputViewer/slice"; 9 | import { 10 | BarElements, 11 | NumberDisplayElements, 12 | UIElements, 13 | } from "./inputViewer/uiElements"; 14 | 15 | 16 | import "./InputViewer.scss"; 17 | 18 | class InputViewerElement extends TemplateElement implements IUIElement { 19 | 20 | _isConnected: boolean = false; 21 | 22 | _el!: { 23 | uiFrame: ingameUiElement, 24 | panelGrid: HTMLElement, 25 | openConf: HTMLElement, 26 | }; 27 | 28 | constructor() { 29 | super(); 30 | } 31 | 32 | connectedCallback() { 33 | this._isConnected = true; 34 | 35 | const find = ( id: string ) => { 36 | const query = "#" + id; 37 | const el = this.querySelector( query ) as HTMLElement; 38 | if ( !el ) { 39 | throw new Error( query + " not found" ); 40 | } 41 | return el; 42 | }; 43 | 44 | const findBar = ( id: string ): BarElements => { 45 | const parent = find( id ); 46 | return { 47 | fg: parent.querySelector( ".fg" )!, 48 | cap: parent.querySelector( ".cap" )!, 49 | }; 50 | }; 51 | 52 | const findNumberDisplay = ( id: string, component: "name" | "label" | "numberDisp" ): NumberDisplayElements => { 53 | const parent = find( id ); 54 | const findNum = ( className: string ) => { 55 | const query = "." + className; 56 | const el = parent.querySelector( `.${component}.${className}` ) as HTMLElement; 57 | if ( !el ) { 58 | throw new Error( query + " not found" ); 59 | } 60 | return el; 61 | }; 62 | 63 | return { 64 | aileron: findNum( "aileron" ), 65 | aileronTrim: findNum( "aileronTrim" ), 66 | elevator: findNum( "elevator" ), 67 | elevatorTrim: findNum( "elevatorTrim" ), 68 | rudder: findNum( "rudder" ), 69 | rudderTrim: findNum( "rudderTrim" ), 70 | brakeLeft: findNum( "brakeLeft" ), 71 | brakeRight: findNum( "brakeRight" ), 72 | throttle1: findNum( "throttle1" ), 73 | throttle2: findNum( "throttle2" ), 74 | throttle3: findNum( "throttle3" ), 75 | throttle4: findNum( "throttle4" ), 76 | propeller1: findNum( "propeller1" ), 77 | mixture1: findNum( "mixture1" ), 78 | }; 79 | }; 80 | 81 | const elUIFrame = find( "InputViewer_UIFrame" ) as ingameUiElement; 82 | const elPanelGrid = find( "PanelGrid" ); 83 | const elOpenConf = find( "OpenConfig" ); 84 | const elConfClose = find( "Config_Close" ); 85 | const elConfScroll = find( "Config_ScrollCont" ); 86 | const elConfAutoHide = find( "Config_AutoHideHeader" ) as ToggleButtonElement; 87 | const elConfPanels = find( "Config_Panels" ) as NewListButtonElement; 88 | const elConfNumericDisp = find( "Config_NumericDisp" ) as NewListButtonElement; 89 | const elConfQHideDur = find( "Config_QuickHideDuration" ) as NewListButtonElement; 90 | const elConfPropMix = find( "Config_TogglePropMix" ) as ToggleButtonElement; 91 | 92 | this._el = { 93 | uiFrame: elUIFrame, 94 | panelGrid: elPanelGrid, 95 | openConf: elOpenConf, 96 | }; 97 | 98 | elOpenConf.addEventListener( "OnValidate", e => { 99 | // Open the config popup 100 | this.setAttribute("data-config-opened", "true"); 101 | (elConfScroll as any).delayedUpdateSizes(); 102 | } ); 103 | elConfClose.addEventListener( "OnValidate", e => { 104 | // Close the config popup 105 | this.setAttribute("data-config-opened", "false"); 106 | } ); 107 | 108 | elConfAutoHide.addEventListener( "OnValidate", e => { 109 | store.dispatch( A.setAutoHideHeader( elConfAutoHide.getValue() ) ); 110 | } ); 111 | 112 | makePropMixToggleButton( elConfPropMix ); 113 | elConfPropMix.addEventListener( "OnValidate", e => { 114 | store.dispatch( A.setEnablePropMixBar( elConfPropMix.getValue() ) ); 115 | } ); 116 | 117 | elConfPanels.addEventListener( "OnValidate", e => { 118 | const input = elConfPanels; 119 | const choice = input.choices[input.getCurrentValue()]; 120 | store.dispatch( A.setPanelsToShow( choice as any ) ); 121 | } ); 122 | elConfNumericDisp.addEventListener( "OnValidate", e => { 123 | const input = elConfNumericDisp; 124 | const choice = input.choices[input.getCurrentValue()]; 125 | store.dispatch( A.setNumberDisplayType( choice as any ) ); 126 | } ); 127 | elConfQHideDur.addEventListener( "OnValidate", e => { 128 | const input = elConfQHideDur; 129 | const value = input.getCurrentValue(); 130 | store.dispatch( A.setQuickHideDuration( value ) ); 131 | } ); 132 | 133 | UIElements.el = { 134 | root: this, 135 | uiFrame: elUIFrame, 136 | 137 | main: { 138 | stick: find( "StickInputPos" ), 139 | stickTrim: find( "StickTrimPos" ), 140 | rudder: find( "RudderInputPos" ), 141 | rudderTrim: find( "RudderTrimPos" ), 142 | brakeLeft: findBar( "WheelBrakeBar_Left" ), 143 | brakeRight: findBar( "WheelBrakeBar_Right" ), 144 | throttle1: findBar( "ThrottleBar_1" ), 145 | throttle2: findBar( "ThrottleBar_2" ), 146 | throttle3: findBar( "ThrottleBar_3" ), 147 | throttle4: findBar( "ThrottleBar_4" ), 148 | propeller1: findBar( "PropellerBar_1" ), 149 | mixture1: findBar( "MixtureBar_1" ), 150 | }, 151 | numberSimpleLabel: findNumberDisplay( "NumberDisp_Simple_Container", "name" ), 152 | numberSimple: findNumberDisplay( "NumberDisp_Simple_Container", "numberDisp" ), 153 | numberVerboseLabel: findNumberDisplay( "NumberDisp_Verbose_Container", "label" ), 154 | numberVerbose: findNumberDisplay( "NumberDisp_Verbose_Container", "numberDisp" ), 155 | 156 | confAutoHideHeader: elConfAutoHide, 157 | confPanels: elConfPanels, 158 | confNumericDisp: elConfNumericDisp, 159 | confQuickHideDuration: elConfQHideDur, 160 | confPropMix: elConfPropMix, 161 | confAircraftModel: find( "Config_AircraftModel" ), 162 | }; 163 | 164 | // Set up update loop 165 | const updateLoop = () => { 166 | if ( !this._isConnected ) { 167 | return; 168 | } 169 | this._onUpdate(); 170 | 171 | requestAnimationFrame( updateLoop ); 172 | }; 173 | requestAnimationFrame( updateLoop ); 174 | 175 | 176 | elPanelGrid.addEventListener( "dblclick", this._onDoubleClick ); 177 | 178 | // TODO: Use ToolBarListener for this handlers 179 | window.addEventListener( "resize", this._onResize ); 180 | // When you close an externalized panel, "resize" event will be emitted 181 | // before ".extern" is removed from the ingameUi element so we need this 182 | // listener to approproately update our widget dimension. 183 | elUIFrame.addEventListener( "ToggleExternPanel", this._onResize ); 184 | 185 | document.addEventListener( "dataStorageReady", this._onStorageReady ); 186 | } 187 | 188 | disconnectedCallback() { 189 | super.disconnectedCallback(); 190 | 191 | document.addEventListener( "dataStorageReady", this._onStorageReady ); 192 | this._el.panelGrid.removeEventListener( "dblclick", this._onDoubleClick ); 193 | window.removeEventListener( "resize", this._onResize ); 194 | this._el.uiFrame.removeEventListener( "ToggleExternPanel", this._onResize ); 195 | 196 | UIElements.el = {} as any; 197 | 198 | this._isConnected = false; 199 | } 200 | 201 | _onResize = ( e?: Event ) => { 202 | // Calculate the dimensions of the widget 203 | // 204 | // Since the return value of `getBoundingClientRect` is not synchronized 205 | // as in-game panel gets resized or externalized, we would rely on 206 | // in-game panel window's size here. 207 | store.dispatch( A.updateWidgetScale() ); 208 | }; 209 | 210 | _onStorageReady = ( e?: Event ) => { 211 | store.dispatch( A.setStorageReady( true ) ); 212 | }; 213 | 214 | _onUpdate() { 215 | try { 216 | if ( !SimVar.IsReady() ) { 217 | // AS of Sim Update 5, this function causes an error when SimVar is not ready 218 | return; 219 | } 220 | } catch (e) { 221 | // Can't find variable: simvar 222 | return; 223 | } 224 | store.dispatch( A.fetchSimVar() ); 225 | } 226 | 227 | _onDoubleClick = ( e: Event ) => { 228 | // console.log( e ); 229 | if ( e.target === this._el.openConf ) { 230 | console.log( "Double clicked on Open Config button. Ignored." ); 231 | return; 232 | } 233 | store.dispatch( A.quickHidePanel() ); 234 | }; 235 | } 236 | 237 | window.customElements.define( "ingamepanel-input-viewer", InputViewerElement ); 238 | checkAutoload(); 239 | -------------------------------------------------------------------------------- /src/inputViewer/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | shallowEq, 4 | zip, 5 | } from "../utils"; 6 | 7 | describe( "utils", () => { 8 | describe( "shallowEq", () => { 9 | it( "should compare two arrays", () => { 10 | expect( shallowEq( [], [] ) ).toBeTruthy(); 11 | expect( shallowEq( [0], [0] ) ).toBeTruthy(); 12 | expect( shallowEq( [1, 2], [1, 2] ) ).toBeTruthy(); 13 | expect( shallowEq( ["foo", "bar"], ["foo", "bar"] ) ).toBeTruthy(); 14 | 15 | expect( shallowEq( [0], [] ) ).toBeFalsy(); 16 | expect( shallowEq( [0], [0, 1] ) ).toBeFalsy(); 17 | expect( shallowEq( [0, 1], [0] ) ).toBeFalsy(); 18 | expect( shallowEq( [0], [1] ) ).toBeFalsy(); 19 | expect( shallowEq( [0, 1], [0, 2] ) ).toBeFalsy(); 20 | expect( shallowEq( ["foo"], ["bar"] ) ).toBeFalsy(); 21 | } ); 22 | } ); 23 | 24 | describe( "zip", () => { 25 | it( "should zip two arrays", () => { 26 | expect( zip( [], [] ) ).toEqual( [] ); 27 | expect( zip( [1], [2] ) ).toEqual( [[1, 2]] ); 28 | expect( zip( [1, 2, 3], ["foo", "bar", "baz"] ) ).toEqual( [[1, "foo"], [2, "bar"], [3, "baz"]] ); 29 | } ); 30 | 31 | it( "should throw when number of items are different", () => { 32 | expect( () => zip( [], [] ) ).not.toThrow(); 33 | 34 | expect( () => zip( [], [0] ) ).toThrow(); 35 | expect( () => zip( [1, 2], [0] ) ).toThrow(); 36 | } ); 37 | } ); 38 | } ); 39 | -------------------------------------------------------------------------------- /src/inputViewer/makePropMixToggleButton.ts: -------------------------------------------------------------------------------- 1 | 2 | export function makePropMixToggleButton( el: HTMLElement ) { 3 | el.classList.add( "togglePropMix" ); 4 | el.addEventListener( "created", e => { 5 | const elToggleWrap = el.querySelector( ".toggleButtonWrapper .toggleButton" )!; 6 | const elState = elToggleWrap.querySelector( ".state" )!; 7 | 8 | const createIcon = ( id: string, svg: string ) => { 9 | const elIcon = document.createElement( "icon-element" ); 10 | elIcon.setAttribute( "id", id ); 11 | elIcon.setAttribute( "data-url", "/InGamePanels/InputViewer/images/" + svg ); 12 | elToggleWrap.insertBefore( elIcon, elState ); 13 | }; 14 | 15 | createIcon( "MixtureIcon", "mixture.svg" ); 16 | createIcon( "PropellerIcon", "propeller.svg" ); 17 | } ); 18 | } 19 | -------------------------------------------------------------------------------- /src/inputViewer/slice/epics.internal.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | diffAndForceToggleClassList, 4 | diffAndSetInnerText, 5 | setTranslate, 6 | } from "../utils"; 7 | import { UIElements } from "../uiElements"; 8 | import { 9 | AircraftData, 10 | BrakeAxis, 11 | Category, 12 | InputData, 13 | NumberDisplayType, 14 | PanelsToShow, 15 | RudAxis, 16 | SimVarAxisInput, 17 | StickInput, 18 | StickValues, 19 | ThrottleAxis, 20 | } from "./models"; 21 | 22 | type SimVarWatcherName = keyof InputData | keyof AircraftData; 23 | 24 | const g_simVarWatchers: { 25 | [key in SimVarWatcherName]: number 26 | } = { 27 | aileron: -1, 28 | elevator: -1, 29 | rudder: -1, 30 | aileronTrim: -1, 31 | elevatorTrim: -1, 32 | rudderTrim: -1, 33 | brakeLeft: -1, 34 | brakeRight: -1, 35 | throttle1: -1, 36 | throttle2: -1, 37 | throttle3: -1, 38 | throttle4: -1, 39 | propeller1: -1, 40 | mixture1: -1, 41 | 42 | category: -1, 43 | model: -1, 44 | name: -1, 45 | numEngines: -1, 46 | }; 47 | 48 | function getSimVarRegId( watcherName: SimVarWatcherName, name: SimVarName, unit: SimVarUnit ) { 49 | let regId = g_simVarWatchers[watcherName]; 50 | if ( regId < 0 ) { 51 | regId = SimVar.GetRegisteredId( name, unit, "" ); 52 | g_simVarWatchers[watcherName] = regId; 53 | } 54 | return regId; 55 | } 56 | 57 | function getSimVar( watcherName: SimVarWatcherName, name: SimVarName | string, unit: SimVarUnit ) { 58 | return SimVar.GetSimVarValueFastReg( getSimVarRegId( watcherName, name as SimVarName, unit ) ); 59 | } 60 | function getSimVarString( watcherName: SimVarWatcherName, name: SimVarName, unit: SimVarUnit ) { 61 | return SimVar.GetSimVarValueFastRegString( getSimVarRegId( watcherName, name, unit ) ); 62 | } 63 | 64 | function sanitizeCategory( categoryRaw: string ): Category { 65 | if ( categoryRaw.toLowerCase() === "helicopter" ) { 66 | return "helicopter"; 67 | } 68 | return "airplane"; 69 | } 70 | 71 | export function getInputData( category: Category ) { 72 | if ( category === "helicopter" ) { 73 | return getInputDataHelicopter(); 74 | } 75 | return getInputDataAirplane(); 76 | } 77 | 78 | function getInputDataAirplane(): InputData { 79 | return { 80 | aileron: getSimVar( "aileron", "AILERON POSITION", "position" ), 81 | elevator: getSimVar( "elevator", "ELEVATOR POSITION", "position" ), 82 | rudder: getSimVar( "rudder", "RUDDER POSITION", "position" ), 83 | aileronTrim: getSimVar( "aileronTrim", "AILERON TRIM PCT", "percent over 100" ), 84 | elevatorTrim: getSimVar( "elevatorTrim", "ELEVATOR TRIM PCT", "percent over 100" ), 85 | rudderTrim: getSimVar( "rudderTrim", "RUDDER TRIM PCT", "percent over 100" ), 86 | 87 | brakeLeft: getSimVar( "brakeLeft", "BRAKE LEFT POSITION", "position" ), 88 | brakeRight: getSimVar( "brakeRight", "BRAKE RIGHT POSITION", "position" ), 89 | 90 | throttle1: getSimVar( "throttle1", "GENERAL ENG THROTTLE LEVER POSITION:1", "position" ), 91 | throttle2: getSimVar( "throttle2", "GENERAL ENG THROTTLE LEVER POSITION:2", "position" ), 92 | throttle3: getSimVar( "throttle3", "GENERAL ENG THROTTLE LEVER POSITION:3", "position" ), 93 | throttle4: getSimVar( "throttle4", "GENERAL ENG THROTTLE LEVER POSITION:4", "position" ), 94 | propeller1: getSimVar( "propeller1", "GENERAL ENG PROPELLER LEVER POSITION:1", "position" ), 95 | mixture1: getSimVar( "mixture1", "GENERAL ENG MIXTURE LEVER POSITION:1", "position" ), 96 | }; 97 | } 98 | 99 | function getInputDataHelicopter(): InputData { 100 | return { 101 | aileron: getSimVar( "aileron", "YOKE X POSITION LINEAR", "percent over 100" ), 102 | elevator: getSimVar( "elevator", "YOKE Y POSITION", "percent over 100" ), 103 | rudder: getSimVar( "rudder", "TAIL ROTOR PEDAL POSITION", "percent over 100" ), 104 | aileronTrim: getSimVar( "aileronTrim", "ROTOR LATERAL TRIM PCT", "percent over 100" ), 105 | elevatorTrim: getSimVar( "elevatorTrim", "ROTOR LONGITUDINAL TRIM PCT", "percent over 100" ), 106 | rudderTrim: getSimVar( "rudderTrim", "RUDDER TRIM PCT", "percent over 100" ), // Not used 107 | 108 | brakeLeft: getSimVar( "brakeLeft", "ROTOR BRAKE HANDLE POS", "percent over 100" ), 109 | brakeRight: getSimVar( "brakeRight", "ROTOR BRAKE HANDLE POS", "percent over 100" ), 110 | 111 | throttle1: getSimVar( "throttle1", "COLLECTIVE POSITION", "percent over 100" ), 112 | throttle2: 0, 113 | throttle3: 0, 114 | throttle4: 0, 115 | propeller1: getSimVar( "propeller1", "GENERAL ENG THROTTLE LEVER POSITION:1", "position" ), 116 | mixture1: getSimVar( "mixture1", "GENERAL ENG MIXTURE LEVER POSITION:1", "position" ), 117 | }; 118 | } 119 | 120 | export function getAircraftData(): AircraftData { 121 | return { 122 | category: sanitizeCategory( getSimVarString( "category", "CATEGORY", "string" ) ), 123 | model: getSimVarString( "model", "ATC MODEL", "string" ), 124 | name: getSimVarString( "name", "TITLE", "string" ), 125 | numEngines: getSimVar( "numEngines", "NUMBER OF ENGINES", "number" ), 126 | }; 127 | } 128 | 129 | const NumDispLabels: Record< 130 | string, 131 | Record 132 | > = { 133 | simple: { 134 | aileron: "AIL", 135 | elevator: "ELEV", 136 | rudder: "RUD", 137 | aileronTrim: null, 138 | elevatorTrim: null, 139 | rudderTrim: null, 140 | brakeLeft: "BRK", 141 | brakeRight: null, 142 | throttle1: "THR", 143 | throttle2: "#2", 144 | throttle3: "#3", 145 | throttle4: "#4", 146 | propeller1: "PROP", 147 | mixture1: "MIX", 148 | }, 149 | simpleHelicopter: { 150 | aileron: "LAT", 151 | elevator: "LONG", 152 | rudder: "RUD", 153 | aileronTrim: null, 154 | elevatorTrim: null, 155 | rudderTrim: null, 156 | brakeLeft: "BRK", 157 | brakeRight: null, 158 | throttle1: "COLL", 159 | throttle2: null, 160 | throttle3: null, 161 | throttle4: null, 162 | propeller1: "THR", 163 | mixture1: "MIX", 164 | }, 165 | verbose: { 166 | aileron: "Aileron", 167 | elevator: "Elevator", 168 | rudder: "Rudder", 169 | aileronTrim: "Aileron Trim", 170 | elevatorTrim: "Elevator Trim", 171 | rudderTrim: "Rudder Trim", 172 | brakeLeft: "Brake Left", 173 | brakeRight: "Brake Right", 174 | throttle1: "Throttle 1", 175 | throttle2: "Throttle 2", 176 | throttle3: "Throttle 3", 177 | throttle4: "Throttle 4", 178 | propeller1: "Propeller 1", 179 | mixture1: "Mixture 1", 180 | }, 181 | verboseHelicopter: { 182 | aileron: "Lateral", 183 | elevator: "Longitudinal", 184 | rudder: "Rudder", 185 | aileronTrim: "Lateral Trim", 186 | elevatorTrim: "Long. Trim", 187 | rudderTrim: "Rudder Trim", 188 | brakeLeft: "Rotor Brake", 189 | brakeRight: "-", 190 | throttle1: "Collective", 191 | throttle2: "-", 192 | throttle3: "-", 193 | throttle4: "-", 194 | propeller1: "Throttle", 195 | mixture1: "Mixture", 196 | }, 197 | }; 198 | 199 | export function updateCategory( category: Category ) { 200 | diffAndSetAttribute( UIElements.el.root, "data-category", category ); 201 | 202 | // Update labels 203 | const simpleLabels = category === "airplane" 204 | ? NumDispLabels.simple 205 | : NumDispLabels.simpleHelicopter; 206 | const verboseLabels = category === "airplane" 207 | ? NumDispLabels.verbose 208 | : NumDispLabels.verboseHelicopter; 209 | 210 | for ( let key_ in simpleLabels ) { 211 | const key = key_ as SimVarAxisInput; 212 | if ( simpleLabels[key] !== null ) { 213 | diffAndSetText( UIElements.el.numberSimpleLabel[key], simpleLabels[key]! ); 214 | } 215 | diffAndSetText( UIElements.el.numberVerboseLabel[key], verboseLabels[key]! ); 216 | } 217 | } 218 | 219 | export function updateStick( key: StickInput, [x, y]: StickValues ) { 220 | setTranslate( UIElements.el.main[key], x * 100, y * 100 ); 221 | }; 222 | 223 | export function updateRudder( key: RudAxis, value: number ) { 224 | setTranslate( UIElements.el.main[key], value * 100, 0 ); 225 | }; 226 | 227 | export function updateHorizontalBar( key: BrakeAxis, value: number ) { 228 | const el = UIElements.el.main[key]; 229 | value = value * 100; 230 | diffAndSetAttribute( el.fg, "width", value.toString() ); 231 | setTranslate( el.cap, value, 0 ); 232 | }; 233 | 234 | export function updateVerticalBar( key: ThrottleAxis, value: number ) { 235 | const el = UIElements.el.main[key]; 236 | value = value * 100; 237 | diffAndSetAttribute( el.fg, "height", value.toString() ); 238 | setTranslate( el.cap, 0, value ); 239 | }; 240 | 241 | export function updateNumberDisplayVerbose( key: SimVarAxisInput, value: number ) { 242 | diffAndSetInnerText( UIElements.el.numberVerbose[key], value.toString() ); 243 | } 244 | 245 | const trimAxis: SimVarAxisInput[] = [ 246 | "aileronTrim", 247 | "elevatorTrim", 248 | "rudderTrim", 249 | ]; 250 | 251 | export function simplifyNumber( key: SimVarAxisInput, value: number ) { 252 | value = value * 100; 253 | let fracDigits = 0; 254 | if ( trimAxis.indexOf( key ) >= 0 ) { 255 | if ( Math.abs( value ) < 0.95 && value !== 0 ) { 256 | fracDigits = 1; 257 | } 258 | } 259 | return value.toFixed( fracDigits ); 260 | } 261 | 262 | export function updateNumberDisplaySimple( key: SimVarAxisInput, value: number ) { 263 | diffAndSetText( UIElements.el.numberSimple[key], simplifyNumber( key, value ) ); 264 | } 265 | 266 | export function updateNumberDisplaySimpleSign( key: SimVarAxisInput, sign: number ) { 267 | diffAndForceToggleClassList( UIElements.el.numberSimple[key], "zero", sign === 0 || sign === -0 ); 268 | } 269 | 270 | export function getThrottlePanelMode( enablePropMixBar: boolean, numEngines: number ) { 271 | if ( enablePropMixBar ) { 272 | return "PropMix"; 273 | } else if ( numEngines <= 1 ) { 274 | return "SingleEngine"; 275 | } else if ( numEngines == 2 ) { 276 | return "TwinEngine"; 277 | } else if ( numEngines == 3 ) { 278 | return "ThreeEngine"; 279 | } else { 280 | return "FourEngine"; 281 | } 282 | } 283 | 284 | export function updateThrottlePanelMode( mode: ReturnType ) { 285 | diffAndSetAttribute( UIElements.el.root, "data-throttle-panel-mode", mode ); 286 | console.log( "[updateThrottlePanelMode] mode: " + mode ); 287 | } 288 | 289 | function setToggleValue( el: ToggleButtonElement, value: boolean ) { 290 | if ( el.getValue() !== value ) { 291 | el.setValue( value ); 292 | } 293 | } 294 | 295 | function setListCurrentValue( el: NewListButtonElement, value: number ) { 296 | if ( value !== el.getCurrentValue() ) { 297 | if ( !el.valueElem ) { 298 | el.defaultValue = value; 299 | } else { 300 | el.setCurrentValue( value ); 301 | } 302 | } 303 | } 304 | function setListCurrentChoice( el: NewListButtonElement, choice: string ) { 305 | const value = el.choices.indexOf( choice ); 306 | if ( value < 0 ) { 307 | throw new Error( "[setListCurrentChoice] Invalid choice: " + choice ); 308 | } 309 | return setListCurrentValue( el, value ); 310 | } 311 | 312 | export function updateAutoHideHeader( value: boolean ) { 313 | diffAndSetAttribute( UIElements.el.root, "data-auto-hide-header", value.toString() ); 314 | setToggleValue( UIElements.el.confAutoHideHeader, value ); 315 | } 316 | 317 | export function updatePanelsToShow( type: PanelsToShow ) { 318 | diffAndSetAttribute( UIElements.el.root, "data-panels", type ); 319 | setListCurrentChoice( UIElements.el.confPanels, type ); 320 | } 321 | 322 | export function updateNumberDisplayType( type: NumberDisplayType ) { 323 | diffAndSetAttribute( UIElements.el.root, "data-number-display-type", type ); 324 | setListCurrentChoice( UIElements.el.confNumericDisp, type ); 325 | } 326 | 327 | export function updateQuickHideDuration( duration: number ) { 328 | setListCurrentValue( UIElements.el.confQuickHideDuration, duration ); 329 | } 330 | 331 | export function updateEnablePropMixBar( value: boolean ) { 332 | setToggleValue( UIElements.el.confPropMix, value ); 333 | } 334 | 335 | export function updateAircraftName( name: string ) { 336 | diffAndSetInnerText( UIElements.el.confAircraftModel, Utils.Translate( name )! ); 337 | } 338 | 339 | export function quickHidePanel( duration: number ) { 340 | const isExtern = document.body.classList.contains( "extern" ); 341 | if ( isExtern ) { 342 | return; 343 | } 344 | 345 | const { uiFrame } = UIElements.el; 346 | uiFrame.visible = false; 347 | setTimeout( () => { 348 | uiFrame.visible = true; 349 | }, duration * 1000 ); 350 | } 351 | 352 | export function updateWidgetScale( 353 | autoHideHeader: boolean, 354 | panelsToShow: PanelsToShow, 355 | numberDispType: NumberDisplayType 356 | ) { 357 | const _style = window.document.documentElement.style; 358 | 359 | const vpWidth = Number( _style.getPropertyValue( "--viewportWidth" ) ); // window.top.innerWidth; 360 | const vpHeight = Number( _style.getPropertyValue( "--viewportHeight" ) ); // window.top.innerHeight; 361 | const screenHeight = Number( _style.getPropertyValue( "--screenHeight" ) ); 362 | 363 | const scaled = ( v: number ) => screenHeight * v / 2160; 364 | const margin = scaled( 6 ); 365 | 366 | const headerHeight = scaled( 84 ); 367 | const isExtern = document.body.classList.contains( "extern" ); 368 | const hasHeader = !isExtern && !autoHideHeader; 369 | 370 | const wrapperWidth = vpWidth - margin * 2; 371 | const wrapperHeight = vpHeight - margin * 2 - ( hasHeader ? ( headerHeight + margin ) : 0 ); 372 | 373 | let widgetPrescaledWidth = 280; 374 | let widgetPrescaledHeight = 260; 375 | 376 | if ( panelsToShow === "throttle" ) { 377 | widgetPrescaledWidth = 60; 378 | if ( numberDispType === "simple" ) { 379 | widgetPrescaledWidth += 60; 380 | } 381 | } 382 | 383 | const widgetAspectRatio = widgetPrescaledWidth / widgetPrescaledHeight; 384 | const widgetWidth = Math.min( 385 | wrapperWidth, 386 | wrapperHeight * widgetAspectRatio 387 | ); 388 | const widgetScale = widgetWidth / widgetPrescaledWidth; 389 | 390 | UIElements.el.uiFrame.style.setProperty( "--widgetScale", widgetScale + "px" ); 391 | // console.log( "widged scale = " + widgetScale ); 392 | } 393 | 394 | 395 | export namespace config { 396 | export const AUTO_HIDE_HEADER = "AUTO_HIDE_HEADER"; 397 | export const PANELS = "PANELS"; 398 | export const NUMBER_DISPLAY_MODE = "NUMBER_DISPLAY_MODE"; 399 | export const QUICK_HIDE_DURATION = "QUICK_HIDE_DURATION"; 400 | export const ENABLE_PROPMIX_BAR = "ENABLE_PROPMIX_BAR"; 401 | 402 | const propMixAircrafts = [ 403 | "TT:ATCCOM.AC_MODEL B350.0.text", // Beechcraft King Air 350i 404 | "TT:ATCCOM.AC_MODEL_BE36.0.text", // Bonanza G36 405 | "TT:ATCCOM.AC_MODEL C152.0.text", // Cessna 152 406 | "TT:ATCCOM.AC_MODEL C172.0.text", // Cessna Skyhawk G1000 407 | "TT:ATCCOM.AC_MODEL C208.0.text", // Cessna 208B Grand Caravan EX 408 | "TT:ATCCOM.AC_MODEL_CC19.0.text", // XCub 409 | "TT:ATCCOM.AC_MODEL CP10.0.text", // Murdy Cap 10C 410 | "TT:ATCCOM.AC_MODEL_DR40.0.text", // DR 400 411 | "TT:ATCCOM.AC_MODEL E300.0.text", // Extra 330 412 | "TT:ATCCOM.AC_MODEL PTS2.0.text", // Pitts 413 | ]; 414 | 415 | const propMixAircraftMap: { [modelName: string]: boolean } = {}; 416 | propMixAircrafts.forEach( modelName => propMixAircraftMap[modelName] = true ); 417 | 418 | const PANEL_NAME = "SPITICE_INPUTVIEWER"; // Used for generating storage keys 419 | function configKey( name: string ) { 420 | return PANEL_NAME + "." + name; 421 | } 422 | 423 | export function dumpStoredData() { 424 | const storedData = SearchStoredData( PANEL_NAME ); 425 | console.log( `${storedData!.length} item(s) are found in Storage.` ); 426 | } 427 | 428 | let _isLoading = false; 429 | 430 | /** 431 | * During loading, `setData` and `deletaData` will ignore all their inputs. 432 | * @param isLoading 433 | */ 434 | export function setLoadingConfig( isLoading: boolean ) { 435 | _isLoading = isLoading; 436 | } 437 | 438 | export function getData( name: string, defaultValue: T ): T { 439 | const key = configKey( name ); 440 | const value = GetStoredData( key )!; 441 | console.log( `[GetStoredData] ${key} = ${value}` ); 442 | return ( value as any as T ) || defaultValue; 443 | } 444 | 445 | export function setData( name: string, value: string ) { 446 | if ( _isLoading ) { 447 | return; 448 | } 449 | 450 | const key = configKey( name ); 451 | SetStoredData( key, value ); 452 | console.log( `[SetStoredData] ${key} = ${value}` ); 453 | } 454 | 455 | function deleteData( name: string ) { 456 | if ( _isLoading ) { 457 | return; 458 | } 459 | 460 | const key = configKey( name ); 461 | DeleteStoredData( key ); 462 | console.log( `[DeleteStoredData] ${key}` ); 463 | } 464 | 465 | export function getQuickHideDuration() { 466 | const DEFAULT_VALUE = 2; 467 | const data = getData( QUICK_HIDE_DURATION, DEFAULT_VALUE.toString() ); 468 | const value = parseInt( data ); 469 | if ( isNaN( value ) ) { 470 | return DEFAULT_VALUE; 471 | } 472 | return Math.max( 0, Math.min( 3, value ) ); 473 | } 474 | 475 | export function getEnablePropMixBar( modelName: string, category: Category ) { 476 | const name = ENABLE_PROPMIX_BAR + ":" + modelName; 477 | let value = getData( name, "" ); 478 | if ( value === "" ) { 479 | console.log( "[getEnablePropMixBar] Using the default value..." ); 480 | if ( category === "helicopter" ) { 481 | return true; // Enable PropMix bar for helicopters by default 482 | } 483 | return !!propMixAircraftMap[modelName]; // Cast undefined to false 484 | } 485 | return value === "1"; 486 | } 487 | 488 | export function setEnablePropMixBar( modelName: string, isEnable: boolean ) { 489 | const name = ENABLE_PROPMIX_BAR + ":" + modelName; 490 | const isEnableByDefault = modelName in propMixAircraftMap; 491 | if ( isEnable === isEnableByDefault ) { 492 | // Remove the current setting 493 | deleteData( name ); 494 | return; 495 | } 496 | // Store the value 497 | setData( name, isEnable ? "1" : "0" ); 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /src/inputViewer/slice/epics.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Action, 4 | isAnyOf, 5 | } from "@reduxjs/toolkit"; 6 | import { 7 | combineEpics, 8 | Epic, 9 | } from "redux-observable"; 10 | import { of } from "rxjs"; 11 | import { 12 | catchError, 13 | distinctUntilChanged, 14 | filter, 15 | ignoreElements, 16 | map, 17 | mergeMap, 18 | pluck, 19 | tap, 20 | throttleTime, 21 | withLatestFrom 22 | } from "rxjs/operators"; 23 | 24 | import { shallowEq } from "../utils"; 25 | import { 26 | AilElevAxis, 27 | AircraftData, 28 | InputViewerState, 29 | NumberDisplayType, 30 | PanelsToShow, 31 | SimVarAxisInput, 32 | StickInput, 33 | StickValues, 34 | } from "./models"; 35 | import { 36 | config, 37 | getAircraftData, 38 | getInputData, 39 | getThrottlePanelMode, 40 | quickHidePanel, 41 | updateAircraftName, 42 | updateAutoHideHeader, 43 | updateCategory, 44 | updateEnablePropMixBar, 45 | updateHorizontalBar, 46 | updateNumberDisplaySimple, 47 | updateNumberDisplaySimpleSign, 48 | updateNumberDisplayType, 49 | updateNumberDisplayVerbose, 50 | updatePanelsToShow, 51 | updateQuickHideDuration, 52 | updateRudder, 53 | updateStick, 54 | updateThrottlePanelMode, 55 | updateVerticalBar, 56 | updateWidgetScale, 57 | } from "./epics.internal"; 58 | import { actions as A } from "./slice"; 59 | 60 | type E = Epic; 61 | 62 | const AIRCRAFT_DATA_UPDATE_INTERVAL = 1000; 63 | 64 | const updateLoadingState: E = action$ => action$.pipe( 65 | filter( A.setLoadingConfig.match ), 66 | tap( ({ payload }) => { 67 | config.setLoadingConfig( payload ); 68 | } ), 69 | ignoreElements() 70 | ); 71 | 72 | const loadConfigGeneral: E = action$ => action$.pipe( 73 | filter( A.setStorageReady.match ), 74 | filter( ({ payload: isReady }) => isReady ), 75 | tap( () => { 76 | config.dumpStoredData(); 77 | } ), 78 | mergeMap( () => of( 79 | A.setLoadingConfig( true ), 80 | A.setAutoHideHeader( config.getData( config.AUTO_HIDE_HEADER, "false" ) === "true" ), 81 | A.setPanelsToShow( config.getData( config.PANELS, "all" ) ), 82 | A.setNumberDisplayType( config.getData( config.NUMBER_DISPLAY_MODE, "none" ) ), 83 | A.setQuickHideDuration( config.getQuickHideDuration() ), 84 | A.setLoadingConfig( false ), 85 | ) ), 86 | ); 87 | const loadConfigAircraft: E = ( action$, state$ ) => action$.pipe( 88 | filter( isAnyOf( 89 | A.setStorageReady, 90 | A.setAircraft, 91 | ) ), 92 | 93 | withLatestFrom( state$ ), 94 | map( ([ _action, state ]) => ([ 95 | state.app.isStorageReady, 96 | state.aircraft, 97 | ] as [boolean, AircraftData]) ), 98 | distinctUntilChanged( shallowEq ), 99 | 100 | filter( tuple => tuple[0] ), // isStorageReady == true 101 | filter( tuple => tuple[1].model !== "" ), 102 | 103 | mergeMap( ([ _, aircraft ]) => of( 104 | A.setLoadingConfig( true ), 105 | A.setEnablePropMixBar( config.getEnablePropMixBar( aircraft.model, aircraft.category ) ), 106 | A.setLoadingConfig( false ), 107 | ) ), 108 | ); 109 | 110 | const saveAutoHideHeader: E = action$ => action$.pipe( 111 | filter( A.setAutoHideHeader.match ), 112 | tap( ({ payload }) => { 113 | config.setData( config.AUTO_HIDE_HEADER, payload.toString() ); 114 | } ), 115 | ignoreElements() 116 | ); 117 | const savePanelsToShow: E = action$ => action$.pipe( 118 | filter( A.setPanelsToShow.match ), 119 | tap( ({ payload }) => { 120 | config.setData( config.PANELS, payload ); 121 | } ), 122 | ignoreElements() 123 | ); 124 | const saveNumberDisplayType: E = action$ => action$.pipe( 125 | filter( A.setNumberDisplayType.match ), 126 | tap( ({ payload }) => { 127 | config.setData( config.NUMBER_DISPLAY_MODE, payload ); 128 | } ), 129 | ignoreElements() 130 | ); 131 | const saveQuickHideDuration: E = action$ => action$.pipe( 132 | filter( A.setQuickHideDuration.match ), 133 | tap( ({ payload }) => { 134 | config.setData( config.QUICK_HIDE_DURATION, payload.toString() ); 135 | } ), 136 | ignoreElements() 137 | ); 138 | const saveEnablePropMixBar: E = ( action$, state$ ) => action$.pipe( 139 | filter( A.setEnablePropMixBar.match ), 140 | withLatestFrom( state$ ), 141 | tap( ([ { payload }, state ]) => { 142 | config.setEnablePropMixBar( state.aircraft.model, payload ); 143 | } ), 144 | ignoreElements() 145 | ); 146 | 147 | 148 | const fetchSimVarAircraft: E = action$ => action$.pipe( 149 | filter( A.fetchSimVar.match ), 150 | throttleTime( AIRCRAFT_DATA_UPDATE_INTERVAL ), 151 | map( () => A.setAircraft( getAircraftData() ) ), 152 | ); 153 | const fetchSimVarInput: E = ( action$, state$ ) => action$.pipe( 154 | filter( A.fetchSimVar.match ), 155 | withLatestFrom( state$ ), 156 | map( ([ _, state ] ) => A.setInput( getInputData( state.aircraft.category ) ) ), 157 | ); 158 | 159 | const forceUpdateAllInput: E = ( action$, state$ ) => action$.pipe( 160 | filter( A.forceUpdateAllInputs.match ), 161 | withLatestFrom( state$ ), 162 | tap( ([ _, state ]) => { 163 | const { input: d } = state; 164 | 165 | updateStick( "stick", [d.aileron, d.elevator] ); 166 | updateStick( "stickTrim", [d.aileronTrim, d.elevatorTrim] ); 167 | updateRudder( "rudder", d.rudder ); 168 | updateRudder( "rudderTrim", d.rudderTrim ); 169 | updateHorizontalBar( "brakeLeft", d.brakeLeft ); 170 | updateHorizontalBar( "brakeRight", d.brakeRight ); 171 | updateVerticalBar( "throttle1", d.throttle1 ); 172 | updateVerticalBar( "throttle2", d.throttle2 ); 173 | updateVerticalBar( "throttle3", d.throttle3 ); 174 | updateVerticalBar( "throttle4", d.throttle4 ); 175 | updateVerticalBar( "propeller1", d.propeller1 ); 176 | updateVerticalBar( "mixture1", d.mixture1 ); 177 | 178 | const updateNumberDisplay = ( key: SimVarAxisInput ) => { 179 | const value = d[key]; 180 | updateNumberDisplayVerbose( key, value ); 181 | updateNumberDisplaySimple( key, value ); 182 | updateNumberDisplaySimpleSign( key, Math.sign( value ) ); 183 | }; 184 | updateNumberDisplay( "aileron" ); 185 | updateNumberDisplay( "aileronTrim" ); 186 | updateNumberDisplay( "elevator" ); 187 | updateNumberDisplay( "elevatorTrim" ); 188 | updateNumberDisplay( "rudder" ); 189 | updateNumberDisplay( "rudderTrim" ); 190 | updateNumberDisplay( "brakeLeft" ); 191 | updateNumberDisplay( "brakeRight" ); 192 | updateNumberDisplay( "throttle1" ); 193 | updateNumberDisplay( "throttle2" ); 194 | updateNumberDisplay( "throttle3" ); 195 | updateNumberDisplay( "throttle4" ); 196 | updateNumberDisplay( "propeller1" ); 197 | updateNumberDisplay( "mixture1" ); 198 | } ), 199 | ignoreElements(), 200 | ); 201 | 202 | function _makeUpdater( 203 | onUpdate: ( key: TAxis, value: number ) => void 204 | ) { 205 | return ( key: TAxis ): E => ( action$, state$ ) => action$.pipe( 206 | filter( A.setInput.match ), 207 | 208 | pluck( "payload", key ), 209 | distinctUntilChanged(), 210 | 211 | // Update the main element 212 | tap( value => onUpdate( key, value ) ) 213 | 214 | ).pipe( 215 | // Update the number display 216 | withLatestFrom( state$ ), 217 | map( ([ value, state ]) => ({ 218 | value, 219 | numberDisplayType: state.config.numberDisplayType, 220 | }) ), 221 | tap( ({ value, numberDisplayType }) => { 222 | if ( numberDisplayType === "verbose" ) { 223 | updateNumberDisplayVerbose( key, value ); 224 | } else if ( numberDisplayType === "simple" ) { 225 | updateNumberDisplaySimple( key, value ); 226 | } 227 | } ) 228 | 229 | ).pipe( 230 | // Update the number display sign 231 | filter( ({ numberDisplayType }) => numberDisplayType === "simple" ), 232 | map( ({ value }) => Math.sign( value ) ), 233 | distinctUntilChanged(), 234 | tap( sign => { 235 | updateNumberDisplaySimpleSign( key, sign ); 236 | } ), 237 | 238 | ).pipe( ignoreElements() ); 239 | } 240 | 241 | const makeAilElevUpdater = _makeUpdater( () => {} ); 242 | const makeRudUpdater = _makeUpdater( updateRudder ); 243 | const makeHBarUpdater = _makeUpdater( updateHorizontalBar ); 244 | const makeVBarUpdater = _makeUpdater( updateVerticalBar ); 245 | 246 | const makeStickUpdater = ( 247 | key: StickInput, 248 | inputKeys: [AilElevAxis, AilElevAxis] 249 | ): E => action$ => action$.pipe( 250 | filter( A.setInput.match ), 251 | 252 | map( ( { payload } ): StickValues => ( [ 253 | payload[inputKeys[0]], 254 | payload[inputKeys[1]], 255 | ] ) ), 256 | distinctUntilChanged( shallowEq ), 257 | 258 | // Update the main element 259 | tap( values => updateStick( key, values ) ), 260 | 261 | ignoreElements() 262 | ); 263 | 264 | const checkThrottlePanelMode: E = ( action$, state$ ) => action$.pipe( 265 | filter( isAnyOf( 266 | A.setAircraft, 267 | A.setEnablePropMixBar, 268 | ) ), 269 | 270 | withLatestFrom( state$ ), 271 | map( ([ _action, state ]) => getThrottlePanelMode( 272 | state.config.enablePropMixBar, 273 | state.aircraft.numEngines 274 | ) ), 275 | distinctUntilChanged(), 276 | 277 | // Update the throttle panel 278 | tap( value => updateThrottlePanelMode( value ) ), 279 | 280 | ignoreElements() 281 | ); 282 | 283 | 284 | const checkAutoHideHeader: E = action$ => action$.pipe( 285 | filter( A.setAutoHideHeader.match ), 286 | tap( ({ payload }) => { 287 | updateAutoHideHeader( payload ); 288 | } ), 289 | map( () => A.updateWidgetScale() ), 290 | ); 291 | const checkPanelsToShow: E = action$ => action$.pipe( 292 | filter( A.setPanelsToShow.match ), 293 | tap( ({ payload }) => { 294 | updatePanelsToShow( payload ); 295 | } ), 296 | map( () => A.updateWidgetScale() ), 297 | ); 298 | const checkNumberDisplayType: E = action$ => action$.pipe( 299 | filter( A.setNumberDisplayType.match ), 300 | tap( ({ payload }) => { 301 | updateNumberDisplayType( payload ); 302 | } ), 303 | map( () => A.forceUpdateAllInputs() ), 304 | ); 305 | const checkQuickHideDuration: E = action$ => action$.pipe( 306 | filter( A.setQuickHideDuration.match ), 307 | tap( ({ payload }) => { 308 | updateQuickHideDuration( payload ); 309 | } ), 310 | ignoreElements(), 311 | ); 312 | const checkConfigEnablePropMixBar: E = action$ => action$.pipe( 313 | filter( A.setEnablePropMixBar.match ), 314 | tap( ({ payload }) => { 315 | updateEnablePropMixBar( payload ); 316 | } ), 317 | ignoreElements(), 318 | ); 319 | const checkAircraftName: E = action$ => action$.pipe( 320 | filter( A.setAircraft.match ), 321 | map( ({ payload }) => payload.name ), 322 | distinctUntilChanged(), 323 | tap( name => { 324 | updateAircraftName( name ); 325 | } ), 326 | ignoreElements(), 327 | ); 328 | const checkCategory: E = action$ => action$.pipe( 329 | filter( A.setAircraft.match ), 330 | map( ({ payload }) => payload.category ), 331 | distinctUntilChanged(), 332 | tap( category => { 333 | updateCategory( category ); 334 | } ), 335 | ignoreElements(), 336 | ); 337 | 338 | const handleUpdateWidgetScale: E = ( action$, state$ ) => action$.pipe( 339 | filter( A.updateWidgetScale.match ), 340 | withLatestFrom( state$ ), 341 | tap( ([ _action, { config } ]) => { 342 | updateWidgetScale( 343 | config.autoHideHeader, 344 | config.panels, 345 | config.numberDisplayType 346 | ); 347 | } ), 348 | ignoreElements(), 349 | ); 350 | 351 | const handleQuickHidePanel: E = ( action$, state$ ) => action$.pipe( 352 | filter( A.quickHidePanel.match ), 353 | withLatestFrom( state$ ), 354 | map( ([ _action, state ]) => state.config.quickHideDuration ), 355 | filter( duration => duration > 0 ), 356 | tap( duration => { 357 | quickHidePanel( duration ); 358 | } ), 359 | ignoreElements(), 360 | ); 361 | 362 | const logActions: E = action$ => action$.pipe( 363 | filter( isAnyOf( 364 | A.setStorageReady, 365 | A.setLoadingConfig, 366 | 367 | // General configs 368 | A.setAutoHideHeader, 369 | A.setPanelsToShow, 370 | A.setNumberDisplayType, 371 | A.setQuickHideDuration, 372 | 373 | // Aircraft-specific configs 374 | A.setEnablePropMixBar, 375 | ) ), 376 | tap( action => console.log( action ) ), 377 | ignoreElements() 378 | ); 379 | 380 | const epics: E[] = [ 381 | // Debug 382 | logActions, 383 | 384 | // Load & Save 385 | updateLoadingState, 386 | loadConfigGeneral, 387 | loadConfigAircraft, 388 | saveAutoHideHeader, 389 | savePanelsToShow, 390 | saveNumberDisplayType, 391 | saveQuickHideDuration, 392 | saveEnablePropMixBar, 393 | 394 | // SimVar 395 | fetchSimVarAircraft, 396 | fetchSimVarInput, 397 | 398 | // Input updaters 399 | forceUpdateAllInput, 400 | makeStickUpdater( "stick", ["aileron", "elevator"] ), 401 | makeStickUpdater( "stickTrim", ["aileronTrim", "elevatorTrim"] ), 402 | makeAilElevUpdater( "aileron" ), 403 | makeAilElevUpdater( "aileronTrim" ), 404 | makeAilElevUpdater( "elevator" ), 405 | makeAilElevUpdater( "elevatorTrim" ), 406 | makeRudUpdater( "rudder" ), 407 | makeRudUpdater( "rudderTrim" ), 408 | makeHBarUpdater( "brakeLeft" ), 409 | makeHBarUpdater( "brakeRight" ), 410 | makeVBarUpdater( "throttle1" ), 411 | makeVBarUpdater( "throttle2" ), 412 | makeVBarUpdater( "throttle3" ), 413 | makeVBarUpdater( "throttle4" ), 414 | makeVBarUpdater( "propeller1" ), 415 | makeVBarUpdater( "mixture1" ), 416 | 417 | // Configuration handlers 418 | checkThrottlePanelMode, 419 | checkAutoHideHeader, 420 | checkPanelsToShow, 421 | checkNumberDisplayType, 422 | checkQuickHideDuration, 423 | checkConfigEnablePropMixBar, 424 | checkAircraftName, 425 | checkCategory, 426 | 427 | // Resize 428 | handleUpdateWidgetScale, 429 | 430 | // Quick hide 431 | handleQuickHidePanel, 432 | ]; 433 | 434 | export const epic: E = ( action$, store$, dependencies ) => 435 | combineEpics( ...epics )( action$, store$, dependencies ).pipe( 436 | catchError( ( e, source ) => { 437 | console.error( e ); 438 | return source; 439 | } ) 440 | ); 441 | -------------------------------------------------------------------------------- /src/inputViewer/slice/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { epic as inputViewerEpic } from "./epics"; 3 | export { InputViewerState } from "./models"; 4 | export { 5 | actions as inputViewerActions, 6 | reducer as inputViewerReducer, 7 | } from "./slice"; 8 | -------------------------------------------------------------------------------- /src/inputViewer/slice/models.ts: -------------------------------------------------------------------------------- 1 | 2 | export type AilElevAxis = "aileron" | "elevator" | "aileronTrim" | "elevatorTrim"; 3 | export type RudAxis = "rudder" | "rudderTrim"; 4 | export type BrakeAxis = "brakeLeft" | "brakeRight"; 5 | export type ThrottleAxis = "throttle1" | "throttle2" | "throttle3" | "throttle4" | "propeller1" | "mixture1"; 6 | export type StickInput = "stick" | "stickTrim"; 7 | 8 | export type SimVarAxisInput = AilElevAxis | RudAxis | BrakeAxis | ThrottleAxis; 9 | export type SliderAxis = BrakeAxis | ThrottleAxis; 10 | 11 | export type StickValues = [aileron: number, elevator: number]; 12 | 13 | export type InputData = { 14 | [key in SimVarAxisInput]: number; 15 | }; 16 | 17 | export type Category = "airplane" | "helicopter"; 18 | 19 | export interface AircraftData { 20 | /** 21 | * "Airplane" or "Helicopter" 22 | */ 23 | category: Category; 24 | /** 25 | * "TITLE" may differ based on the livery currently using so we use "ATC 26 | * MODEL" to distinguish the aircraft types. 27 | */ 28 | model: string; 29 | name: string; 30 | numEngines: number; 31 | } 32 | 33 | export type PanelsToShow = "all" | "throttle"; 34 | export type NumberDisplayType = "none" | "simple" | "verbose"; 35 | 36 | export interface Config { 37 | // General 38 | autoHideHeader: boolean; 39 | panels: PanelsToShow; 40 | numberDisplayType: NumberDisplayType; 41 | quickHideDuration: number; 42 | 43 | // Aircraft-specific 44 | enablePropMixBar: boolean; 45 | } 46 | 47 | export interface App { 48 | isStorageReady: boolean; 49 | } 50 | 51 | export interface InputViewerState { 52 | app: App; 53 | input: InputData; 54 | aircraft: AircraftData; 55 | config: Config; 56 | } 57 | -------------------------------------------------------------------------------- /src/inputViewer/slice/slice.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | createSlice, 4 | PayloadAction as PA 5 | } from "@reduxjs/toolkit"; 6 | 7 | import { 8 | AircraftData, 9 | InputData, 10 | InputViewerState, 11 | NumberDisplayType, 12 | PanelsToShow, 13 | } from "./models"; 14 | 15 | const noop = () => {}; 16 | 17 | const initialState: InputViewerState = { 18 | app: { 19 | isStorageReady: false, 20 | }, 21 | 22 | input: { 23 | aileron: 0, 24 | elevator: 0, 25 | rudder: 0, 26 | aileronTrim: 0, 27 | elevatorTrim: 0, 28 | rudderTrim: 0, 29 | brakeLeft: 0, 30 | brakeRight: 0, 31 | throttle1: 0, 32 | throttle2: 0, 33 | throttle3: 0, 34 | throttle4: 0, 35 | propeller1: 0, 36 | mixture1: 0, 37 | }, 38 | 39 | aircraft: { 40 | category: "airplane", 41 | model: "", 42 | name: "", 43 | numEngines: 1, 44 | }, 45 | 46 | config: { 47 | autoHideHeader: false, 48 | panels: "all", 49 | numberDisplayType: "none", 50 | quickHideDuration: 2, 51 | enablePropMixBar: false, 52 | }, 53 | }; 54 | 55 | const inputViewerSlice = createSlice({ 56 | name: "inputViewer", 57 | initialState, 58 | reducers: { 59 | setStorageReady: ( state, { payload }: PA ) => { 60 | state.app.isStorageReady = payload; 61 | }, 62 | 63 | setInput: ( state, { payload }: PA ) => { 64 | state.input = payload; 65 | }, 66 | setAircraft: ( state, { payload }: PA ) => { 67 | state.aircraft = payload; 68 | }, 69 | 70 | setAutoHideHeader: ( state, { payload }: PA ) => { 71 | state.config.autoHideHeader = payload; 72 | }, 73 | setPanelsToShow: ( state, { payload }: PA ) => { 74 | state.config.panels = payload; 75 | }, 76 | setNumberDisplayType: ( state, { payload }: PA ) => { 77 | state.config.numberDisplayType = payload; 78 | }, 79 | setQuickHideDuration: ( state, { payload }: PA ) => { 80 | state.config.quickHideDuration = payload; 81 | }, 82 | setEnablePropMixBar: ( state, { payload }: PA ) => { 83 | state.config.enablePropMixBar = payload; 84 | }, 85 | 86 | // Epic triggers 87 | setLoadingConfig: ( state, { payload }: PA ) => {}, 88 | updateWidgetScale: noop, 89 | fetchSimVar: noop, 90 | forceUpdateAllInputs: noop, 91 | quickHidePanel: noop, 92 | }, 93 | }) 94 | 95 | export const { 96 | actions, 97 | reducer, 98 | } = inputViewerSlice; 99 | -------------------------------------------------------------------------------- /src/inputViewer/uiElements.ts: -------------------------------------------------------------------------------- 1 | 2 | // TODO: Use Redux to avoid this pattern completely. 3 | 4 | import { 5 | RudAxis, 6 | SimVarAxisInput, 7 | SliderAxis, 8 | StickInput, 9 | } from "./slice/models"; 10 | 11 | export interface BarElements { 12 | fg: HTMLElement; 13 | cap: HTMLElement; 14 | } 15 | 16 | type Elements = Record; 17 | type Elements_Bar = Record; 18 | export type NumberDisplayElements = Elements; 19 | 20 | export class UIElements { 21 | static el: { 22 | root: HTMLElement; 23 | uiFrame: ingameUiElement; 24 | 25 | main: Elements & Elements & Elements_Bar; 26 | numberSimple: NumberDisplayElements; 27 | numberSimpleLabel: NumberDisplayElements; 28 | numberVerbose: NumberDisplayElements; 29 | numberVerboseLabel: NumberDisplayElements; 30 | 31 | confAutoHideHeader: ToggleButtonElement; 32 | confPanels: NewListButtonElement; 33 | confNumericDisp: NewListButtonElement; 34 | confQuickHideDuration: NewListButtonElement; 35 | confPropMix: ToggleButtonElement; 36 | confAircraftModel: HTMLElement; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/inputViewer/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function diffAndSetInnerText( el: HTMLElement, text: string ) { 3 | if ( el.innerText !== text ) { 4 | el.innerText = text; 5 | } 6 | } 7 | 8 | export function diffAndForceToggleClassList( el: HTMLElement, token: string, value: boolean ) { 9 | const currentValue = el.classList.contains( token ); 10 | if ( currentValue !== value ) { 11 | el.classList.toggle( token, value ); 12 | } 13 | } 14 | 15 | export function safeCall( fn: ( ...args: T ) => R ) { 16 | return ( ...args: T ) => { 17 | try { 18 | return fn( ...args ); 19 | } catch ( e ) { 20 | const elError = document.querySelector( "#DevOverlay .error" ) as HTMLElement; 21 | elError.innerText = "" + e; 22 | } 23 | } 24 | } 25 | 26 | export function debugMsg( ...args: any[] ) { 27 | const elDebugMsg = document.querySelector( "#DevOverlay .info" ) as HTMLElement; 28 | elDebugMsg.innerText = "" + args.join( " " ); 29 | } 30 | 31 | export function appendDebugMsg( ...args: any[] ) { 32 | const elDebugMsg = document.querySelector( "#DevOverlay .info" ) as HTMLElement; 33 | elDebugMsg.innerText += "" + args.join( " " ) + "\n"; 34 | } 35 | 36 | export function setTranslate( el: HTMLElement, x: number, y: number ) { 37 | diffAndSetAttribute( el, "transform", `translate(${x}, ${y})` ); 38 | } 39 | 40 | export function zip( a: T[], b: U[] ): [T, U][] { 41 | if ( a.length != b. length ) { 42 | throw new Error( "zip: Mismatched number of items" ); 43 | } 44 | return a.map( ( aa, idx ) => [aa, b[idx]] ); 45 | } 46 | 47 | export function shallowEq( a: T[], b: T[] ) { 48 | if ( a.length != b.length ) { 49 | return false; 50 | } 51 | for ( let i = 0; i < a.length; i++ ) { 52 | if ( a[i] != b[i] ) { 53 | return false; 54 | } 55 | } 56 | return true; 57 | } 58 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | 2 | import { configureStore } from "@reduxjs/toolkit"; 3 | import { createEpicMiddleware } from "redux-observable"; 4 | 5 | import { 6 | inputViewerEpic, 7 | inputViewerReducer, 8 | } from "./inputViewer/slice"; 9 | 10 | const epicMiddleware = createEpicMiddleware(); 11 | 12 | export const store = configureStore({ 13 | reducer: inputViewerReducer, 14 | middleware: getDefaultMiddleware => getDefaultMiddleware().concat( 15 | epicMiddleware as any 16 | ), 17 | devTools: false, 18 | }); 19 | 20 | epicMiddleware.run( inputViewerEpic as any ); 21 | -------------------------------------------------------------------------------- /src/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | 2 | @function scaled($px) { 3 | @return calc(var(--screenHeight) * (#{$px}px / 2160)); 4 | } 5 | 6 | @function basic-transition($props...) { 7 | $result: (); 8 | @for $i from 1 through length($props) { 9 | $prop: nth($props, $i); 10 | $result: append($result, $prop); 11 | $result: append($result, #{"var(--animationTime) var(--animationEffect)"}); 12 | @if $i != length($props) { 13 | $result: append($result, #{","}); 14 | } 15 | } 16 | @return $result; 17 | } 18 | -------------------------------------------------------------------------------- /static/html_ui/InGamePanels/InputViewer/InputViewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 40 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | THR 86 | 0 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | #2 96 | 0 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | #3 106 | 0 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | #4 116 | 0 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | PROP 126 | 0 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | MIX 136 | 0 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | AIL 150 | 0 151 | 0 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | ELEV 163 | 0 164 | 0 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | RUD 176 | 0 177 | 0 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 0 189 | BRK 190 | 0 191 | 192 | 193 | 194 |
195 |
196 |
197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 |
231 |
232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 |
293 |
294 | 295 |
296 |
297 |
298 |
299 |
300 |
Aileron
301 |
0
302 |
Aileron Trim
303 |
0
304 |
Elevator
305 |
0
306 |
Elevator Trim
307 |
0
308 |
Rudder
309 |
0
310 |
Rudder Trim
311 |
0
312 |
Brake Left
313 |
0
314 |
Brake Right
315 |
0
316 |
Throttle 1
317 |
0
318 |
Throttle 2
319 |
0
320 |
Throttle 3
321 |
0
322 |
Throttle 4
323 |
0
324 |
Propeller 1
325 |
0
326 |
Mixture 1
327 |
0
328 |
329 |
330 |
331 | 379 |
380 |
381 |
382 | 383 | 384 | -------------------------------------------------------------------------------- /static/html_ui/InGamePanels/InputViewer/images/config.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/html_ui/InGamePanels/InputViewer/images/mixture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/html_ui/InGamePanels/InputViewer/images/propeller.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/html_ui/icons/toolbar/ICON_TOOLBAR_INPUT_VIEWER.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/manual-testing.md: -------------------------------------------------------------------------------- 1 | 2 | Core functionalities: 3 | 4 | - Stick XY 5 | - Rudder 6 | - Aileron Trim 7 | - Elevator Trim 8 | - Rudder Trim (`Ctrl + Num 0` / `Ctrl + Num Enter`) 9 | - Wheel Brake L/R 10 | - Throttle 11 | - PropMix Toggle Switch 12 | - Propeller RPM 13 | - Mixture 14 | 15 | Layout: 16 | 17 | - In Windowed mode > Change window height > Check if layout is not broken 18 | - Externalize the panel > Check if layout is not broken 19 | - Fly > Back to Main menu > Re-fly > Check if layout is not broken 20 | 21 | Engine modes: 22 | 23 | - DevMode menu bar > Windows > Aircraft Selector 24 | - Fly with single engine aircraft 25 | - Fly with twin engine aircraft (e.g., A320) 26 | - Fly with four engine aircraft (e.g., B747) 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "lib": [ 7 | "es6", 8 | "DOM" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@src/*": ["src/*"] 16 | }, 17 | "esModuleInterop": true 18 | }, 19 | "include": [ 20 | "typings/_includeAll.d.ts", 21 | "src/**/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /typings/_includeAll.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /typings/msfs/index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export {} 3 | 4 | declare global { 5 | class TemplateElement extends HTMLElement { 6 | disconnectedCallback(): void; 7 | } 8 | 9 | class ButtonElement extends TemplateElement { 10 | 11 | } 12 | 13 | class ingameUiElement extends TemplateElement { 14 | visible: boolean; 15 | } 16 | 17 | class NewListButtonElement extends ButtonElement { 18 | /** 19 | * You should set the current choice via `setCurrentValue` otherwise the 20 | * current selected index will not be properly updated. 21 | */ 22 | value: string; 23 | defaultValue: number; 24 | getCurrentValue(): number; 25 | setCurrentValue( val: number ): void; 26 | choices: string[]; 27 | valueElem?: HTMLElement; 28 | } 29 | 30 | class ToggleButtonElement extends ButtonElement { 31 | getValue(): boolean; 32 | setValue( val: boolean ): void; 33 | } 34 | 35 | interface IUIElement { 36 | connectedCallback(): void; 37 | disconnectedCallback?(): void; 38 | } 39 | 40 | function checkAutoload(): void; 41 | 42 | class Coherent { 43 | static on( eventName: string, callback: ( ...args: any[] ) => any ): void; 44 | static off( eventName: string, callback: ( ...args: any[] ) => any ): void; 45 | } 46 | 47 | class Utils { 48 | static Translate( key: string ): string | null; 49 | } 50 | 51 | interface SimVarNameTypeMap { 52 | "AILERON POSITION": number; 53 | "ELEVATOR POSITION": number; 54 | "RUDDER POSITION": number; 55 | "AILERON TRIM PCT": number; 56 | "ELEVATOR TRIM PCT": number; 57 | "RUDDER TRIM PCT": number; 58 | "BRAKE LEFT POSITION": number; 59 | "BRAKE RIGHT POSITION": number; 60 | "GENERAL ENG THROTTLE LEVER POSITION:1": number; 61 | "GENERAL ENG THROTTLE LEVER POSITION:2": number; 62 | "GENERAL ENG THROTTLE LEVER POSITION:3": number; 63 | "GENERAL ENG THROTTLE LEVER POSITION:4": number; 64 | "GENERAL ENG PROPELLER LEVER POSITION:1": number; 65 | "GENERAL ENG MIXTURE LEVER POSITION:1": number; 66 | 67 | "CATEGORY": string, 68 | "NUMBER OF ENGINES": number; 69 | 70 | "TITLE": string; 71 | "ATC MODEL": string; 72 | 73 | "AUTOPILOT AIRSPEED HOLD": boolean; 74 | 75 | // Helicopters 76 | "DISK BANK PCT:1": number; 77 | } 78 | 79 | interface SimVarUnitTypeMap { 80 | number: number; 81 | position: number; 82 | "percent over 100": number; 83 | string: string; 84 | } 85 | 86 | type SimVarName = keyof SimVarNameTypeMap; 87 | type SimVarUnit = keyof SimVarUnitTypeMap; 88 | 89 | class SimVar { 90 | static IsReady(): boolean; 91 | 92 | /** @deprecated */ 93 | static GetSimVarValue< 94 | TName extends SimVarName, 95 | TReturn extends SimVarNameTypeMap[TName] = SimVarNameTypeMap[TName] 96 | >( 97 | name: TName, unit: SimVarUnit, dataSource?: string 98 | ): TReturn; 99 | /** @deprecated */ 100 | static GetSimVarValue( 101 | name: SimVarName, unit: SimVarUnit, dataSource?: string 102 | ): T; 103 | 104 | static GetRegisteredId( 105 | name: SimVarName, unit: SimVarUnit, dataSource: string 106 | ): number; 107 | 108 | static GetSimVarValueFastReg( registeredID: number ): number; 109 | static GetSimVarValueFastRegString( registeredID: number ): string; 110 | } 111 | 112 | // dataStorage.js 113 | function GetStoredData( _key: string ): string | null; 114 | function SetStoredData( _key: string, _data: string ): string | null; 115 | function DeleteStoredData( _key: string ): string | null; 116 | function SearchStoredData( _key: string ): { key: string, data: string }[] | null; 117 | 118 | // common.js 119 | /** 120 | * Updates `textContent` if the value is changed. 121 | */ 122 | function diffAndSetText( _element: HTMLElement, _newValue: string ): void; 123 | /** 124 | * Updates `innerHTML` if the value is changed. 125 | */ 126 | function diffAndSetHTML( _element: HTMLElement, _newValue: string ): void; 127 | /** 128 | * Calls `setAttribute` if the value is changed. 129 | */ 130 | function diffAndSetAttribute( _element: HTMLElement, _attribute: string, _newValue: any ): void; 131 | } 132 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | //@ts-check 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | const webpack = require("webpack"); 8 | const FileManagerPlugin = require("filemanager-webpack-plugin"); 9 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 10 | const PnpPlugin = require("pnp-webpack-plugin"); 11 | 12 | const packageName = "spitice-ingamepanels-inputviewer"; 13 | 14 | /** @type {import("webpack").Configuration} */ 15 | const webpackConfig = { 16 | mode: "none", 17 | 18 | context: __dirname, 19 | entry: { 20 | InputViewer: "./src/InputViewer.ts", 21 | }, 22 | 23 | resolve: { 24 | extensions: [ ".ts", ".js" ], 25 | plugins: [ PnpPlugin ], 26 | }, 27 | resolveLoader: { 28 | plugins: [ 29 | PnpPlugin.moduleLoader( module ), 30 | ], 31 | }, 32 | 33 | module: { 34 | rules: [{ 35 | test: /\.ts$/i, 36 | exclude: /node_modules/, 37 | use: [{ 38 | loader: "ts-loader", 39 | options: { 40 | compilerOptions: { 41 | "transpileOnly": true, 42 | "sourceMap": true, 43 | // "declaration": false, 44 | }, 45 | }, 46 | }], 47 | }, { 48 | test: /\.s[ac]ss$/i, 49 | use: [ 50 | MiniCssExtractPlugin.loader, 51 | "css-loader", 52 | "sass-loader", 53 | ] 54 | }] 55 | }, 56 | 57 | plugins: [ 58 | new MiniCssExtractPlugin(), 59 | new FileManagerPlugin({ 60 | events: { 61 | onEnd: { 62 | copy: [ 63 | { 64 | source: "./dist/*", 65 | destination: `./Packages/${packageName}/html_ui/InGamePanels/InputViewer`, 66 | } 67 | ] 68 | } 69 | } 70 | }), 71 | new webpack.BannerPlugin({ 72 | banner: "Interested in the source code? You may want to visit our github repository: https://github.com/spitice/msfs-input-viewer", 73 | }), 74 | ], 75 | 76 | output: { 77 | filename: "[name].js", 78 | path: path.resolve( __dirname, "dist" ), 79 | }, 80 | }; 81 | 82 | module.exports = webpackConfig; 83 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | 2 | //@ts-check 3 | "use strict"; 4 | 5 | const baseConfig = require("./webpack.config"); 6 | 7 | /** @type {import("webpack").Configuration} */ 8 | const webpackConfig = { 9 | ...baseConfig, 10 | mode: "development", 11 | devtool: "cheap-source-map", 12 | }; 13 | 14 | module.exports = webpackConfig; 15 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | 2 | //@ts-check 3 | "use strict"; 4 | 5 | const TerserPlugin = require("terser-webpack-plugin"); 6 | 7 | const baseConfig = require("./webpack.config"); 8 | 9 | // Mitigating the type error 10 | /** @type {any} */ 11 | const terserPlugin = new TerserPlugin({ 12 | extractComments: false, // Avoid ".js.LICENSE.txt" to be generated 13 | terserOptions: { 14 | mangle: false, 15 | output: { 16 | // beautify: true, 17 | }, 18 | }, 19 | }); 20 | 21 | /** @type {import("webpack").Configuration} */ 22 | const webpackConfig = { 23 | ...baseConfig, 24 | 25 | mode: "production", 26 | 27 | optimization: { 28 | minimize: true, 29 | minimizer: [ 30 | terserPlugin, 31 | ], 32 | }, 33 | }; 34 | 35 | module.exports = webpackConfig; 36 | --------------------------------------------------------------------------------