├── .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 | 
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 | 
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 | 
61 |
62 | ### Configuration
63 |
64 | Click the "gear" button located at the bottom left of the panel to open the configuration menu.
65 |
66 | 
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 | 
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 | 
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 | 
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 |
77 |
78 |
140 |
194 |
195 |
196 |
197 |
230 |
231 |
232 |
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 |
4 |
--------------------------------------------------------------------------------
/static/html_ui/InGamePanels/InputViewer/images/mixture.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/html_ui/InGamePanels/InputViewer/images/propeller.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/html_ui/icons/toolbar/ICON_TOOLBAR_INPUT_VIEWER.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------