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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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,
+ },
+});