├── .editorconfig ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── assets ├── css │ ├── aframe.less │ └── kbd.css ├── fonts │ ├── Roboto-msdf.json │ └── Roboto-msdf.png ├── icons │ ├── README.md │ ├── linux │ │ └── skybrush.png │ ├── mac │ │ └── skybrush.icns │ ├── splash.png │ └── win │ │ └── skybrush.ico ├── img │ └── logo.png ├── models │ ├── flapper-drone.obj │ └── quadcopter.obj └── shows │ └── demo.json ├── config ├── baseline.ts ├── default.ts ├── demo.ts ├── index.ts └── webapp.ts ├── electron-builder.json ├── i18next-parser.config.json ├── index.html ├── launcher.mjs ├── package-lock.json ├── package.json ├── patches ├── aframe-environment-component+1.3.7.patch ├── express+4.21.2.patch ├── meshline+3.1.0.patch └── react-chartjs-2+2.11.2.patch ├── scripts └── skyc-to-json.mjs ├── src ├── aframe │ ├── components │ │ ├── drone-flock.js │ │ └── glow-material.js │ ├── index.js │ ├── materials │ │ └── GlowingMaterial.js │ └── primitives │ │ └── drone-flock.js ├── app.tsx ├── components │ ├── AudioController.tsx │ ├── CameraSelectorChip.tsx │ ├── CentralHelperPanel.tsx │ ├── DragDropHandler.tsx │ ├── LoadingScreen.tsx │ ├── MainTopLevelView.tsx │ ├── PageLoadingIndicator.tsx │ ├── Sidebar.tsx │ ├── SkybrushLogo.tsx │ ├── SplashScreen.tsx │ ├── WelcomeScreen.tsx │ ├── WindowDragMoveArea.tsx │ ├── WindowTitleManager.tsx │ └── buttons │ │ ├── OpenButton.tsx │ │ ├── SettingsButton.tsx │ │ ├── TrackDronesButton.tsx │ │ ├── VolumeButton.tsx │ │ └── ZoomOutButton.tsx ├── constants.ts ├── custom.d.ts ├── desktop │ ├── index.js │ ├── launcher │ │ ├── api-v1.mjs │ │ ├── app-menu.mjs │ │ ├── dialogs.mjs │ │ ├── file-opener.mjs │ │ ├── http-server.mjs │ │ ├── index.mjs │ │ ├── ipc.mjs │ │ ├── media-buffers.mjs │ │ ├── media-protocol.mjs │ │ ├── show-loader.mjs │ │ ├── utils.mjs │ │ └── window-title.mjs │ └── preload │ │ ├── index.js │ │ └── ipc.js ├── features │ ├── audio │ │ ├── selectors.ts │ │ └── slice.ts │ ├── hotkeys │ │ ├── AppHotkeys.tsx │ │ ├── HotkeyDialog.tsx │ │ ├── ShowHotkeysDialogButton.tsx │ │ ├── keymap.ts │ │ ├── selectors.ts │ │ ├── slice.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── index.ts │ ├── playback │ │ ├── actions.ts │ │ ├── selectors.ts │ │ └── slice.ts │ ├── settings │ │ ├── DroneModelSelector.tsx │ │ ├── DroneSizeSlider.tsx │ │ ├── LanguageSelector.tsx │ │ ├── PlaybackSpeedSelector.tsx │ │ ├── ScenerySelector.tsx │ │ ├── ThreeDViewSettingToggles.tsx │ │ ├── actions.ts │ │ ├── selectors.ts │ │ ├── slice.ts │ │ └── types.ts │ ├── sharing │ │ ├── ShareButton.tsx │ │ └── actions.ts │ ├── show │ │ ├── actions.ts │ │ ├── async.ts │ │ ├── selectors.ts │ │ ├── slice.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── sidebar │ │ └── slice.ts │ ├── three-d │ │ ├── actions.ts │ │ ├── saga.ts │ │ ├── selectors.ts │ │ └── slice.ts │ ├── ui │ │ ├── actions.ts │ │ ├── modes.ts │ │ ├── selectors.ts │ │ └── slice.ts │ └── validation │ │ ├── AltitudeChartPanel.tsx │ │ ├── ChartPanel.tsx │ │ ├── HorizontalAccelerationChartPanel.tsx │ │ ├── HorizontalVelocityChartPanel.tsx │ │ ├── ProximityChartPanel.tsx │ │ ├── ToggleValidationModeButton.tsx │ │ ├── VerticalAccelerationChartPanel.tsx │ │ ├── VerticalVelocityChartPanel.tsx │ │ ├── actions.ts │ │ ├── closest-pair.ts │ │ ├── constants.ts │ │ ├── items.ts │ │ ├── panels.ts │ │ ├── selectors.ts │ │ ├── slice.ts │ │ ├── types.ts │ │ └── utils.ts ├── hooks │ ├── store.ts │ └── useDarkMode.ts ├── i18n │ ├── LanguageWatcher.tsx │ ├── de.json │ ├── en.json │ ├── hu.json │ ├── index.ts │ ├── it.json │ ├── ja.json │ ├── pl.json │ ├── ru.json │ └── zh-Hans.json ├── icons │ └── VirtualReality.tsx ├── index.tsx ├── sagas │ ├── index.ts │ └── loader.ts ├── startup.tsx ├── store.ts ├── theme.ts ├── utils │ ├── formatters.ts │ ├── platform.ts │ └── types.ts ├── views │ ├── player │ │ ├── BottomOverlay.tsx │ │ ├── CameraSelectorChip.tsx │ │ ├── CoordinateSystemAxes.tsx │ │ ├── OverlayVisibilityController.ts │ │ ├── Overlays.tsx │ │ ├── PlaybackSlider.tsx │ │ ├── PlayerView.tsx │ │ ├── Scenery.tsx │ │ ├── ThreeDView.tsx │ │ ├── TopOverlay.tsx │ │ ├── constants.ts │ │ ├── index.ts │ │ └── utils.ts │ └── validation │ │ ├── ChartGrid.tsx │ │ ├── PanelToggleChip.tsx │ │ ├── ValidationHeader.tsx │ │ ├── ValidationSidebar.tsx │ │ ├── ValidationView.tsx │ │ └── index.ts └── window.ts ├── tsconfig.json ├── types └── config │ └── index.d.ts └── webpack ├── base.config.js ├── browser.config.js ├── dist.config.js ├── electron.config.js ├── helpers.js ├── launcher.config.js └── preload.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,jsx,css,less}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode settings 2 | .vscode 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Generated API documentation 33 | doc/api/ 34 | 35 | # Stuff packed by webpack 36 | build/ 37 | dist/ 38 | webpack-stats.json 39 | 40 | # Typescript type definitions 41 | typings/ 42 | 43 | # Dependency directories 44 | node_modules 45 | jspm_packages 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Environment file containing sensitive data (e.g.: API keys) 54 | .env 55 | 56 | # Show files used as examples during testing 57 | assets/shows/ 58 | 59 | # Large icons 60 | assets/icons/*512.png 61 | assets/icons/*1024.png 62 | 63 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | @collmot:registry=https://npm.collmot.com 3 | @skybrush:registry=https://npm.collmot.com 4 | legacy-peer-deps=true 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skybrush Viewer 2 | 3 | This repo contains the source code of Skybrush Viewer, the viewer app for 4 | Skybrush drone shows. 5 | 6 | ## Usage 7 | 8 | Make sure that you are using a recent LTS Node.js release, then run the 9 | following commands: 10 | 11 | ``` 12 | npm install 13 | npm run start:electron 14 | ``` 15 | 16 | You may also run the app inside a browser environment: 17 | 18 | ``` 19 | npm install 20 | npm run start 21 | ``` 22 | 23 | Navigate to `http://localhost:8080` after startup to use the app in 24 | a browser. Note that not all features may be available in a browser 25 | environment. 26 | 27 | ## Support 28 | 29 | For any support questions please contact us on our [Discord server](https://skybrush.io/r/discord). 30 | 31 | ## License 32 | 33 | Copyright 2020-2024 CollMot Robotics Ltd. 34 | 35 | Skybrush Viewer is free software: you can redistribute it and/or modify it under 36 | the terms of the GNU General Public License as published by the Free Software 37 | Foundation, either version 3 of the License, or (at your option) any later 38 | version. 39 | 40 | Skybrush Viewer is distributed in the hope that it will be useful, but WITHOUT 41 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 42 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 43 | more details. 44 | 45 | You should have received a copy of the GNU General Public License along with 46 | this program. If not, see . 47 | -------------------------------------------------------------------------------- /assets/css/aframe.less: -------------------------------------------------------------------------------- 1 | /* Tweaks for A-Frame's dialogs and modals to fit our styling */ 2 | 3 | .a-dialog { 4 | background-color: unset; 5 | font-family: unset; 6 | font-size: unset; 7 | } 8 | 9 | .a-dialog-text { 10 | font-size: unset; 11 | } 12 | 13 | .a-dialog-button { 14 | font-size: unset; 15 | border-radius: 30px; 16 | text-transform: uppercase; 17 | font-weight: bold; 18 | } 19 | 20 | .a-dialog-ok-button { 21 | background-color: #F44336; /* Material-UI red[500] */ 22 | color: white; 23 | } 24 | -------------------------------------------------------------------------------- /assets/css/kbd.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS styling for tags. 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | kbd { 27 | font-family: 'Fira Sans', Arial, sans-serif; 28 | display: inline-block; 29 | border-radius: 3px; 30 | padding: 0px 4px; 31 | box-shadow: 1px 1px 1px #777; 32 | margin: 2px; 33 | background: #eee; 34 | font-weight: normal; 35 | color: #555; 36 | cursor: pointer; 37 | 38 | /* Prevent selection */ 39 | -webkit-touch-callout: none; 40 | -webkit-user-select: none; 41 | -khtml-user-select: none; 42 | -moz-user-select: none; 43 | -ms-user-select: none; 44 | user-select: none; 45 | } 46 | 47 | kbd:hover, kbd:hover * { 48 | color: black; 49 | /* box-shadow: 1px 1px 1px #333; */ 50 | } 51 | kbd:active, kbd:active * { 52 | color: black; 53 | box-shadow: 1px 1px 0px #ddd inset; 54 | } 55 | 56 | kbd kbd { 57 | padding: 0px; 58 | margin: 0 1px; 59 | box-shadow: 0px 0px 0px black; 60 | vertical-align: baseline; 61 | background: none; 62 | } 63 | 64 | kbd kbd:hover { 65 | box-shadow: 0px 0px 0px black; 66 | } 67 | 68 | kbd:active kbd { 69 | box-shadow: 0px 0px 0px black; 70 | background: none; 71 | } 72 | 73 | /* Deep blue */ 74 | kbd.deep-blue, .deep-blue kbd { 75 | background: steelblue; 76 | color: #eee; 77 | } 78 | 79 | kbd.deep-blue:hover, kbd.deep-blue:hover *, .deep-blue kbd:hover { 80 | color: white; 81 | } 82 | 83 | /* Dark apple */ 84 | kbd.dark-apple, .dark-apple kbd { 85 | background: black; 86 | color: #ddd; 87 | } 88 | 89 | kbd.dark-apple:hover, kbd.dark-apple:hover *, .dark-apple kbd:hover { 90 | color: white; 91 | } 92 | 93 | /* Type writer */ 94 | kbd.type-writer, .type-writer kbd { 95 | border-radius: 10px; 96 | background: #333; 97 | color: white; 98 | } 99 | 100 | /* Keyboard tag styling speciifc for dark mode */ 101 | kbd { 102 | background: #616161 !important; 103 | box-shadow: 1px 1px 1px #333; 104 | color: #ddd !important; 105 | } 106 | 107 | kbd:hover, 108 | kbd:hover * { 109 | color: white !important; 110 | } 111 | -------------------------------------------------------------------------------- /assets/fonts/Roboto-msdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/fonts/Roboto-msdf.png -------------------------------------------------------------------------------- /assets/icons/README.md: -------------------------------------------------------------------------------- 1 | Current icon was generated with the _Launcher icon generator_ from the 2 | _Android Asset Studio_: 3 | 4 | https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html 5 | 6 | Background color: #dc3545 ("Skybrush red", probably from the Bootstrap 7 | palette) 8 | Clipart: search 9 | Font (if we need): Allura 10 | Padding (if we need text): 0% 11 | 12 | Take the 512px version, scale it up to 1024px and add rounded corners in 13 | GIMP with a corner radius of 180px. Then upload the image to the following 14 | URL to get it converted to .icns format: 15 | 16 | https://cloudconvert.com/ 17 | 18 | Also add rounded corners to the 512px version with a corner radius of 90px, 19 | and upload this icon to the following URL to get it converted to .ico format 20 | for Windows: 21 | 22 | https://icoconvert.com/ 23 | -------------------------------------------------------------------------------- /assets/icons/linux/skybrush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/linux/skybrush.png -------------------------------------------------------------------------------- /assets/icons/mac/skybrush.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/mac/skybrush.icns -------------------------------------------------------------------------------- /assets/icons/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/splash.png -------------------------------------------------------------------------------- /assets/icons/win/skybrush.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/icons/win/skybrush.ico -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skybrush-io/viewer/3e240765797d4b258763034372ddd3e89085ef78/assets/img/logo.png -------------------------------------------------------------------------------- /config/baseline.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Baseline values for the configuration options of the application. 3 | */ 4 | 5 | import { type Config } from 'config'; 6 | 7 | const baseline: Config = { 8 | buttons: { 9 | playbackHint: false, 10 | }, 11 | io: { 12 | localFiles: true, 13 | }, 14 | language: { 15 | default: 'en', 16 | enabled: ['en', 'hu', 'zh-Hans'], 17 | fallback: 'en', 18 | }, 19 | modes: { 20 | deepLinking: false, 21 | player: true, 22 | validation: true, 23 | vr: false, 24 | }, 25 | preloadedShow: {}, 26 | startAutomatically: true, 27 | useWelcomeScreen: true, 28 | }; 29 | 30 | export default baseline; 31 | -------------------------------------------------------------------------------- /config/default.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Empty configuration override, which preserves all the defaults. 3 | */ 4 | 5 | import { type ConfigOverrides } from 'config-overrides'; 6 | 7 | const overrides: ConfigOverrides = {}; 8 | 9 | export default overrides; 10 | -------------------------------------------------------------------------------- /config/demo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Default application configuration at startup to run a local hardcoded demo. 3 | */ 4 | 5 | import type { ShowSpecification } from '@skybrush/show-format'; 6 | import { type ConfigOverrides } from 'config-overrides'; 7 | 8 | import audio from '~/../assets/shows/demo.mp3'; 9 | 10 | const show = async (): Promise => 11 | import( 12 | /* webpackChunkName: "show" */ '~/../assets/shows/demo.json' 13 | ) as any as ShowSpecification; 14 | 15 | const overrides: ConfigOverrides = { 16 | buttons: { 17 | playbackHint: true, 18 | }, 19 | electronBuilder: { 20 | productName: 'Skybrush Viewer Demo', 21 | }, 22 | io: { 23 | localFiles: false, 24 | }, 25 | modes: { 26 | deepLinking: true, 27 | validation: false, 28 | }, 29 | preloadedShow: { 30 | audio, 31 | show, 32 | }, 33 | useWelcomeScreen: false, 34 | }; 35 | 36 | export default overrides; 37 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file File for merging the default config with overrides from external files. 3 | */ 4 | import mergeWith from 'lodash-es/mergeWith.js'; 5 | 6 | import { type Config } from 'config'; 7 | import overrides from 'config-overrides'; 8 | 9 | import baseline from './baseline'; 10 | 11 | // Completely replace arrays in the configuration instead of merging them. 12 | const customizer = ( 13 | defaultValue: unknown, 14 | overrideValue: T 15 | ): T | undefined => { 16 | if (Array.isArray(defaultValue) && Array.isArray(overrideValue)) { 17 | return overrideValue; 18 | } 19 | }; 20 | 21 | const merged: Config = mergeWith(baseline, overrides, customizer); 22 | 23 | export default merged; 24 | -------------------------------------------------------------------------------- /config/webapp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Default application configuration at startup when running as a web app. 3 | */ 4 | 5 | import { type ConfigOverrides } from 'config-overrides'; 6 | 7 | const overrides: ConfigOverrides = { 8 | buttons: { 9 | playbackHint: true, 10 | }, 11 | io: { 12 | localFiles: false, 13 | }, 14 | modes: { 15 | deepLinking: true, 16 | validation: false, 17 | }, 18 | startAutomatically: false, 19 | useWelcomeScreen: false, 20 | }; 21 | 22 | export default overrides; 23 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "com.collmot.skybrush.viewer", 3 | "productName": "Skybrush Viewer", 4 | 5 | "artifactName": "${productName} ${version}.${ext}", 6 | 7 | "files": ["!**/*", "package.json", { "from": "build" }], 8 | 9 | "fileAssociations": [{ 10 | "ext": "skyc", 11 | "description": "Skybrush compiled show file", 12 | "role": "Viewer" 13 | }], 14 | 15 | "linux": { 16 | "category": "Utility", 17 | "target": { 18 | "target": "AppImage", 19 | "arch": "x64" 20 | } 21 | }, 22 | 23 | "mac": { 24 | "category": "public.app-category.utilities", 25 | "target": { 26 | "target": "dmg", 27 | "arch": "universal" 28 | } 29 | }, 30 | 31 | "win": { 32 | "target": { 33 | "target": "portable", 34 | "arch": "x64" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /i18next-parser.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": ["src/**/*.{ts,tsx}"], 3 | "locales": ["en", "hu"], 4 | "sort": true, 5 | "output": "src/i18n/$LOCALE.json" 6 | } 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /launcher.mjs: -------------------------------------------------------------------------------- 1 | import main from './src/desktop/launcher/index.mjs'; 2 | 3 | await main(); 4 | -------------------------------------------------------------------------------- /patches/aframe-environment-component+1.3.7.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/aframe-environment-component/index.js b/node_modules/aframe-environment-component/index.js 2 | index 33781bf..778f3ce 100644 3 | --- a/node_modules/aframe-environment-component/index.js 4 | +++ b/node_modules/aframe-environment-component/index.js 5 | @@ -258,7 +258,6 @@ AFRAME.registerComponent('environment', { 6 | Object.assign(this.environmentData, this.data); 7 | Object.assign(this.environmentData, this.presets[this.data.preset]); 8 | Object.assign(this.environmentData, this.el.components.environment.attrValue); 9 | - console.log(this.environmentData); 10 | } 11 | 12 | var skyType = this.environmentData.skyType; 13 | @@ -453,7 +452,6 @@ AFRAME.registerComponent('environment', { 14 | str += ', '; 15 | } 16 | str += '}'; 17 | - console.log(str); 18 | }, 19 | 20 | // dumps current component settings to console. 21 | @@ -497,7 +495,6 @@ AFRAME.registerComponent('environment', { 22 | } 23 | } 24 | } 25 | - console.log('%c' + params.join('; '), 'color: #f48;font-weight:bold'); 26 | }, 27 | 28 | // Custom Math.random() with seed. Given this.environmentData.seed and x, it always returns the same "random" number 29 | -------------------------------------------------------------------------------- /patches/express+4.21.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/express/lib/view.js b/node_modules/express/lib/view.js 2 | index c08ab4d..f49ddea 100644 3 | --- a/node_modules/express/lib/view.js 4 | +++ b/node_modules/express/lib/view.js 5 | @@ -78,7 +78,7 @@ function View(name, options) { 6 | debug('require "%s"', mod) 7 | 8 | // default engine export 9 | - var fn = require(mod).__express 10 | + var fn = null // require(mod).__express 11 | 12 | if (typeof fn !== 'function') { 13 | throw new Error('Module "' + mod + '" does not provide a view engine.') 14 | -------------------------------------------------------------------------------- /patches/react-chartjs-2+2.11.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-chartjs-2/es/index.js b/node_modules/react-chartjs-2/es/index.js 2 | index 7ca177d..a3c9be6 100644 3 | --- a/node_modules/react-chartjs-2/es/index.js 4 | +++ b/node_modules/react-chartjs-2/es/index.js 5 | @@ -249,7 +249,9 @@ var ChartComponent = /*#__PURE__*/function (_React$Component) { 6 | if (current && current.type === next.type && next.data) { 7 | // Be robust to no data. Relevant for other update mechanisms as in chartjs-plugin-streaming. 8 | // The data array must be edited in place. As chart.js adds listeners to it. 9 | - current.data.splice(next.data.length); 10 | + if (current.data.length > next.data.length) { 11 | + current.data.splice(next.data.length); 12 | + } 13 | next.data.forEach(function (point, pid) { 14 | current.data[pid] = next.data[pid]; 15 | }); 16 | -------------------------------------------------------------------------------- /scripts/skyc-to-json.mjs: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | import process from 'node:process'; 3 | 4 | import { program } from 'commander'; 5 | import showFormat from '@skybrush/show-format'; 6 | 7 | const { loadCompiledShow } = showFormat; 8 | 9 | program 10 | .storeOptionsAsProperties(false) 11 | .requiredOption('-i, --input ', 'name of the input file') 12 | .requiredOption('-o, --output ', 'name of the output file') 13 | .parse(process.argv); 14 | 15 | async function main(options) { 16 | const data = await fs.readFile(options.input); 17 | const show = await loadCompiledShow(data); 18 | const output = JSON.stringify(show, null, 2); 19 | await fs.writeFile(options.output, output); 20 | } 21 | 22 | await main(program.opts()); 23 | -------------------------------------------------------------------------------- /src/aframe/components/glow-material.js: -------------------------------------------------------------------------------- 1 | import AFrame from '@skybrush/aframe-components'; 2 | 3 | import GlowingMaterial from '../materials/GlowingMaterial'; 4 | 5 | AFrame.registerComponent('glow-material', { 6 | schema: { 7 | color: { type: 'color', is: 'uniform', default: '#0080ff' }, 8 | falloff: { type: 'number', is: 'uniform', default: 0.1 }, 9 | internalRadius: { type: 'number', is: 'uniform', default: 6 }, 10 | sharpness: { type: 'number', is: 'uniform', default: 1 }, 11 | opacity: { type: 'number', is: 'uniform', default: 1 }, 12 | }, 13 | 14 | init() { 15 | this.material = new GlowingMaterial(this._getMaterialProperties()); 16 | this.el.addEventListener('loaded', () => { 17 | const mesh = this.el.getObject3D('mesh'); 18 | if (mesh) { 19 | mesh.material = this.material; 20 | } 21 | }); 22 | }, 23 | 24 | update() { 25 | this.material?.setValues(this._getMaterialProperties()); 26 | }, 27 | 28 | _getMaterialProperties() { 29 | const { color, falloff, internalRadius, sharpness, opacity } = this.data; 30 | return { 31 | color, 32 | falloff, 33 | internalRadius, 34 | sharpness, 35 | opacity, 36 | }; 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/aframe/index.js: -------------------------------------------------------------------------------- 1 | import AFrame from '@skybrush/aframe-components'; 2 | 3 | import 'aframe-environment-component'; 4 | import 'aframe-look-at-component'; 5 | 6 | import '@skybrush/aframe-components/advanced-camera-controls'; 7 | import '@skybrush/aframe-components/deallocate'; 8 | import '@skybrush/aframe-components/meshline'; 9 | import { createSyncPoseWithStoreComponent } from '@skybrush/aframe-components/factories'; 10 | 11 | import './components/drone-flock'; 12 | import './components/glow-material'; 13 | 14 | import './primitives/drone-flock'; 15 | 16 | AFrame.registerComponent( 17 | 'sync-pose-with-store', 18 | createSyncPoseWithStoreComponent({ 19 | getCameraPose() {}, 20 | 21 | setCameraPose() {}, 22 | }) 23 | ); 24 | 25 | // eslint-disable-next-line unicorn/prefer-export-from 26 | export default AFrame; 27 | -------------------------------------------------------------------------------- /src/aframe/primitives/drone-flock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A-Frame primitive that creates a drone flock entity that will contain the 3 | * individual drones. 4 | */ 5 | 6 | import AFrame from '@skybrush/aframe-components'; 7 | 8 | AFrame.registerPrimitive('a-drone-flock', { 9 | // Attaches the 'drone-flock' component by default. 10 | defaultComponents: { 11 | 'drone-flock': {}, 12 | }, 13 | mappings: { 14 | 'drone-model': 'drone-flock.droneModel', 15 | 'drone-radius': 'drone-flock.droneRadius', 16 | indoor: 'drone-flock.indoor', 17 | 'label-color': 'drone-flock.labelColor', 18 | 'scale-labels': 'drone-flock.scaleLabels', 19 | 'show-glow': 'drone-flock.showGlow', 20 | 'show-labels': 'drone-flock.showLabels', 21 | 'show-yaw': 'drone-flock.showYaw', 22 | size: 'drone-flock.size', 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import delay from 'delay'; 2 | import React from 'react'; 3 | import { Toaster } from 'react-hot-toast'; 4 | import { Provider as StoreProvider } from 'react-redux'; 5 | import { PersistGate } from 'redux-persist/es/integration/react'; 6 | 7 | import CssBaseline from '@mui/material/CssBaseline'; 8 | import { StyledEngineProvider } from '@mui/material/styles'; 9 | 10 | import DragDropHandler from './components/DragDropHandler'; 11 | import MainTopLevelView from './components/MainTopLevelView'; 12 | import Sidebar from './components/Sidebar'; 13 | import SplashScreen from './components/SplashScreen'; 14 | import WindowTitleManager from './components/WindowTitleManager'; 15 | 16 | import AppHotkeys from './features/hotkeys/AppHotkeys'; 17 | import { loadShowFromRequest } from './features/show/slice'; 18 | import { type ShowLoadingRequest } from './features/show/types'; 19 | import rootSaga from './sagas'; 20 | import { persistor, store } from './store'; 21 | import ThemeProvider, { toastOptions } from './theme'; 22 | 23 | import '~/../assets/css/aframe.less'; 24 | import '~/../assets/css/kbd.css'; 25 | 26 | import '@fontsource/fira-sans/400.css'; 27 | import '@fontsource/fira-sans/500.css'; 28 | import 'react-cover-page/themes/dark.css'; 29 | import LanguageWatcher from './i18n/LanguageWatcher'; 30 | import HotkeyDialog from './features/hotkeys/HotkeyDialog'; 31 | 32 | interface AppProps { 33 | readonly initialShow?: ShowLoadingRequest; 34 | } 35 | 36 | const App = ({ initialShow }: AppProps) => { 37 | const waitForTopLevelView = React.useCallback(async () => { 38 | // Start the root saga 39 | store.runSaga(rootSaga); 40 | 41 | // Load the initial show file (if any) 42 | if (initialShow) { 43 | store.dispatch(loadShowFromRequest(initialShow)); 44 | } 45 | 46 | // Give some time for the 3D scene to initialize itself 47 | await delay(1000); 48 | }, [initialShow]); 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | {(bootstrapped) => ( 57 | <> 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | )} 69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default App; 77 | -------------------------------------------------------------------------------- /src/components/AudioController.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Component that controls the audio playback synchronized to the 3 | * visuals. 4 | */ 5 | 6 | import React, { useCallback, useEffect, useRef } from 'react'; 7 | import toast from 'react-hot-toast'; 8 | import { connect } from 'react-redux'; 9 | 10 | import { 11 | notifyAudioCanPlay, 12 | notifyAudioMetadataLoaded, 13 | notifyAudioSeeked, 14 | notifyAudioSeeking, 15 | } from '~/features/audio/slice'; 16 | import { 17 | getElapsedSecondsGetter, 18 | isAdjustingPlaybackPosition, 19 | isPlayingInRealTime, 20 | } from '~/features/playback/selectors'; 21 | import type { RootState } from '~/store'; 22 | 23 | interface AudioControllerProps { 24 | readonly elapsedSecondsGetter: () => number; 25 | readonly muted: boolean; 26 | readonly onCanPlay: () => void; 27 | readonly onLoadedMetadata: () => void; 28 | readonly onSeeked: () => void; 29 | readonly onSeeking: () => void; 30 | readonly playing: boolean; 31 | readonly url?: string; 32 | } 33 | 34 | const AudioController = ({ 35 | elapsedSecondsGetter, 36 | muted, 37 | onCanPlay, 38 | onLoadedMetadata, 39 | onSeeked, 40 | onSeeking, 41 | playing, 42 | url, 43 | }: AudioControllerProps) => { 44 | const audioRef = useRef(null); 45 | const onError = useCallback(() => { 46 | toast.error('Error while playing audio; playback stopped.'); 47 | 48 | if (audioRef?.current) { 49 | console.error(audioRef.current.error); 50 | } 51 | }, [audioRef]); 52 | 53 | // Effect that takes care of stopping / starting the audio and re-syncing the 54 | // playback position when needed 55 | useEffect(() => { 56 | if (audioRef.current) { 57 | if (playing) { 58 | // TODO(ntamas): there is a hardcoded delay between the audio and the 59 | // visuals. I don't know why it's needed or whether it varies from 60 | // machine to machine. We need to test it. 61 | audioRef.current.currentTime = elapsedSecondsGetter() + 0.15; 62 | void audioRef.current.play(); 63 | } else { 64 | audioRef.current.pause(); 65 | } 66 | } 67 | }, [elapsedSecondsGetter, playing]); 68 | 69 | return url ? ( 70 |