commit 6074a5739deec73335304a4da6f158295a9c497b Author: moriy <786332118@qq.com> Date: Tue Apr 28 17:24:58 2026 +0800 Initialize web-test frontend diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84bc221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies and local runtimes +node_modules/ +**/node_modules/ +venv/ +**/venv/ +.venv/ +**/.venv/ +offline-deps/python-runtime/ + +# Build outputs and generated assets +dist/ +**/dist/ +web-test/public/cesium/ +coverage/ +**/coverage/ +__pycache__/ +**/__pycache__/ +*.py[cod] + +# Local environment and runtime logs +.env +.env.* +!.env.example +service/.env.local +*.log +*.err.log +*.out.log + +# Local/private reference material +docs/ + +# OS/editor files +.DS_Store +Thumbs.db +.idea/ +.vscode/ diff --git a/web-test/README.md b/web-test/README.md new file mode 100644 index 0000000..8758859 --- /dev/null +++ b/web-test/README.md @@ -0,0 +1,15 @@ +# Cesium Web Test + +This frontend is the local Cesium test client. + +- Direct dependency: `cesium` +- Dev server: `vite` +- Local simulation service integration +- Trajectory rendering and playback +- ArcRotate camera around the aircraft + +Run: + +```powershell +npm.cmd run web:dev +``` diff --git a/web-test/index.html b/web-test/index.html new file mode 100644 index 0000000..e73ccd2 --- /dev/null +++ b/web-test/index.html @@ -0,0 +1,115 @@ + + + + + + Cesium Reset Baseline + + +
+
+
Flight Sim Prototype
+
+ Service + Checking +
+
+ Aircraft + - +
+
+ Simulation + Waiting +
+
+ Playback + Idle +
+
+ + + + +
+
+
Cobra Controller
+
+ + + + + + + + +
+
+
+ + +
+
+ + + + +
+
+
+
F-22 Cobra Telemetry
+ +
No simulation
+
+
+
+ + + diff --git a/web-test/package-lock.json b/web-test/package-lock.json new file mode 100644 index 0000000..59576a3 --- /dev/null +++ b/web-test/package-lock.json @@ -0,0 +1,1462 @@ +{ + "name": "flight-sim-cesium-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flight-sim-cesium-web", + "version": "0.1.0", + "dependencies": { + "cesium": "^1.138.0" + }, + "devDependencies": { + "vite": "^7.2.4" + } + }, + "node_modules/@cesium/engine": { + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-24.0.0.tgz", + "integrity": "sha512-zJ2gl0tyw/FFhBtvp6UYw+0JQJb2J9EiTJYvVSndc6+6qPR5GHLFzFjA1msLLxucfcpc7uI9R2pXNEPluheR/g==", + "license": "Apache-2.0", + "dependencies": { + "@cesium/wasm-splats": "^0.1.0-alpha.2", + "@spz-loader/core": "0.3.1", + "@tweenjs/tween.js": "^25.0.0", + "@zip.js/zip.js": "^2.8.1", + "autolinker": "^4.0.0", + "bitmap-sdf": "^1.0.3", + "dompurify": "^3.3.0", + "draco3d": "^1.5.1", + "earcut": "^3.0.0", + "grapheme-splitter": "^1.0.4", + "jsep": "^1.3.8", + "kdbush": "^4.0.1", + "ktx-parse": "^1.0.0", + "lerc": "^2.0.0", + "mersenne-twister": "^1.1.0", + "meshoptimizer": "^1.0.1", + "pako": "^2.0.4", + "protobufjs": "^8.0.0", + "rbush": "^4.0.1", + "topojson-client": "^3.1.0", + "urijs": "^1.19.7" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@cesium/wasm-splats": { + "version": "0.1.0-alpha.2", + "resolved": "https://registry.npmjs.org/@cesium/wasm-splats/-/wasm-splats-0.1.0-alpha.2.tgz", + "integrity": "sha512-t9pMkknv31hhIbLpMa8yPvmqfpvs5UkUjgqlQv9SeO8VerCXOYnyP8/486BDaFrztM0A7FMbRjsXtNeKvqQghA==", + "license": "Apache-2.0" + }, + "node_modules/@cesium/widgets": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-14.5.0.tgz", + "integrity": "sha512-h/hKVooXyOtUQJUtrfyBFGMIXb+Q3RLwqE6FzXfzyC0JQuuThLXCz8nRzCPbyiRuAR/aAYT/jbbhQrUxfiWqhQ==", + "license": "Apache-2.0", + "dependencies": { + "@cesium/engine": "^24.0.0", + "nosleep.js": "^0.12.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@spz-loader/core": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@spz-loader/core/-/core-0.3.1.tgz", + "integrity": "sha512-8qJ1WIBXaJu8HjnJAjYniE0kYcr0kCe5Hp7kDzYiGVvvd7zyrOBwbF5imoW5mvwx1Qba0hxGEK5R9jEoaHKJFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=16", + "pnpm": ">=8" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.26", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", + "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/autolinker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.1.5.tgz", + "integrity": "sha512-vEfYZPmvVOIuE567XBVCsx8SBgOYtjB2+S1iAaJ+HgH+DNjAcrHem2hmAeC9yaNGWayicv4yR+9UaJlkF3pvtw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "engines": { + "pnpm": ">=10.10.0" + } + }, + "node_modules/bitmap-sdf": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", + "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", + "license": "MIT" + }, + "node_modules/cesium": { + "version": "1.140.0", + "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.140.0.tgz", + "integrity": "sha512-3RvW0rvZWuXiS6regtNE5u9vt0uXohgpsRBIo6Qc922IIIamkitYiEdr4fg+u4qX4EoK9xS3BosCza7iPOExEQ==", + "license": "Apache-2.0", + "workspaces": [ + "packages/engine", + "packages/widgets", + "packages/sandcastle" + ], + "dependencies": { + "@cesium/engine": "^24.0.0", + "@cesium/widgets": "^14.5.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "license": "MIT" + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, + "node_modules/ktx-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz", + "integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==", + "license": "MIT" + }, + "node_modules/lerc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-2.0.0.tgz", + "integrity": "sha512-7qo1Mq8ZNmaR4USHHm615nEW2lPeeWJ3bTyoqFbd35DLx0LUH7C6ptt5FDCTAlbIzs3+WKrk5SkJvw8AFDE2hg==", + "license": "Apache-2.0" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/mersenne-twister": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==", + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.1.tgz", + "integrity": "sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", + "license": "MIT", + "dependencies": { + "quickselect": "^3.0.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/web-test/package.json b/web-test/package.json new file mode 100644 index 0000000..20692e3 --- /dev/null +++ b/web-test/package.json @@ -0,0 +1,18 @@ +{ + "name": "flight-sim-cesium-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "npm run copy:cesium && vite build", + "preview": "vite preview --host 127.0.0.1 --port 4173", + "copy:cesium": "node ../tools/copy-cesium-assets.mjs" + }, + "dependencies": { + "cesium": "^1.138.0" + }, + "devDependencies": { + "vite": "^7.2.4" + } +} diff --git a/web-test/src/api/simulationService.js b/web-test/src/api/simulationService.js new file mode 100644 index 0000000..3b1caf9 --- /dev/null +++ b/web-test/src/api/simulationService.js @@ -0,0 +1,161 @@ +const DEFAULT_BASE_URL = "http://127.0.0.1:4317"; + +export class SimulationServiceClient { + constructor(baseUrl = DEFAULT_BASE_URL) { + this.baseUrl = baseUrl.replace(/\/$/, ""); + } + + getHealth() { + return this.request("/health"); + } + + getAircraft() { + return this.request("/aircraft"); + } + + simulateManeuver(payload) { + return this.request("/simulate", { + method: "POST", + body: JSON.stringify(payload), + }); + } + + async request(path, options = {}) { + const response = await fetch(`${this.baseUrl}${path}`, { + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + ...options, + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message ?? `${response.status} ${response.statusText}`); + } + + return data; + } +} + +export function createLevelTurnRequest(aircraftId) { + return createManeuverRequest({ + aircraftId, + maneuverType: "level-turn", + durationSec: 20, + speedMps: 150, + bankDeg: 45, + }); +} + +export function createCobraRequest(aircraftId) { + return createManeuverRequest({ + aircraftId, + maneuverType: "cobra", + durationSec: 5, + speedMps: 120, + bankDeg: 0, + }); +} + +export function createManeuverRequest({ + aircraftId, + maneuverType, + durationSec, + speedMps, + bankDeg, + cobraParameters, +}) { + const parameters = createManeuverParameters({ + maneuverType, + speedMps, + bankDeg, + cobraParameters, + }); + + const isF22Research = aircraftId === "f22-jsbsim" || aircraftId === "f22cobra-jsbsim"; + const entrySpeedMps = isF22Research ? Math.max(speedMps, 120) : speedMps; + + return { + aircraftId, + sampleRateHz: 30, + integrationRateHz: 120, + trim: !isF22Research, + durationSec, + initialState: { + position: [0, isF22Research ? 3000 : 1200, 0], + velocityMps: entrySpeedMps, + headingDeg: 0, + }, + maneuver: { + type: maneuverType, + parameters, + }, + }; +} + +function createManeuverParameters({ maneuverType, speedMps, bankDeg, cobraParameters = {} }) { + if (maneuverType === "cobra") { + return { + speedMps, + controlMode: "closed-loop", + flightPathAngleDeg: 0, + entryDelaySec: 0.15, + pullDurationSec: 0.7, + holdDurationSec: 0, + recoveryDurationSec: 2.0, + targetAlphaDeg: 80, + recoveryAlphaDeg: 8, + pitchRateLimitDegS: 80, + holdPitchRateDegS: 0, + maxElevatorCmd: -0.45, + recoveryElevatorCmd: 0.65, + throttleCmd: 1, + pullTvcCmd: -0.05, + recoveryTvcCmd: 0.85, + speedbrakeCmd: 0, + ...sanitizeCobraParameters(cobraParameters), + }; + } + + if (maneuverType === "control-script") { + return { + speedMps, + flightPathAngleDeg: 0, + timeline: [ + { t: 0, elevator: 0, aileron: 0, rudder: 0, throttle: 0.85, speedbrake: 0 }, + { t: 0.5, elevator: -0.8, aileron: 0, rudder: 0, throttle: 1, tvc: 0, speedbrake: 1 }, + { t: 1.6, elevator: 0, aileron: 0, rudder: 0, throttle: 1, tvc: 0, speedbrake: 1 }, + { t: 2.4, elevator: 0.25, aileron: 0, rudder: 0, throttle: 1, tvc: 1, speedbrake: 1 }, + { t: 4.0, elevator: 0, aileron: 0, rudder: 0, throttle: 0.85, tvc: 0, speedbrake: 0 }, + ], + }; + } + + return { + bankDeg, + speedMps, + flightPathAngleDeg: 8, + }; +} + +function sanitizeCobraParameters(parameters) { + return { + targetAlphaDeg: clampNumber(parameters.targetAlphaDeg, 45, 130, 80), + pitchRateLimitDegS: clampNumber(parameters.pitchRateLimitDegS, 20, 220, 80), + pullDurationSec: clampNumber(parameters.pullDurationSec, 0.1, 3, 0.7), + recoveryDurationSec: clampNumber(parameters.recoveryDurationSec, 0.1, 5, 2.0), + maxElevatorCmd: clampNumber(parameters.maxElevatorCmd, -1, 0, -0.45), + recoveryElevatorCmd: clampNumber(parameters.recoveryElevatorCmd, 0, 1, 0.65), + pullTvcCmd: clampNumber(parameters.pullTvcCmd, -1, 0, -0.05), + recoveryTvcCmd: clampNumber(parameters.recoveryTvcCmd, -1, 1, 0.85), + }; +} + +function clampNumber(value, minimum, maximum, fallback) { + if (!Number.isFinite(value)) { + return fallback; + } + + return Math.max(minimum, Math.min(maximum, value)); +} diff --git a/web-test/src/arcRotateCamera.js b/web-test/src/arcRotateCamera.js new file mode 100644 index 0000000..c02f97b --- /dev/null +++ b/web-test/src/arcRotateCamera.js @@ -0,0 +1,182 @@ +import * as Cesium from "cesium"; + +const MIN_PITCH = Cesium.Math.toRadians(-89); +const MAX_PITCH = Cesium.Math.toRadians(89); + +export class ArcRotateCamera { + constructor(viewer, target, options = {}) { + this.viewer = viewer; + this.target = Cesium.Cartesian3.clone(target); + this.heading = options.heading ?? Cesium.Math.toRadians(35); + this.pitch = options.pitch ?? Cesium.Math.toRadians(-28); + this.radius = options.radius ?? 2500; + this.minRadius = options.minRadius ?? 350; + this.maxRadius = options.maxRadius ?? 25000; + this.rotateSensitivity = options.rotateSensitivity ?? 0.004; + this.zoomSensitivity = options.zoomSensitivity ?? 0.0012; + this.inertiaFactor = options.inertiaFactor ?? 0.9; + + this.velocityHeading = 0; + this.velocityPitch = 0; + this.velocityZoom = 0; + + this.isLeftDragging = false; + this.isRightDragging = false; + this.lastPointerX = 0; + this.lastPointerY = 0; + + this.canvas = viewer.scene.canvas; + this.controller = viewer.scene.screenSpaceCameraController; + this.controller.enableInputs = false; + + this.onPointerDown = this.handlePointerDown.bind(this); + this.onPointerMove = this.handlePointerMove.bind(this); + this.onPointerUp = this.handlePointerUp.bind(this); + this.onWheel = this.handleWheel.bind(this); + this.onContextMenu = (event) => event.preventDefault(); + this.onPreRender = this.update.bind(this); + + this.canvas.addEventListener("pointerdown", this.onPointerDown); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.canvas.addEventListener("wheel", this.onWheel, { passive: false }); + this.canvas.addEventListener("contextmenu", this.onContextMenu); + viewer.scene.preRender.addEventListener(this.onPreRender); + + this.applyCamera(); + } + + setTarget(target) { + Cesium.Cartesian3.clone(target, this.target); + } + + destroy() { + this.canvas.removeEventListener("pointerdown", this.onPointerDown); + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + this.canvas.removeEventListener("wheel", this.onWheel); + this.canvas.removeEventListener("contextmenu", this.onContextMenu); + this.viewer.scene.preRender.removeEventListener(this.onPreRender); + this.controller.enableInputs = true; + } + + handlePointerDown(event) { + if (event.button !== 0 && event.button !== 2) { + return; + } + + this.canvas.setPointerCapture?.(event.pointerId); + this.lastPointerX = event.clientX; + this.lastPointerY = event.clientY; + this.isLeftDragging = event.button === 0; + this.isRightDragging = event.button === 2; + + if (this.isLeftDragging) { + this.velocityHeading = 0; + this.velocityPitch = 0; + } + + if (this.isRightDragging) { + this.velocityZoom = 0; + } + + event.preventDefault(); + } + + handlePointerMove(event) { + if (!this.isLeftDragging && !this.isRightDragging) { + return; + } + + const deltaX = event.clientX - this.lastPointerX; + const deltaY = event.clientY - this.lastPointerY; + this.lastPointerX = event.clientX; + this.lastPointerY = event.clientY; + + if (this.isLeftDragging) { + const headingDelta = deltaX * this.rotateSensitivity; + const pitchDelta = -deltaY * this.rotateSensitivity; + this.heading += headingDelta; + this.pitch = clamp(this.pitch + pitchDelta, MIN_PITCH, MAX_PITCH); + this.velocityHeading = headingDelta; + this.velocityPitch = pitchDelta; + } + + if (this.isRightDragging) { + this.zoomBy(deltaY); + } + + event.preventDefault(); + } + + handlePointerUp(event) { + if (event.button === 0) { + this.isLeftDragging = false; + } + + if (event.button === 2) { + this.isRightDragging = false; + } + } + + handleWheel(event) { + this.zoomBy(event.deltaY); + event.preventDefault(); + } + + zoomBy(deltaY) { + const zoomFactor = 1 + deltaY * this.zoomSensitivity; + this.radius = clamp(this.radius * zoomFactor, this.minRadius, this.maxRadius); + this.velocityZoom = zoomFactor; + } + + update() { + if (!this.isLeftDragging) { + if (Math.abs(this.velocityHeading) > 0.00001) { + this.heading += this.velocityHeading; + this.velocityHeading *= this.inertiaFactor; + } else { + this.velocityHeading = 0; + } + + if (Math.abs(this.velocityPitch) > 0.00001) { + this.pitch = clamp( + this.pitch + this.velocityPitch, + MIN_PITCH, + MAX_PITCH + ); + this.velocityPitch *= this.inertiaFactor; + } else { + this.velocityPitch = 0; + } + } + + if (!this.isRightDragging && this.velocityZoom !== 0) { + if (Math.abs(this.velocityZoom - 1) > 0.001) { + this.radius = clamp( + this.radius * this.velocityZoom, + this.minRadius, + this.maxRadius + ); + this.velocityZoom = + 1 + (this.velocityZoom - 1) * this.inertiaFactor; + } else { + this.velocityZoom = 0; + } + } + + this.applyCamera(); + } + + applyCamera() { + this.viewer.camera.lookAt( + this.target, + new Cesium.HeadingPitchRange(this.heading, this.pitch, this.radius) + ); + this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); + } +} + +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} diff --git a/web-test/src/main.js b/web-test/src/main.js new file mode 100644 index 0000000..31d0c29 --- /dev/null +++ b/web-test/src/main.js @@ -0,0 +1,122 @@ +window.CESIUM_BASE_URL = "/cesium"; + +import "cesium/Build/Cesium/Widgets/widgets.css"; +import { + SimulationServiceClient, + createManeuverRequest, +} from "./api/simulationService.js"; +import { createTrajectoryPlayer } from "./playback/trajectoryPlayer.js"; +import { createCesiumScene } from "./renderer/cesiumScene.js"; +import { createControls } from "./ui/controls.js"; +import { createHud } from "./ui/hud.js"; +import { createResearchPanel } from "./ui/researchPanel.js"; +import "./styles.css"; + +const ORIGIN = { + longitudeDeg: 116.3913, + latitudeDeg: 39.9075, + heightM: 3000, +}; + +const PREFERRED_AIRCRAFT_ID = "f22cobra-jsbsim"; + +const hud = createHud(); +const researchPanel = createResearchPanel(); +const scene = createCesiumScene({ + containerId: "cesiumContainer", + origin: ORIGIN, +}); +const simulationService = new SimulationServiceClient(); +let activeAircraftId = null; + +const trajectoryPlayer = createTrajectoryPlayer({ + onPlay: () => hud.setPlayback("Playing"), + onPause: () => hud.setPlayback("Paused"), + onReset: () => hud.setPlayback("0.0s"), + onEmpty: () => hud.setPlayback("No samples"), + onFrame: ({ sample, nextSample, timeSec }) => { + scene.setAircraftSample(sample, nextSample); + hud.setPlayback(`${timeSec.toFixed(1)}s`); + }, +}); +const controls = createControls({ + onSimulate: (values) => runSimulation(values), + onRetry: () => connectSimulationService(), + onPlay: () => trajectoryPlayer.play(), + onPause: () => trajectoryPlayer.pause(), + onReset: () => trajectoryPlayer.reset(), + onSpeedChange: (speed) => trajectoryPlayer.setSpeed(speed), +}); + +connectSimulationService(); + +window.__viewer = scene.viewer; +window.__arcRotateCamera = scene.arcRotateCamera; +window.__simulationService = simulationService; +window.__trajectoryPlayer = trajectoryPlayer; + +async function connectSimulationService() { + hud.reset(); + trajectoryPlayer.reset(); + activeAircraftId = null; + + try { + const health = await simulationService.getHealth(); + console.log("[service] health", health); + hud.setService(health.ok ? `Online ${health.version}` : "Unhealthy"); + + const aircraftResponse = await simulationService.getAircraft(); + console.log("[service] aircraft", aircraftResponse); + const aircraft = aircraftResponse.aircraft ?? []; + hud.setAircraft(`${aircraft.length}`); + + const aircraftId = + aircraft.find((item) => item.id === PREFERRED_AIRCRAFT_ID)?.id ?? aircraft[0]?.id; + if (!aircraftId) { + hud.setSimulation("No aircraft"); + return; + } + activeAircraftId = aircraftId; + hud.setAircraft(aircraftId); + + await runSimulation(controls.getValues()); + } catch (error) { + console.error("[service] unavailable", error); + hud.setService("Offline"); + hud.setSimulation("Service unavailable"); + hud.setPlayback("Idle"); + } +} + +async function runSimulation(values) { + if (!activeAircraftId) { + hud.setSimulation("No aircraft"); + return; + } + + controls.setBusy(true); + hud.setSimulation(`Running ${values.maneuverType}`); + + try { + const request = createManeuverRequest({ + aircraftId: activeAircraftId, + ...values, + }); + const simulation = await simulationService.simulateManeuver(request); + console.log("[service] simulation request", request); + console.log("[service] simulation result", simulation); + + window.__lastSimulation = simulation; + scene.renderSimulation(simulation); + researchPanel.render(simulation); + hud.setSimulation(`${simulation.samples?.length ?? 0} samples`); + trajectoryPlayer.load(simulation); + trajectoryPlayer.play(); + } catch (error) { + console.error("[service] simulation failed", error); + hud.setSimulation("Simulation failed"); + hud.setPlayback("Idle"); + } finally { + controls.setBusy(false); + } +} diff --git a/web-test/src/playback/trajectoryPlayer.js b/web-test/src/playback/trajectoryPlayer.js new file mode 100644 index 0000000..7b8e1e8 --- /dev/null +++ b/web-test/src/playback/trajectoryPlayer.js @@ -0,0 +1,204 @@ +export function createTrajectoryPlayer({ onFrame, onPlay, onPause, onReset, onEmpty }) { + let animationFrame = 0; + let lastFrameTime = 0; + let timeSec = 0; + let speed = 1; + let simulation = null; + let isPlaying = false; + + function load(nextSimulation) { + stopLoop(); + simulation = nextSimulation; + timeSec = 0; + emitFrame(); + } + + function play(nextSimulation = null) { + if (nextSimulation) { + load(nextSimulation); + } + + if (!hasSamples()) { + onEmpty?.(); + return; + } + + if (isPlaying) { + return; + } + + isPlaying = true; + lastFrameTime = performance.now(); + onPlay?.(); + animationFrame = requestAnimationFrame(tick); + } + + function pause() { + if (!isPlaying) { + return; + } + + stopLoop(); + onPause?.(); + } + + function reset() { + stopLoop(); + timeSec = 0; + emitFrame(); + onReset?.(); + } + + function setSpeed(nextSpeed) { + speed = Number.isFinite(nextSpeed) ? nextSpeed : 1; + } + + function tick(now) { + if (!simulation) { + stopLoop(); + return; + } + + const deltaSec = (now - lastFrameTime) / 1000; + lastFrameTime = now; + timeSec = (timeSec + deltaSec * speed) % simulation.durationSec; + emitFrame(); + animationFrame = requestAnimationFrame(tick); + } + + function emitFrame() { + const samples = simulation?.samples ?? []; + if (samples.length < 2) { + return; + } + + const samplePair = getSamplePair(samples, timeSec); + const sample = interpolateSamples( + samplePair.current, + samplePair.next, + samplePair.amount + ); + + onFrame?.({ + sample, + nextSample: samplePair.next, + timeSec, + }); + } + + function stopLoop() { + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = 0; + } + isPlaying = false; + } + + function hasSamples() { + return (simulation?.samples?.length ?? 0) >= 2; + } + + return { + load, + play, + pause, + reset, + setSpeed, + }; +} + +function getSamplePair(samples, timeSec) { + let nextIndex = samples.findIndex((sample) => sample.t >= timeSec); + if (nextIndex <= 0) { + nextIndex = 1; + } + + const current = samples[nextIndex - 1]; + const next = samples[nextIndex] ?? samples[0]; + const span = next.t - current.t || 1; + const amount = clamp((timeSec - current.t) / span, 0, 1); + + return { current, next, amount }; +} + +function interpolateSamples(current, next, amount) { + return { + ...current, + t: lerp(current.t, next.t, amount), + position: [ + lerp(current.position[0], next.position[0], amount), + lerp(current.position[1], next.position[1], amount), + lerp(current.position[2], next.position[2], amount), + ], + rotation: interpolateQuat(current.rotation, next.rotation, amount), + headingDeg: interpolateAngleDeg(current.headingDeg, next.headingDeg, amount), + }; +} + +function interpolateQuat(start, end, amount) { + if (!isQuat(start) || !isQuat(end)) { + return isQuat(start) ? start : end; + } + + let [ex, ey, ez, ew] = end; + let dot = start[0] * ex + start[1] * ey + start[2] * ez + start[3] * ew; + if (dot < 0) { + dot = -dot; + ex = -ex; + ey = -ey; + ez = -ez; + ew = -ew; + } + + if (dot > 0.9995) { + return normalizeQuat([ + lerp(start[0], ex, amount), + lerp(start[1], ey, amount), + lerp(start[2], ez, amount), + lerp(start[3], ew, amount), + ]); + } + + const theta0 = Math.acos(clamp(dot, -1, 1)); + const theta = theta0 * amount; + const sinTheta = Math.sin(theta); + const sinTheta0 = Math.sin(theta0); + const s0 = Math.cos(theta) - (dot * sinTheta) / sinTheta0; + const s1 = sinTheta / sinTheta0; + + return [ + start[0] * s0 + ex * s1, + start[1] * s0 + ey * s1, + start[2] * s0 + ez * s1, + start[3] * s0 + ew * s1, + ]; +} + +function isQuat(value) { + return Array.isArray(value) && value.length === 4 && value.every(Number.isFinite); +} + +function normalizeQuat(value) { + const length = Math.hypot(value[0], value[1], value[2], value[3]); + if (length <= 0) { + return [0, 0, 0, 1]; + } + return value.map((item) => item / length); +} + +function interpolateAngleDeg(start, end, amount) { + if (!Number.isFinite(start) || !Number.isFinite(end)) { + return Number.isFinite(start) ? start : 0; + } + + const delta = ((end - start + 540) % 360) - 180; + return start + delta * amount; +} + +function lerp(start, end, amount) { + return start + (end - start) * amount; +} + +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} diff --git a/web-test/src/renderer/cesiumScene.js b/web-test/src/renderer/cesiumScene.js new file mode 100644 index 0000000..7ed0d82 --- /dev/null +++ b/web-test/src/renderer/cesiumScene.js @@ -0,0 +1,231 @@ +import * as Cesium from "cesium"; +import { ArcRotateCamera } from "../arcRotateCamera.js"; + +export function createCesiumScene({ containerId, origin }) { + const viewer = new Cesium.Viewer(containerId, { + animation: false, + timeline: false, + baseLayerPicker: false, + geocoder: false, + homeButton: false, + sceneModePicker: false, + navigationHelpButton: false, + fullscreenButton: false, + infoBox: false, + selectionIndicator: false, + terrainProvider: new Cesium.EllipsoidTerrainProvider(), + imageryProvider: new Cesium.UrlTemplateImageryProvider({ + url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + credit: "Esri World Imagery", + maximumLevel: 19, + }), + }); + + configureScene(viewer); + + const originPosition = Cesium.Cartesian3.fromDegrees( + origin.longitudeDeg, + origin.latitudeDeg, + origin.heightM + ); + const originTransform = Cesium.Transforms.eastNorthUpToFixedFrame( + originPosition + ); + const aircraftPrimitive = createAircraftPrimitive(viewer, originPosition); + const targetMarkerEntity = createTargetMarker(viewer, originPosition); + const arcRotateCamera = new ArcRotateCamera(viewer, originPosition, { + heading: Cesium.Math.toRadians(35), + pitch: Cesium.Math.toRadians(-28), + radius: 2500, + }); + + let trajectoryEntity = null; + + function renderSimulation(simulation) { + const samples = simulation.samples ?? []; + if (samples.length === 0) { + return; + } + + const positions = samples.map((sample) => + localPositionToWorld(sample.position) + ); + + if (trajectoryEntity) { + viewer.entities.remove(trajectoryEntity); + } + + trajectoryEntity = viewer.entities.add({ + polyline: { + positions, + width: 3, + material: Cesium.Color.CYAN.withAlpha(0.92), + clampToGround: false, + }, + }); + + setAircraftSample(samples[0], samples[1]); + viewer.scene.requestRender(); + } + + function setAircraftSample(sample, nextSample = null) { + const position = localPositionToWorld(sample.position); + + aircraftPrimitive.modelMatrix = getAircraftModelMatrix(position, sample, nextSample); + targetMarkerEntity.position = new Cesium.ConstantPositionProperty(position); + arcRotateCamera.setTarget(position); + } + + function localPositionToWorld(position) { + const [east, up, north] = position; + return Cesium.Matrix4.multiplyByPoint( + originTransform, + new Cesium.Cartesian3(east, north, up - origin.heightM), + new Cesium.Cartesian3() + ); + } + + return { + viewer, + arcRotateCamera, + renderSimulation, + setAircraftSample, + localPositionToWorld, + }; +} + +function configureScene(viewer) { + viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString("#24323f"); + viewer.scene.skyAtmosphere.show = false; + viewer.scene.fog.enabled = false; + viewer.scene.moon.show = false; + viewer.scene.sun.show = false; + viewer.scene.screenSpaceCameraController.inertiaSpin = 0; + viewer.scene.screenSpaceCameraController.inertiaTranslate = 0; + viewer.scene.screenSpaceCameraController.inertiaZoom = 0; + viewer.scene.screenSpaceCameraController.enableCollisionDetection = false; +} + +function createAircraftPrimitive(viewer, position) { + const primitive = viewer.scene.primitives.add( + new Cesium.Primitive({ + geometryInstances: new Cesium.GeometryInstance({ + geometry: new Cesium.CylinderGeometry({ + length: 320, + topRadius: 0, + bottomRadius: 78, + slices: 32, + vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT, + }), + attributes: { + color: Cesium.ColorGeometryInstanceAttribute.fromColor( + Cesium.Color.ORANGE.withAlpha(0.96) + ), + }, + }), + appearance: new Cesium.PerInstanceColorAppearance({ + closed: true, + translucent: true, + }), + asynchronous: false, + }) + ); + + primitive.modelMatrix = getAircraftModelMatrix(position, 0); + return primitive; +} + +function createTargetMarker(viewer, position) { + return viewer.entities.add({ + position, + point: { + pixelSize: 8, + color: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + }, + }); +} + +function getAircraftModelMatrix(position, sampleOrHeadingDeg, nextSample = null) { + const orientation = + typeof sampleOrHeadingDeg === "number" + ? fallbackOrientation(position, sampleOrHeadingDeg, 0, 0) + : sampleOrientation(position, sampleOrHeadingDeg, nextSample); + + return Cesium.Matrix4.fromTranslationQuaternionRotationScale( + position, + orientation, + new Cesium.Cartesian3(1, 1, 1), + new Cesium.Matrix4() + ); +} + +function sampleOrientation(position, sample, nextSample) { + if (isQuat(sample?.rotation)) { + return localEunQuaternionToWorld(position, sample.rotation); + } + + const estimatedHeadingDeg = estimateHeadingDeg(sample, nextSample); + const headingDeg = Number.isFinite(estimatedHeadingDeg) + ? estimatedHeadingDeg + : numberOrDefault(sample?.headingDeg, 0); + + return fallbackOrientation( + position, + headingDeg, + numberOrDefault(sample?.pitchDeg, 0), + numberOrDefault(sample?.rollDeg, 0) + ); +} + +function localEunQuaternionToWorld(position, rotation) { + const localToWorld = Cesium.Transforms.eastNorthUpToFixedFrame(position); + const localRotation = Cesium.Matrix3.fromQuaternion( + new Cesium.Quaternion(rotation[0], rotation[1], rotation[2], rotation[3]), + new Cesium.Matrix3() + ); + const geometryRotation = Cesium.Matrix3.fromRotationY( + Cesium.Math.PI_OVER_TWO, + new Cesium.Matrix3() + ); + const worldRotation = Cesium.Matrix3.multiply( + Cesium.Matrix4.getMatrix3(localToWorld, new Cesium.Matrix3()), + Cesium.Matrix3.multiply(localRotation, geometryRotation, new Cesium.Matrix3()), + new Cesium.Matrix3() + ); + return Cesium.Quaternion.fromRotationMatrix(worldRotation, new Cesium.Quaternion()); +} + +function fallbackOrientation(position, headingDeg, pitchDeg = 0, rollDeg = 0) { + return Cesium.Transforms.headingPitchRollQuaternion( + position, + new Cesium.HeadingPitchRoll( + Cesium.Math.toRadians(headingDeg + 90), + Cesium.Math.PI_OVER_TWO - Cesium.Math.toRadians(pitchDeg), + Cesium.Math.toRadians(rollDeg) + ) + ); +} + +function estimateHeadingDeg(sample, nextSample) { + if (!nextSample) { + return Number.NaN; + } + + const eastDelta = nextSample.position[0] - sample.position[0]; + const northDelta = nextSample.position[2] - sample.position[2]; + if (Math.hypot(eastDelta, northDelta) < 0.01) { + return Number.NaN; + } + + return Cesium.Math.toDegrees(Math.atan2(eastDelta, northDelta)); +} + +function numberOrDefault(value, fallback) { + return Number.isFinite(value) ? value : fallback; +} + +function isQuat(value) { + return Array.isArray(value) && value.length === 4 && value.every(Number.isFinite); +} diff --git a/web-test/src/styles.css b/web-test/src/styles.css new file mode 100644 index 0000000..bfafd59 --- /dev/null +++ b/web-test/src/styles.css @@ -0,0 +1,175 @@ +html, +body, +#app, +#cesiumContainer { + margin: 0; + width: 100%; + height: 100%; + overflow: hidden; + background: #0f1720; +} + +body { + font-family: Arial, Helvetica, sans-serif; + color: #e6eef5; +} + +#app { + position: relative; +} + +#hud { + position: absolute; + top: 16px; + left: 16px; + z-index: 10; + width: 320px; + padding: 12px 14px; + border: 1px solid rgba(170, 200, 220, 0.25); + background: rgba(12, 18, 24, 0.82); + backdrop-filter: blur(8px); +} + +#researchPanel { + position: absolute; + right: 16px; + bottom: 16px; + z-index: 10; + width: min(440px, calc(100vw - 32px)); + padding: 12px 14px; + border: 1px solid rgba(170, 200, 220, 0.25); + background: rgba(12, 18, 24, 0.82); + backdrop-filter: blur(8px); +} + +.hud-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 6px; +} + +.panel-title { + font-size: 13px; + font-weight: 600; + margin-bottom: 8px; +} + +#telemetryCanvas { + display: block; + width: 100%; + height: 300px; +} + +.telemetry-summary { + margin-top: 8px; + color: #c8d6df; + font-size: 11px; + line-height: 1.45; +} + +.hud-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + font-size: 12px; + line-height: 1.8; + color: #b8c7d3; +} + +.hud-row strong { + color: #f4f8fb; + font-size: 12px; + font-weight: 600; + text-align: right; +} + +.control-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 10px; +} + +.cobra-controls { + margin-top: 12px; + padding-top: 10px; + border-top: 1px solid rgba(170, 200, 220, 0.18); +} + +.cobra-controls[hidden] { + display: none; +} + +.control-grid label { + display: grid; + gap: 4px; + font-size: 11px; + color: #b8c7d3; +} + +.control-grid input, +.control-grid select, +.button-row button, +.button-row select { + min-width: 0; + height: 28px; + border: 1px solid rgba(170, 200, 220, 0.26); + background: rgba(20, 30, 40, 0.92); + color: #f4f8fb; + font: inherit; +} + +.control-grid input, +.control-grid select { + padding: 0 8px; +} + +.button-row { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.button-row button { + flex: 1; + cursor: pointer; +} + +.button-row select { + width: 72px; + padding: 0 6px; +} + +.button-row button:hover, +.control-grid input:focus, +.control-grid select:focus, +.button-row select:focus { + border-color: rgba(116, 213, 255, 0.75); + outline: none; +} + +.button-row button:disabled { + cursor: default; + opacity: 0.55; +} + +.cesium-viewer-toolbar, +.cesium-viewer-animationContainer, +.cesium-viewer-timelineContainer, +.cesium-viewer-bottom { + display: none !important; +} + +@media (max-width: 780px) { + #hud, + #researchPanel { + width: calc(100vw - 32px); + } + + #researchPanel { + left: 16px; + right: auto; + bottom: 16px; + } +} diff --git a/web-test/src/ui/controls.js b/web-test/src/ui/controls.js new file mode 100644 index 0000000..0c42ac4 --- /dev/null +++ b/web-test/src/ui/controls.js @@ -0,0 +1,93 @@ +export function createControls(handlers = {}) { + const elements = { + maneuverType: document.querySelector("#maneuverType"), + durationSec: document.querySelector("#durationSec"), + speedMps: document.querySelector("#speedMps"), + bankDeg: document.querySelector("#bankDeg"), + cobraControls: document.querySelector("#cobraControls"), + targetAlphaDeg: document.querySelector("#targetAlphaDeg"), + pitchRateLimitDegS: document.querySelector("#pitchRateLimitDegS"), + pullDurationSec: document.querySelector("#pullDurationSec"), + recoveryDurationSec: document.querySelector("#recoveryDurationSec"), + maxElevatorCmd: document.querySelector("#maxElevatorCmd"), + recoveryElevatorCmd: document.querySelector("#recoveryElevatorCmd"), + pullTvcCmd: document.querySelector("#pullTvcCmd"), + recoveryTvcCmd: document.querySelector("#recoveryTvcCmd"), + simulateButton: document.querySelector("#simulateButton"), + retryButton: document.querySelector("#retryButton"), + playButton: document.querySelector("#playButton"), + pauseButton: document.querySelector("#pauseButton"), + resetButton: document.querySelector("#resetButton"), + playbackSpeed: document.querySelector("#playbackSpeed"), + }; + + elements.simulateButton?.addEventListener("click", () => { + handlers.onSimulate?.(getValues()); + }); + elements.retryButton?.addEventListener("click", () => { + handlers.onRetry?.(); + }); + elements.playButton?.addEventListener("click", () => { + handlers.onPlay?.(); + }); + elements.pauseButton?.addEventListener("click", () => { + handlers.onPause?.(); + }); + elements.resetButton?.addEventListener("click", () => { + handlers.onReset?.(); + }); + elements.playbackSpeed?.addEventListener("change", () => { + handlers.onSpeedChange?.(readNumber(elements.playbackSpeed, 1)); + }); + elements.maneuverType?.addEventListener("change", updateMode); + updateMode(); + + function getValues() { + const maneuverType = elements.maneuverType?.value ?? "level-turn"; + return { + maneuverType, + durationSec: readNumber(elements.durationSec, 20), + speedMps: readNumber(elements.speedMps, 150), + bankDeg: readNumber(elements.bankDeg, 45), + cobraParameters: readCobraParameters(), + }; + } + + function setBusy(isBusy) { + if (elements.simulateButton) { + elements.simulateButton.disabled = isBusy; + elements.simulateButton.textContent = isBusy ? "Running" : "Simulate"; + } + } + + return { + getValues, + setBusy, + }; + + function updateMode() { + if (!elements.cobraControls) { + return; + } + + elements.cobraControls.hidden = elements.maneuverType?.value !== "cobra"; + } + + function readCobraParameters() { + return { + targetAlphaDeg: readNumber(elements.targetAlphaDeg, 80), + pitchRateLimitDegS: readNumber(elements.pitchRateLimitDegS, 80), + pullDurationSec: readNumber(elements.pullDurationSec, 0.7), + recoveryDurationSec: readNumber(elements.recoveryDurationSec, 2.0), + maxElevatorCmd: readNumber(elements.maxElevatorCmd, -0.45), + recoveryElevatorCmd: readNumber(elements.recoveryElevatorCmd, 0.65), + pullTvcCmd: readNumber(elements.pullTvcCmd, -0.05), + recoveryTvcCmd: readNumber(elements.recoveryTvcCmd, 0.85), + }; + } +} + +function readNumber(element, fallback) { + const value = Number.parseFloat(element?.value ?? ""); + return Number.isFinite(value) ? value : fallback; +} diff --git a/web-test/src/ui/hud.js b/web-test/src/ui/hud.js new file mode 100644 index 0000000..a0c4db1 --- /dev/null +++ b/web-test/src/ui/hud.js @@ -0,0 +1,39 @@ +export function createHud() { + const elements = { + service: document.querySelector("#serviceStatus"), + aircraft: document.querySelector("#aircraftStatus"), + simulation: document.querySelector("#simulationStatus"), + playback: document.querySelector("#playbackStatus"), + }; + + return { + reset() { + setText(elements.service, "Checking"); + setText(elements.aircraft, "-"); + setText(elements.simulation, "Waiting"); + setText(elements.playback, "Idle"); + }, + + setService(value) { + setText(elements.service, value); + }, + + setAircraft(value) { + setText(elements.aircraft, value); + }, + + setSimulation(value) { + setText(elements.simulation, value); + }, + + setPlayback(value) { + setText(elements.playback, value); + }, + }; +} + +function setText(element, value) { + if (element) { + element.textContent = value; + } +} diff --git a/web-test/src/ui/researchPanel.js b/web-test/src/ui/researchPanel.js new file mode 100644 index 0000000..ce35dba --- /dev/null +++ b/web-test/src/ui/researchPanel.js @@ -0,0 +1,169 @@ +const SERIES = [ + { key: "alphaDeg", label: "alpha", color: "#5eead4", min: 0, max: 120 }, + { key: "pitchDeg", label: "pitch", color: "#fbbf24", min: -20, max: 120 }, + { key: "pitchRateDegS", label: "q", color: "#fb7185", min: -260, max: 260 }, + { key: "velocityMps", label: "speed", color: "#93c5fd", min: 0, max: 260 }, + { key: "altitudeDeltaM", label: "dAlt", color: "#c4b5fd", min: -200, max: 600 }, + { key: "elevatorCmd", label: "elev", color: "#f97316", min: -1, max: 1 }, + { key: "tvcCmd", label: "tvc", color: "#a3e635", min: -1, max: 1 }, + { key: "speedbrakeCmd", label: "brake", color: "#e5e7eb", min: 0, max: 1 }, +]; + +export function createResearchPanel() { + const canvas = document.querySelector("#telemetryCanvas"); + const summary = document.querySelector("#telemetrySummary"); + const context = canvas?.getContext("2d"); + + function render(simulation) { + if (!canvas || !context) { + return; + } + + const samples = normalizeSamples(simulation?.samples ?? []); + drawBackground(context, canvas); + + if (samples.length < 2) { + setText(summary, "No simulation"); + return; + } + + drawGrid(context, canvas); + for (const series of SERIES) { + drawSeries(context, canvas, samples, simulation.durationSec, series); + } + drawLegend(context, canvas); + setText(summary, createSummary(samples)); + } + + render(null); + + return { render }; +} + +function normalizeSamples(samples) { + const startAltitude = samples[0]?.position?.[1] ?? 0; + return samples.map((sample) => ({ + ...sample, + altitudeDeltaM: (sample.position?.[1] ?? startAltitude) - startAltitude, + elevatorCmd: sample.controlInputs?.elevator ?? 0, + tvcCmd: sample.controlInputs?.tvc ?? 0, + speedbrakeCmd: sample.controlInputs?.speedbrake ?? 0, + })); +} + +function drawBackground(context, canvas) { + context.clearRect(0, 0, canvas.width, canvas.height); + context.fillStyle = "rgba(8, 13, 18, 0.82)"; + context.fillRect(0, 0, canvas.width, canvas.height); +} + +function drawGrid(context, canvas) { + context.save(); + context.strokeStyle = "rgba(180, 205, 220, 0.16)"; + context.lineWidth = 1; + for (let x = 40; x < canvas.width; x += 76) { + line(context, x, 18, x, canvas.height - 34); + } + for (let y = 28; y < canvas.height - 24; y += 48) { + line(context, 34, y, canvas.width - 12, y); + } + context.restore(); +} + +function drawSeries(context, canvas, samples, durationSec, series) { + const left = 34; + const top = 18; + const right = canvas.width - 12; + const bottom = canvas.height - 34; + const width = right - left; + const height = bottom - top; + + context.save(); + context.strokeStyle = series.color; + context.lineWidth = series.key.includes("Cmd") ? 1.5 : 2; + context.globalAlpha = series.key.includes("Cmd") ? 0.85 : 0.95; + context.beginPath(); + + samples.forEach((sample, index) => { + const x = left + (sample.t / durationSec) * width; + const value = clamp(numberOrDefault(sample[series.key], 0), series.min, series.max); + const y = bottom - ((value - series.min) / (series.max - series.min)) * height; + if (index === 0) { + context.moveTo(x, y); + } else { + context.lineTo(x, y); + } + }); + + context.stroke(); + context.restore(); +} + +function drawLegend(context, canvas) { + context.save(); + context.font = "11px Arial, Helvetica, sans-serif"; + let x = 34; + const y = canvas.height - 12; + for (const series of SERIES) { + context.fillStyle = series.color; + context.fillRect(x, y - 7, 8, 2); + context.fillText(series.label, x + 12, y - 3); + x += context.measureText(series.label).width + 28; + } + context.restore(); +} + +function createSummary(samples) { + const maxAlpha = maxOf(samples, "alphaDeg"); + const maxPitch = maxOf(samples, "pitchDeg"); + const maxQ = maxAbsOf(samples, "pitchRateDegS"); + const minSpeed = minOf(samples, "velocityMps"); + const end = samples.at(-1); + const maxBrake = maxOf(samples, "speedbrakeCmd"); + + return [ + `max alpha ${format(maxAlpha)} deg`, + `max pitch ${format(maxPitch)} deg`, + `max q ${format(maxQ)} deg/s`, + `min speed ${format(minSpeed)} m/s`, + `end alpha ${format(end?.alphaDeg)} deg`, + `brake ${format(maxBrake, 1)}`, + ].join(" | "); +} + +function maxOf(samples, key) { + return Math.max(...samples.map((sample) => numberOrDefault(sample[key], -Infinity))); +} + +function minOf(samples, key) { + return Math.min(...samples.map((sample) => numberOrDefault(sample[key], Infinity))); +} + +function maxAbsOf(samples, key) { + return Math.max(...samples.map((sample) => Math.abs(numberOrDefault(sample[key], 0)))); +} + +function format(value, digits = 1) { + return Number.isFinite(value) ? value.toFixed(digits) : "-"; +} + +function line(context, x1, y1, x2, y2) { + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.stroke(); +} + +function numberOrDefault(value, fallback) { + return Number.isFinite(value) ? value : fallback; +} + +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function setText(element, value) { + if (element) { + element.textContent = value; + } +} diff --git a/web-test/vite.config.js b/web-test/vite.config.js new file mode 100644 index 0000000..e9a96ec --- /dev/null +++ b/web-test/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + optimizeDeps: { + include: ["cesium"], + }, + server: { + host: "127.0.0.1", + port: 5173, + }, + preview: { + host: "127.0.0.1", + port: 4173, + }, +});