From b836d9c01b307928392415df2cde53fb5503d1c6 Mon Sep 17 00:00:00 2001 From: munsel Date: Sat, 25 Apr 2026 23:15:46 +0200 Subject: [PATCH] now with mapLibre instead of cesium --- web/package-lock.json | 1039 ++++++------------ web/package.json | 6 +- web/src/App.tsx | 12 +- web/src/cesium/CesiumViewer.tsx | 71 -- web/src/cesium/cesiumContext.ts | 14 - web/src/cesium/geoUtils.ts | 63 -- web/src/cesium/useCesiumCamera.ts | 37 - web/src/challenges/ChallengeLayer.tsx | 135 ++- web/src/challenges/ChallengePanel.tsx | 10 +- web/src/challenges/usePolygonDraw.ts | 373 +++---- web/src/coaster/CoasterEditorPage.module.css | 24 +- web/src/coaster/CoasterEditorPage.tsx | 311 +++--- web/src/coaster/CoasterListPanel.module.css | 42 + web/src/coaster/CoasterListPanel.tsx | 44 +- web/src/coaster/RideRenderer.tsx | 78 +- web/src/coaster/SimulationPlots.module.css | 2 +- web/src/coaster/SimulationPlots.tsx | 6 +- web/src/coaster/bezierUtils.ts | 109 +- web/src/coaster/useAccelerationStrips.ts | 301 ++--- web/src/coaster/useCoasterPath.ts | 486 ++++---- web/src/coaster/useTerrainCapture.ts | 234 ++-- web/src/maplibre/MapLibreViewer.tsx | 116 ++ web/src/maplibre/custom3DLayer.ts | 271 +++++ web/src/maplibre/geoUtils.ts | 162 +++ web/src/maplibre/maplibreContext.ts | 12 + web/src/maplibre/useMapLibreCamera.ts | 36 + web/src/splat/SplatLayer.tsx | 98 +- web/src/splat/SplatRenderer.tsx | 64 +- web/src/splat/useSplatCamera.ts | 85 +- web/src/ui/OverlayControls.tsx | 75 +- web/src/ui/SearchBar.tsx | 11 +- web/vite.config.ts | 18 +- 32 files changed, 2232 insertions(+), 2113 deletions(-) delete mode 100644 web/src/cesium/CesiumViewer.tsx delete mode 100644 web/src/cesium/cesiumContext.ts delete mode 100644 web/src/cesium/geoUtils.ts delete mode 100644 web/src/cesium/useCesiumCamera.ts create mode 100644 web/src/maplibre/MapLibreViewer.tsx create mode 100644 web/src/maplibre/custom3DLayer.ts create mode 100644 web/src/maplibre/geoUtils.ts create mode 100644 web/src/maplibre/maplibreContext.ts create mode 100644 web/src/maplibre/useMapLibreCamera.ts diff --git a/web/package-lock.json b/web/package-lock.json index dc8c0e1..83d8cbb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@mkkellogg/gaussian-splats-3d": "^0.4.6", "axios": "^1.7.9", - "cesium": "^1.124.0", + "maplibre-gl": "^4.7.1", "oidc-client-ts": "^3.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -21,13 +21,13 @@ }, "devDependencies": { "@types/geojson": "^7946.0.16", + "@types/maplibre-gl": "^1.0.0", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/three": "^0.171.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "vite": "^5.4.11", - "vite-plugin-cesium": "^1.2.22" + "vite": "^5.4.11" } }, "node_modules/@babel/code-frame": { @@ -312,69 +312,6 @@ "node": ">=6.9.0" } }, - "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/engine/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/@cesium/engine/node_modules/meshoptimizer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.0.tgz", - "integrity": "sha512-KYYsWvWduDIm86HVtTKW1luZSusEv+tgSqYvgSJcKzaHIUNKL9/qQ+48YKcLxknAE8GQAeVi68mgvmEGrjwqjA==", - "license": "MIT" - }, - "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.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -816,6 +753,89 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", + "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, "node_modules/@mkkellogg/gaussian-splats-3d": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/@mkkellogg/gaussian-splats-3d/-/gaussian-splats-3d-0.4.7.tgz", @@ -825,70 +845,6 @@ "three": ">=0.160.0" } }, - "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/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -932,20 +888,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -1296,16 +1238,6 @@ "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/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1444,18 +1376,50 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "@types/geojson": "*" } }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "node_modules/@types/maplibre-gl": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/maplibre-gl/-/maplibre-gl-1.13.2.tgz", + "integrity": "sha512-IC1RBMhKXpGDpiFsEwt17c/hbff0GCS/VmzqmrY6G+kyy2wfv2e7BoSQRAfqrvhBQPCoO8yc0SNCi5HkmCcVqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -1483,6 +1447,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/three": { "version": "0.171.0", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.171.0.tgz", @@ -1498,13 +1471,6 @@ "meshoptimizer": "~0.18.1" } }, - "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/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -1546,45 +1512,12 @@ "dev": true, "license": "BSD-3-Clause" }, - "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/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.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/axios": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", @@ -1609,12 +1542,6 @@ "node": ">=6.0.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/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1683,24 +1610,6 @@ ], "license": "CC-BY-4.0" }, - "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/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1722,12 +1631,6 @@ "node": ">= 0.8" } }, - "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1909,42 +1812,6 @@ "node": ">=0.4.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dompurify": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", - "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", - "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/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1965,13 +1832,6 @@ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", "license": "ISC" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.331", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", @@ -1979,16 +1839,6 @@ "dev": true, "license": "ISC" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2093,30 +1943,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -2166,32 +1992,6 @@ "node": ">= 6" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2226,6 +2026,12 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2263,6 +2069,38 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2275,19 +2113,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "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/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2327,26 +2152,25 @@ "node": ">= 0.4" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" }, "node_modules/immer": { "version": "10.2.0", @@ -2358,12 +2182,14 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/internmap": { "version": "2.0.3", @@ -2374,14 +2200,13 @@ "node": ">=12" } }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/js-tokens": { @@ -2391,15 +2216,6 @@ "dev": true, "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/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2413,6 +2229,12 @@ "node": ">=6" } }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2426,19 +2248,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -2454,23 +2263,14 @@ "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/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/lru-cache": { "version": "5.1.1", @@ -2482,14 +2282,45 @@ "yallist": "^3.0.2" } }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "license": "MIT", + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", "dependencies": { - "sourcemap-codec": "^1.4.8" + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" } }, "node_modules/math-intrinsics": { @@ -2501,12 +2332,6 @@ "node": ">= 0.4" } }, - "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": "0.18.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", @@ -2514,19 +2339,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2548,6 +2360,15 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2555,6 +2376,12 @@ "dev": true, "license": "MIT" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2581,12 +2408,6 @@ "dev": true, "license": "MIT" }, - "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/oidc-client-ts": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", @@ -2599,33 +2420,17 @@ "node": ">=18" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", "dependencies": { - "ee-first": "1.1.1" + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" }, - "engines": { - "node": ">= 0.8" - } - }, - "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/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + "bin": { + "pbf": "bin/pbf" } }, "node_modules/picocolors": { @@ -2635,19 +2440,6 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -2677,29 +2469,17 @@ "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/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" }, "node_modules/proxy-from-env": { "version": "2.1.0", @@ -2716,25 +2496,6 @@ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "license": "ISC" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -2885,6 +2646,15 @@ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "license": "MIT" }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -2930,6 +2700,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2946,77 +2722,12 @@ "semver": "bin/semver.js" } }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3027,22 +2738,13 @@ "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" } }, "node_modules/three": { @@ -3057,35 +2759,11 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "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/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" }, "node_modules/typescript": { "version": "5.9.3", @@ -3101,22 +2779,6 @@ "node": ">=14.17" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3148,12 +2810,6 @@ "browserslist": ">= 4.21.0" } }, - "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/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -3245,53 +2901,30 @@ } } }, - "node_modules/vite-plugin-cesium": { - "version": "1.2.23", - "resolved": "https://registry.npmjs.org/vite-plugin-cesium/-/vite-plugin-cesium-1.2.23.tgz", - "integrity": "sha512-x9A8ZCEoegceXg/E+LnxKr0XBsI9CR4cgYWQ2Dd3cUEYwKcTnHQ3kBfpol7BUcGtgQnQos/mtVrRmuVQBXFjHw==", - "dev": true, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", "license": "MIT", "dependencies": { - "fs-extra": "^9.1.0", - "rollup-plugin-external-globals": "^0.6.1", - "serve-static": "^1.14.1" - }, - "peerDependencies": { - "cesium": "^1.95.0", - "vite": ">=2.7.1" + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" } }, - "node_modules/vite-plugin-cesium/node_modules/rollup": { - "version": "2.80.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", - "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", - "dev": true, - "license": "MIT", - "peer": true, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, "bin": { - "rollup": "dist/bin/rollup" + "node-which": "bin/which.js" }, "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/vite-plugin-cesium/node_modules/rollup-plugin-external-globals": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.6.1.tgz", - "integrity": "sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^4.0.0", - "estree-walker": "^2.0.1", - "is-reference": "^1.2.1", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^2.25.0" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/yallist": { diff --git a/web/package.json b/web/package.json index 509934b..0136de3 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ "dependencies": { "@mkkellogg/gaussian-splats-3d": "^0.4.6", "axios": "^1.7.9", - "cesium": "^1.124.0", + "maplibre-gl": "^4.7.1", "oidc-client-ts": "^3.1.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -22,12 +22,12 @@ }, "devDependencies": { "@types/geojson": "^7946.0.16", + "@types/maplibre-gl": "^1.0.0", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/three": "^0.171.0", "@vitejs/plugin-react": "^4.3.4", "typescript": "^5.7.2", - "vite": "^5.4.11", - "vite-plugin-cesium": "^1.2.22" + "vite": "^5.4.11" } } diff --git a/web/src/App.tsx b/web/src/App.tsx index d07523b..fcb6cd3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,7 +1,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom' import { AuthProvider } from './auth/AuthProvider' import { CallbackPage } from './auth/CallbackPage' -import { CesiumViewer } from './cesium/CesiumViewer' +import { MapLibreViewer } from './maplibre/MapLibreViewer' import { SplatLayer } from './splat/SplatLayer' import { SplatRenderer } from './splat/SplatRenderer' import { ChallengeLayer } from './challenges/ChallengeLayer' @@ -17,17 +17,17 @@ import { UserProfilePage } from './users/UserProfilePage' function MapPage() { return ( - - {/* Imperative Cesium layers — render no DOM, manage entities */} + + {/* Map-layer components — render no DOM, manage MapLibre sources */} - {/* Three.js splat overlay — portalled canvas above Cesium */} + {/* Three.js splat overlay — portalled canvas above the map */} - {/* React UI — z-indexed above Cesium canvas */} + {/* React UI — z-indexed above the map canvas */} - + ) } diff --git a/web/src/cesium/CesiumViewer.tsx b/web/src/cesium/CesiumViewer.tsx deleted file mode 100644 index c5c43b6..0000000 --- a/web/src/cesium/CesiumViewer.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useRef, useState } from 'react' -import * as Cesium from 'cesium' -import 'cesium/Build/Cesium/Widgets/widgets.css' -import { CesiumContext } from './cesiumContext' - -interface Props { - children?: React.ReactNode -} - -export function CesiumViewer({ children }: Props) { - const containerRef = useRef(null) - const [viewer, setViewer] = useState(null) - - useEffect(() => { - // Guard: only create if the container is mounted and no viewer yet - if (!containerRef.current || viewer) return - - Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_ION_TOKEN ?? '' - - const v = new Cesium.Viewer(containerRef.current, { - terrainProvider: new Cesium.EllipsoidTerrainProvider(), - homeButton: false, - baseLayerPicker: false, - navigationHelpButton: false, - animation: false, - timeline: false, - geocoder: false, - sceneModePicker: false, - fullscreenButton: false, - infoBox: false, - selectionIndicator: false, - }) - - // Async: upgrade to world terrain after initial load - Cesium.createWorldTerrainAsync() - .then((tp) => { - if (!v.isDestroyed()) v.terrainProvider = tp - }) - .catch(() => {/* non-fatal: fall back to ellipsoid */}) - - // Async: switch base imagery to Bing Aerial with Labels - Cesium.createWorldImageryAsync({ style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS }) - .then((ip) => { - if (!v.isDestroyed()) v.imageryLayers.get(0).imageryProvider = ip - }) - .catch(() => {/* non-fatal: keep default imagery */}) - - setViewer(v) - - return () => { - if (!v.isDestroyed()) v.destroy() - setViewer(null) - } - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - return ( - <> - {/* Cesium mounts itself into this div and fills it completely */} -
- {/* Provide viewer to all children; only render children once viewer is ready */} - {viewer && ( - - {children} - - )} - - ) -} diff --git a/web/src/cesium/cesiumContext.ts b/web/src/cesium/cesiumContext.ts deleted file mode 100644 index 28d6ff9..0000000 --- a/web/src/cesium/cesiumContext.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createContext, useContext } from 'react' -import type * as CesiumType from 'cesium' - -type Viewer = InstanceType - -export const CesiumContext = createContext(null) - -export function useCesiumViewer(): Viewer { - const viewer = useContext(CesiumContext) - if (!viewer) { - throw new Error('useCesiumViewer must be used inside ') - } - return viewer -} diff --git a/web/src/cesium/geoUtils.ts b/web/src/cesium/geoUtils.ts deleted file mode 100644 index a2ec58b..0000000 --- a/web/src/cesium/geoUtils.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as Cesium from 'cesium' -import * as THREE from 'three' - -/** - * Build a Three.js Matrix4 that positions and orients a local scene - * at the given geographic coordinate. - * - * The returned matrix transforms from local ENU space (metres from the - * anchor point, X=East, Y=North, Z=Up) to Cesium ECEF space (metres - * from Earth centre). Apply it to a Three.js Object3D.matrixWorld and - * set matrixAutoUpdate = false. - * - * @param lon Longitude in degrees - * @param lat Latitude in degrees - * @param alt Altitude in metres above WGS-84 ellipsoid - * @param headingDeg Clockwise heading in degrees (0 = North, 90 = East) - */ -export function buildSplatWorldMatrix( - lon: number, - lat: number, - alt: number, - headingDeg: number, -): THREE.Matrix4 { - const position = Cesium.Cartesian3.fromDegrees(lon, lat, alt) - - // 4×4 column-major matrix: local ENU → ECEF - const enuToEcef = Cesium.Transforms.eastNorthUpToFixedFrame(position) - - // Apply a rotation around local Up (Z in ENU) for heading. - // Cesium heading is clockwise from North, which is –Z rotation in ENU. - const headingRad = Cesium.Math.toRadians(-headingDeg) - const headingRotation = Cesium.Matrix4.fromRotationTranslation( - Cesium.Matrix3.fromRotationZ(headingRad), - ) - const finalCesiumMatrix = new Cesium.Matrix4() - Cesium.Matrix4.multiply(enuToEcef, headingRotation, finalCesiumMatrix) - - // Cesium Matrix4 is a Float64Array in column-major order. - // Three.js Matrix4 uses Float32Array, also column-major. - // Direct cast works since both use the same element layout. - const threeMatrix = new THREE.Matrix4() - threeMatrix.set( - finalCesiumMatrix[0], finalCesiumMatrix[4], finalCesiumMatrix[8], finalCesiumMatrix[12], - finalCesiumMatrix[1], finalCesiumMatrix[5], finalCesiumMatrix[9], finalCesiumMatrix[13], - finalCesiumMatrix[2], finalCesiumMatrix[6], finalCesiumMatrix[10], finalCesiumMatrix[14], - finalCesiumMatrix[3], finalCesiumMatrix[7], finalCesiumMatrix[11], finalCesiumMatrix[15], - ) - return threeMatrix -} - -/** - * Convert a Cesium Rectangle (radians) to a bbox tuple (degrees). - */ -export function rectangleToBbox( - rect: Cesium.Rectangle, -): [number, number, number, number] { - return [ - Cesium.Math.toDegrees(rect.west), - Cesium.Math.toDegrees(rect.south), - Cesium.Math.toDegrees(rect.east), - Cesium.Math.toDegrees(rect.north), - ] -} diff --git a/web/src/cesium/useCesiumCamera.ts b/web/src/cesium/useCesiumCamera.ts deleted file mode 100644 index b1ea1ad..0000000 --- a/web/src/cesium/useCesiumCamera.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect } from 'react' -import { useCesiumViewer } from './cesiumContext' -import { useMapStore } from '../store/mapStore' -import { rectangleToBbox } from './geoUtils' - -/** - * Attaches a scene.preUpdate listener that writes camera height and - * the current view bbox to mapStore on every frame. - * - * Throttled so the store update fires at most once per 200 ms to avoid - * triggering expensive API queries on every rendered frame. - */ -export function useCesiumCamera() { - const viewer = useCesiumViewer() - const setCameraState = useMapStore((s) => s.setCameraState) - - useEffect(() => { - let lastFired = 0 - const THROTTLE_MS = 200 - - const removeListener = viewer.scene.preUpdate.addEventListener(() => { - const now = Date.now() - if (now - lastFired < THROTTLE_MS) return - lastFired = now - - const height = viewer.camera.positionCartographic.height - const rect = viewer.camera.computeViewRectangle() - if (rect) { - setCameraState(height, rectangleToBbox(rect)) - } - }) - - return () => { - removeListener() - } - }, [viewer, setCameraState]) -} diff --git a/web/src/challenges/ChallengeLayer.tsx b/web/src/challenges/ChallengeLayer.tsx index cc1d779..f9e31be 100644 --- a/web/src/challenges/ChallengeLayer.tsx +++ b/web/src/challenges/ChallengeLayer.tsx @@ -1,31 +1,53 @@ import { useEffect, useRef } from 'react' -import * as Cesium from 'cesium' -import { useCesiumViewer } from '../cesium/cesiumContext' +import maplibregl, { type GeoJSONSource } from 'maplibre-gl' +import { useMapLibreMap } from '../maplibre/maplibreContext' +import { safeRemoveLayers } from '../maplibre/geoUtils' import { useMapStore } from '../store/mapStore' import { useChallengeStore } from '../store/challengeStore' import { usePolygonDraw } from './usePolygonDraw' -import { fetchChallenges } from '../api/challenges' +import { fetchChallenges, fetchChallengeDetail } from '../api/challenges' import type { BBox } from '../types/geo' import type { ChallengeMapProperties } from '../types/api' const CHALLENGE_VISIBLE_HEIGHT = 200_000 +const SRC_REGION = 'challenge-region' +const LYR_REGION_FILL = 'challenge-region-fill' +const LYR_REGION_LINE = 'challenge-region-line' + +function emptyFC(): GeoJSON.FeatureCollection { return { type: 'FeatureCollection', features: [] } } + export function ChallengeLayer() { - const viewer = useCesiumViewer() + const map = useMapLibreMap() usePolygonDraw() const { bbox, cameraHeight, setLoadedChallenges } = useMapStore() const { selectedChallengeId, setSelectedChallengeId } = useChallengeStore() - const entityMapRef = useRef>(new Map()) - const regionEntityRef = useRef(null) + const markersRef = useRef>(new Map()) const lastBboxRef = useRef(null) - // Fetch and render challenge pins + // ── Set up region polygon sources + layers ───────────────────────────────── + useEffect(() => { + map.addSource(SRC_REGION, { type: 'geojson', data: emptyFC() }) + map.addLayer({ id: LYR_REGION_FILL, type: 'fill', source: SRC_REGION, + paint: { 'fill-color': '#fbbf24', 'fill-opacity': 0.15 } }) + map.addLayer({ id: LYR_REGION_LINE, type: 'line', source: SRC_REGION, + paint: { 'line-color': '#fbbf24', 'line-width': 2 } }) + + return () => { + safeRemoveLayers(map, + [LYR_REGION_FILL, LYR_REGION_LINE], + [SRC_REGION], + ) + } + }, [map]) + + // ── Fetch and render challenge markers ───────────────────────────────────── useEffect(() => { if (!bbox || cameraHeight > CHALLENGE_VISIBLE_HEIGHT) { - entityMapRef.current.forEach((e) => viewer.entities.remove(e)) - entityMapRef.current.clear() + markersRef.current.forEach(m => m.remove()) + markersRef.current.clear() setLoadedChallenges([]) return } @@ -43,101 +65,68 @@ export function ChallengeLayer() { fetchChallenges({ bbox }).then((fc) => { const incoming = new Set(fc.features.map((f: GeoJSON.Feature) => String(f.id))) - entityMapRef.current.forEach((entity, id) => { - if (!incoming.has(id)) { - viewer.entities.remove(entity) - entityMapRef.current.delete(id) - } + markersRef.current.forEach((marker, id) => { + if (!incoming.has(id)) { marker.remove(); markersRef.current.delete(id) } }) fc.features.forEach((feature: GeoJSON.Feature) => { const id = String(feature.id) - if (entityMapRef.current.has(id)) return + if (markersRef.current.has(id)) return const [lon, lat] = feature.geometry.coordinates + const el = createChallengePinElement() + el.addEventListener('click', () => setSelectedChallengeId(id)) - const entity = viewer.entities.add({ - id: `challenge-${id}`, - position: Cesium.Cartesian3.fromDegrees(lon, lat), - billboard: { - image: createChallengePinSvg(), - width: 36, - height: 36, - verticalOrigin: Cesium.VerticalOrigin.BOTTOM, - disableDepthTestDistance: Number.POSITIVE_INFINITY, - }, - properties: { challengeId: id }, - }) - entityMapRef.current.set(id, entity) + const marker = new maplibregl.Marker({ element: el, anchor: 'bottom' }) + .setLngLat([lon, lat]) + .addTo(map) + markersRef.current.set(id, marker) }) setLoadedChallenges(fc.features) }).catch(console.error) }, [bbox, cameraHeight]) // eslint-disable-line react-hooks/exhaustive-deps - // Show region polygon for selected challenge + // ── Show region polygon for selected challenge ───────────────────────────── useEffect(() => { - if (regionEntityRef.current) { - viewer.entities.remove(regionEntityRef.current) - regionEntityRef.current = null + const src = map.getSource(SRC_REGION) as GeoJSONSource | undefined + if (!src) return + + if (!selectedChallengeId) { + src.setData(emptyFC()) + return } - if (!selectedChallengeId) return - - // We need the full detail to get the region polygon — ChallengePanel fetches - // it; we read from the DOM or re-fetch. For simplicity, re-fetch here. - import('../api/challenges').then(({ fetchChallengeDetail }) => - fetchChallengeDetail(selectedChallengeId), - ).then((detail) => { + fetchChallengeDetail(selectedChallengeId).then((detail) => { if (!detail.region) return - const coords = detail.region.coordinates[0] - const positions = coords.map((c) => - Cesium.Cartesian3.fromDegrees(c[0], c[1]), - ) - - regionEntityRef.current = viewer.entities.add({ - polygon: { - hierarchy: new Cesium.PolygonHierarchy(positions), - material: Cesium.Color.YELLOW.withAlpha(0.15), - outline: true, - outlineColor: Cesium.Color.YELLOW, - outlineWidth: 2, - heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, - }, + src.setData({ + type: 'Feature', + geometry: detail.region, + properties: {}, }) }).catch(console.error) - }, [selectedChallengeId, viewer]) + }, [selectedChallengeId, map]) - // Wire up entity selection - useEffect(() => { - const remove = viewer.selectedEntityChanged.addEventListener((entity) => { - if (!entity) return - const challengeId = entity.properties?.challengeId?.getValue() - if (challengeId) setSelectedChallengeId(challengeId) - }) - return () => remove() - }, [viewer, setSelectedChallengeId]) - - // Cleanup on unmount + // ── Cleanup on unmount ───────────────────────────────────────────────────── useEffect(() => { return () => { - if (viewer.isDestroyed()) return - entityMapRef.current.forEach((e) => viewer.entities.remove(e)) - entityMapRef.current.clear() - if (regionEntityRef.current) viewer.entities.remove(regionEntityRef.current) + markersRef.current.forEach(m => m.remove()) + markersRef.current.clear() } - }, [viewer]) + }, []) return null } -function createChallengePinSvg(): string { - const svg = ` +function createChallengePinElement(): HTMLElement { + const el = document.createElement('div') + el.style.cssText = 'width:36px;height:36px;cursor:pointer' + el.innerHTML = ` ! ` - return `data:image/svg+xml;base64,${btoa(svg)}` + return el } diff --git a/web/src/challenges/ChallengePanel.tsx b/web/src/challenges/ChallengePanel.tsx index b2193d4..4aacbd5 100644 --- a/web/src/challenges/ChallengePanel.tsx +++ b/web/src/challenges/ChallengePanel.tsx @@ -1,15 +1,14 @@ import { useEffect, useState } from 'react' -import * as Cesium from 'cesium' import { useNavigate } from 'react-router-dom' import { Panel } from '../ui/Panel' -import { useCesiumViewer } from '../cesium/cesiumContext' +import { useMapLibreMap } from '../maplibre/maplibreContext' import { useChallengeStore } from '../store/challengeStore' import { fetchChallengeDetail, participateInChallenge } from '../api/challenges' import type { ChallengeDetail } from '../types/api' import styles from './ChallengePanel.module.css' export function ChallengePanel() { - const viewer = useCesiumViewer() + const map = useMapLibreMap() const navigate = useNavigate() const { selectedChallengeId, setSelectedChallengeId } = useChallengeStore() const [detail, setDetail] = useState(null) @@ -31,10 +30,7 @@ export function ChallengePanel() { function handleCenterMap() { if (!detail?.region_centroid) return const [lon, lat] = detail.region_centroid.coordinates - viewer.camera.flyTo({ - destination: Cesium.Cartesian3.fromDegrees(lon, lat, 2000), - duration: 1.5, - }) + map.flyTo({ center: [lon, lat], zoom: 14, pitch: 50, duration: 1500 }) } async function handleParticipate() { diff --git a/web/src/challenges/usePolygonDraw.ts b/web/src/challenges/usePolygonDraw.ts index 2961e90..07f4989 100644 --- a/web/src/challenges/usePolygonDraw.ts +++ b/web/src/challenges/usePolygonDraw.ts @@ -1,158 +1,157 @@ import { useEffect, useRef } from 'react' -import * as Cesium from 'cesium' -import { useCesiumViewer } from '../cesium/cesiumContext' +import type { Map, MapMouseEvent, GeoJSONSource, MapLayerMouseEvent } from 'maplibre-gl' +import { useMapLibreMap } from '../maplibre/maplibreContext' +import { safeRemoveLayers } from '../maplibre/geoUtils' import { useChallengeStore } from '../store/challengeStore' -function vertsToGeoJson(verts: Cesium.Cartesian3[]): GeoJSON.Polygon { - const coords: [number, number][] = verts.map((v) => { - const c = Cesium.Cartographic.fromCartesian(v) - return [Cesium.Math.toDegrees(c.longitude), Cesium.Math.toDegrees(c.latitude)] - }) - return { type: 'Polygon', coordinates: [[...coords, coords[0]]] } +// ── Layer / source IDs ──────────────────────────────────────────────────────── + +const SRC_FILL = 'pd-fill' +const SRC_OUTLINE = 'pd-outline' +const SRC_RUBBER = 'pd-rubber' +const SRC_VERTICES = 'pd-vertices' +const LYR_FILL = 'pd-fill-lyr' +const LYR_OUTLINE = 'pd-outline-lyr' +const LYR_RUBBER = 'pd-rubber-lyr' +const LYR_VERTICES = 'pd-vertices-lyr' + +function emptyFC(): GeoJSON.FeatureCollection { + return { type: 'FeatureCollection', features: [] } } -function geoJsonToVerts(polygon: GeoJSON.Polygon): Cesium.Cartesian3[] { - const ring = polygon.coordinates[0] - // Drop the closing duplicate point - return ring.slice(0, -1).map(([lon, lat]) => Cesium.Cartesian3.fromDegrees(lon, lat)) +function addDrawLayers(map: Map) { + map.addSource(SRC_FILL, { type: 'geojson', data: emptyFC() }) + map.addSource(SRC_OUTLINE, { type: 'geojson', data: emptyFC() }) + map.addSource(SRC_RUBBER, { type: 'geojson', data: emptyFC() }) + map.addSource(SRC_VERTICES, { type: 'geojson', data: emptyFC() }) + + map.addLayer({ id: LYR_FILL, type: 'fill', source: SRC_FILL, + paint: { 'fill-color': '#fbbf24', 'fill-opacity': 0.15 } }) + map.addLayer({ id: LYR_OUTLINE, type: 'line', source: SRC_OUTLINE, + paint: { 'line-color': '#fbbf24', 'line-width': 2 } }) + map.addLayer({ id: LYR_RUBBER, type: 'line', source: SRC_RUBBER, + paint: { 'line-color': '#ffffff', 'line-width': 1.5, 'line-dasharray': [4, 4] } }) + map.addLayer({ id: LYR_VERTICES, type: 'circle', source: SRC_VERTICES, + paint: { + 'circle-radius': 6, + 'circle-color': '#ffffff', + 'circle-stroke-color': '#000000', + 'circle-stroke-width': 2, + } }) } -function pickGlobe( - viewer: Cesium.Viewer, - windowPos: Cesium.Cartesian2, -): Cesium.Cartesian3 | null { - const ray = viewer.camera.getPickRay(windowPos) - if (!ray) return null - return viewer.scene.globe.pick(ray, viewer.scene) ?? null +function removeDrawLayers(map: Map) { + safeRemoveLayers(map, + [LYR_FILL, LYR_OUTLINE, LYR_RUBBER, LYR_VERTICES], + [SRC_FILL, SRC_OUTLINE, SRC_RUBBER, SRC_VERTICES], + ) +} + +function setSource(map: Map, id: string, data: GeoJSON.GeoJSON) { + (map.getSource(id) as GeoJSONSource | undefined)?.setData(data) } /** * Manages two phases of polygon interaction: * * Drawing (drawingMode=true) - * LEFT_CLICK → place vertex - * MOUSE_MOVE → rubber-band line from last vertex to cursor - * RIGHT_CLICK → close polygon (≥3 verts), enter edit phase + * click → place vertex + * mousemove → rubber-band line from last vertex to cursor + * contextmenu → close polygon (≥3 verts), enter edit phase * * Editing (drawingMode=false, draftPolygon set) * Drag vertex handles to reposition them. - * Changes are written back to the store on mouse-up so the submit - * form always reads the latest geometry. */ export function usePolygonDraw() { - const viewer = useCesiumViewer() + const map = useMapLibreMap() const { drawingMode, setDrawingMode, setDraftPolygon, draftPolygon } = useChallengeStore() - // Persists vertex positions across edit-phase effect re-runs that are - // triggered by setDraftPolygon being called after each drag. - const editVertsRef = useRef([]) + const editVertsRef = useRef<[number, number][]>([]) + + // ── Set up / tear down MapLibre sources + layers ────────────────────────── + useEffect(() => { + addDrawLayers(map) + return () => { removeDrawLayers(map) } + }, [map]) // ── Drawing phase ────────────────────────────────────────────────────────── useEffect(() => { if (!drawingMode) return - const verts: Cesium.Cartesian3[] = [] - const vertPointEntities: Cesium.Entity[] = [] - let outlineEntity: Cesium.Entity | null = null - let rubberBandEntity: Cesium.Entity | null = null + const verts: [number, number][] = [] + const canvas = map.getCanvas() - const canvas = viewer.scene.canvas - - // Prevent the browser context menu so right-click can close the polygon. - const suppressContextMenu = (e: MouseEvent) => e.preventDefault() - canvas.addEventListener('contextmenu', suppressContextMenu) - - const handler = new Cesium.ScreenSpaceEventHandler(canvas) + canvas.style.cursor = 'crosshair' function refreshOutline() { - if (outlineEntity) { - viewer.entities.remove(outlineEntity) - outlineEntity = null + if (verts.length < 2) { + setSource(map, SRC_OUTLINE, emptyFC()) + return } - if (verts.length < 2) return - outlineEntity = viewer.entities.add({ - polyline: { - positions: [...verts, verts[0]], - width: 2, - material: new Cesium.ColorMaterialProperty( - Cesium.Color.YELLOW.withAlpha(0.9), - ), - clampToGround: true, - }, + setSource(map, SRC_OUTLINE, { + type: 'Feature', + geometry: { type: 'LineString', coordinates: [...verts, verts[0]] }, + properties: {}, }) } - function refreshRubberBand(mousePos: Cesium.Cartesian3) { - if (rubberBandEntity) { - viewer.entities.remove(rubberBandEntity) - rubberBandEntity = null - } + function refreshVertices() { + setSource(map, SRC_VERTICES, { + type: 'FeatureCollection', + features: verts.map((v, i) => ({ + type: 'Feature', + id: i, + geometry: { type: 'Point', coordinates: v }, + properties: {}, + })), + }) + } + + const onMouseMove = (e: MapMouseEvent) => { if (verts.length === 0) return - rubberBandEntity = viewer.entities.add({ - polyline: { - positions: [verts[verts.length - 1], mousePos], - width: 1.5, - material: new Cesium.ColorMaterialProperty( - Cesium.Color.WHITE.withAlpha(0.5), - ), - clampToGround: true, - }, + setSource(map, SRC_RUBBER, { + type: 'Feature', + geometry: { type: 'LineString', coordinates: [verts[verts.length - 1], [e.lngLat.lng, e.lngLat.lat]] }, + properties: {}, }) } - handler.setInputAction((e: { endPosition: Cesium.Cartesian2 }) => { - const pos = pickGlobe(viewer, e.endPosition) - if (pos) refreshRubberBand(pos) - }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) - - handler.setInputAction((e: { position: Cesium.Cartesian2 }) => { - const pos = pickGlobe(viewer, e.position) - if (!pos) return - verts.push(pos.clone()) - vertPointEntities.push( - viewer.entities.add({ - position: pos, - point: { - pixelSize: 8, - color: Cesium.Color.YELLOW, - outlineColor: Cesium.Color.BLACK, - outlineWidth: 1, - disableDepthTestDistance: Number.POSITIVE_INFINITY, - }, - }), - ) + const onClick = (e: MapMouseEvent) => { + verts.push([e.lngLat.lng, e.lngLat.lat]) refreshOutline() - }, Cesium.ScreenSpaceEventType.LEFT_CLICK) + refreshVertices() + } - handler.setInputAction(() => { + const onContextMenu = (e: MouseEvent) => { + e.preventDefault() if (verts.length < 3) return - const polygon = vertsToGeoJson(verts) + const polygon: GeoJSON.Polygon = { + type: 'Polygon', + coordinates: [[...verts, verts[0]]], + } cleanup() setDraftPolygon(polygon) setDrawingMode(false) - }, Cesium.ScreenSpaceEventType.RIGHT_CLICK) + } + + canvas.addEventListener('contextmenu', onContextMenu) + map.on('click', onClick) + map.on('mousemove', onMouseMove) function cleanup() { - if (viewer.isDestroyed()) return - vertPointEntities.forEach((e) => viewer.entities.remove(e)) - vertPointEntities.length = 0 - if (outlineEntity) { - viewer.entities.remove(outlineEntity) - outlineEntity = null - } - if (rubberBandEntity) { - viewer.entities.remove(rubberBandEntity) - rubberBandEntity = null - } + canvas.style.cursor = '' + map.off('click', onClick) + map.off('mousemove', onMouseMove) + canvas.removeEventListener('contextmenu', onContextMenu) + setSource(map, SRC_FILL, emptyFC()) + setSource(map, SRC_OUTLINE, emptyFC()) + setSource(map, SRC_RUBBER, emptyFC()) + setSource(map, SRC_VERTICES, emptyFC()) } - return () => { - handler.destroy() - canvas.removeEventListener('contextmenu', suppressContextMenu) - cleanup() - } - }, [drawingMode, viewer, setDrawingMode, setDraftPolygon]) + return cleanup + }, [drawingMode, map, setDrawingMode, setDraftPolygon]) // ── Edit phase ───────────────────────────────────────────────────────────── useEffect(() => { @@ -161,120 +160,84 @@ export function usePolygonDraw() { return } - // Only initialise from the store on the first entry into edit mode. - // Subsequent runs (triggered by setDraftPolygon after each drag) reuse - // the already-mutated ref so vertex positions are not reset. + // Only initialise from the store on first entry into edit mode. if (editVertsRef.current.length === 0) { - editVertsRef.current = geoJsonToVerts(draftPolygon) + const ring = draftPolygon.coordinates[0] + editVertsRef.current = ring.slice(0, -1) as [number, number][] } const verts = editVertsRef.current let draggingIndex = -1 - const entities: Cesium.Entity[] = [] - const canvas = viewer.scene.canvas - const suppressContextMenu = (e: MouseEvent) => e.preventDefault() - canvas.addEventListener('contextmenu', suppressContextMenu) - - // Filled polygon - entities.push( - viewer.entities.add({ - polygon: { - hierarchy: new Cesium.CallbackProperty( - () => new Cesium.PolygonHierarchy(verts), - false, - ), - material: Cesium.Color.YELLOW.withAlpha(0.15), - }, - }), - ) - - // Outline - entities.push( - viewer.entities.add({ - polyline: { - positions: new Cesium.CallbackProperty( - () => [...verts, verts[0]], - false, - ), - width: 2, - material: new Cesium.ColorMaterialProperty( - Cesium.Color.YELLOW.withAlpha(0.9), - ), - clampToGround: true, - }, - }), - ) - - // Vertex handles — one point entity per vertex, driven by CallbackProperty - // so they track the mutable verts array without entity recreation. - const vertEntities: Cesium.Entity[] = verts.map((_, i) => { - const e = viewer.entities.add({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - position: new Cesium.CallbackProperty(() => verts[i], false) as any, - point: { - pixelSize: 10, - color: Cesium.Color.WHITE, - outlineColor: Cesium.Color.YELLOW, - outlineWidth: 2, - disableDepthTestDistance: Number.POSITIVE_INFINITY, - }, + function refreshAll() { + setSource(map, SRC_FILL, { + type: 'Feature', + geometry: { type: 'Polygon', coordinates: [[...verts, verts[0]]] }, + properties: {}, }) - entities.push(e) - return e - }) + setSource(map, SRC_OUTLINE, { + type: 'Feature', + geometry: { type: 'LineString', coordinates: [...verts, verts[0]] }, + properties: {}, + }) + setSource(map, SRC_VERTICES, { + type: 'FeatureCollection', + features: verts.map((v, i) => ({ + type: 'Feature', + id: i, + geometry: { type: 'Point', coordinates: v }, + properties: { idx: i }, + })), + }) + } - const handler = new Cesium.ScreenSpaceEventHandler(canvas) + refreshAll() - handler.setInputAction((e: { endPosition: Cesium.Cartesian2 }) => { - if (draggingIndex !== -1) { - // Update dragged vertex - const pos = pickGlobe(viewer, e.endPosition) - if (pos) verts[draggingIndex] = pos - return - } - // Cursor feedback - const picked = viewer.scene.pick(e.endPosition) - const overVertex = - picked?.id instanceof Cesium.Entity && - vertEntities.includes(picked.id) - canvas.style.cursor = overVertex ? 'grab' : 'default' - }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) + const canvas = map.getCanvas() - handler.setInputAction((e: { position: Cesium.Cartesian2 }) => { - const picked = viewer.scene.pick(e.position) - if (!(picked?.id instanceof Cesium.Entity)) return - const idx = vertEntities.indexOf(picked.id) - if (idx === -1) return + const onVertexMouseDown = (e: MapLayerMouseEvent) => { + const idx = e.features?.[0]?.properties?.idx as number | undefined + if (idx == null) return + e.preventDefault() draggingIndex = idx canvas.style.cursor = 'grabbing' - viewer.scene.screenSpaceCameraController.enableRotate = false - viewer.scene.screenSpaceCameraController.enableTranslate = false - }, Cesium.ScreenSpaceEventType.LEFT_DOWN) + map.dragPan.disable() + } - handler.setInputAction(() => { + const onMouseMove = (e: MapMouseEvent) => { + if (draggingIndex !== -1) { + verts[draggingIndex] = [e.lngLat.lng, e.lngLat.lat] + refreshAll() + return + } + const features = map.queryRenderedFeatures(e.point, { layers: [LYR_VERTICES] }) + canvas.style.cursor = features.length ? 'grab' : '' + } + + const onMouseUp = () => { if (draggingIndex === -1) return draggingIndex = -1 - canvas.style.cursor = 'default' - viewer.scene.screenSpaceCameraController.enableRotate = true - viewer.scene.screenSpaceCameraController.enableTranslate = true - // Sync updated geometry back to the store for the submit form. - setDraftPolygon(vertsToGeoJson(verts)) - }, Cesium.ScreenSpaceEventType.LEFT_UP) - - function cleanup() { - if (viewer.isDestroyed()) return - entities.forEach((e) => viewer.entities.remove(e)) - entities.length = 0 - canvas.style.cursor = 'default' - viewer.scene.screenSpaceCameraController.enableRotate = true - viewer.scene.screenSpaceCameraController.enableTranslate = true + canvas.style.cursor = '' + map.dragPan.enable() + setDraftPolygon({ + type: 'Polygon', + coordinates: [[...verts, verts[0]]], + }) } + map.on('mousedown', LYR_VERTICES, onVertexMouseDown) + map.on('mousemove', onMouseMove) + map.on('mouseup', onMouseUp) + return () => { - handler.destroy() - canvas.removeEventListener('contextmenu', suppressContextMenu) - cleanup() + map.off('mousedown', LYR_VERTICES, onVertexMouseDown) + map.off('mousemove', onMouseMove) + map.off('mouseup', onMouseUp) + map.dragPan.enable() + canvas.style.cursor = '' + setSource(map, SRC_FILL, emptyFC()) + setSource(map, SRC_OUTLINE, emptyFC()) + setSource(map, SRC_VERTICES, emptyFC()) } - }, [drawingMode, draftPolygon, viewer, setDraftPolygon]) + }, [drawingMode, draftPolygon, map, setDraftPolygon]) } diff --git a/web/src/coaster/CoasterEditorPage.module.css b/web/src/coaster/CoasterEditorPage.module.css index 015975a..5b38b6f 100644 --- a/web/src/coaster/CoasterEditorPage.module.css +++ b/web/src/coaster/CoasterEditorPage.module.css @@ -521,15 +521,33 @@ .rightColumn { position: fixed; right: 16px; - top: 60px; - z-index: 600; + top: 116px; + z-index: 150; display: flex; flex-direction: column; gap: 8px; - max-height: calc(100vh - 76px); + max-height: calc(100vh - 132px); overflow-y: auto; } +/* ── Path hover tooltip ──────────────────────────────────────────────────────── */ + +.pathTooltip { + position: fixed; + z-index: 300; + pointer-events: none; + padding: 3px 8px; + background: rgba(8, 8, 12, 0.85); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: #fbbf24; + white-space: nowrap; +} + /* ── Loading overlay ─────────────────────────────────────────────────────────── */ .loading { diff --git a/web/src/coaster/CoasterEditorPage.tsx b/web/src/coaster/CoasterEditorPage.tsx index 151fda1..3dd9c75 100644 --- a/web/src/coaster/CoasterEditorPage.tsx +++ b/web/src/coaster/CoasterEditorPage.tsx @@ -1,20 +1,24 @@ import { useEffect, useRef, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' -import * as Cesium from 'cesium' -import { CesiumViewer } from '../cesium/CesiumViewer' -import { useCesiumViewer } from '../cesium/cesiumContext' +import type { Map, GeoJSONSource } from 'maplibre-gl' +import { MapLibreViewer } from '../maplibre/MapLibreViewer' +import { useMapLibreMap } from '../maplibre/maplibreContext' +import { safeRemoveLayers } from '../maplibre/geoUtils' +import { createCustom3DLayer } from '../maplibre/custom3DLayer' +import type { Layer3DHandle } from '../maplibre/custom3DLayer' import { fetchChallengeDetail } from '../api/challenges' import { simulateCoaster, saveCoaster, listCoasters } from '../api/coaster' import { useCoasterPath } from './useCoasterPath' -import { useAccelerationStrips } from './useAccelerationStrips' +import { useAccelerationStrips, computeArcLengths, snapToPath } from './useAccelerationStrips' import { useTerrainCapture } from './useTerrainCapture' import { RideRenderer } from './RideRenderer' import { SimulationPlots } from './SimulationPlots' import { AccelerationStripsPanel } from './AccelerationStripsPanel' import { CoasterListPanel } from './CoasterListPanel' -import { effectivePosition } from './bezierUtils' +import { effectiveLngLatAlt } from './bezierUtils' import { useAuthStore } from '../store/authStore' import type { ChallengeDetail, CoasterSimulationResult, SavedCoaster } from '../types/api' +import type { AnchorPoint } from './bezierUtils' import styles from './CoasterEditorPage.module.css' // ── Route pages ─────────────────────────────────────────────────────────────── @@ -29,9 +33,9 @@ export function CoasterEditorPage() { }, [id]) return ( - + - + ) } @@ -52,32 +56,30 @@ export function CoasterViewerPage() { }, [challengeId, coasterId]) return ( - + - + ) } -// ── Helpers ─────────────────────────────────────────────────────────────────── +// ── Source / layer IDs for editor-specific overlays ─────────────────────────── -/** Build a circle cross-section shape for PolylineVolumeGraphics. */ -function buildCircleShape(radius: number, segments = 8): Cesium.Cartesian2[] { - const pts: Cesium.Cartesian2[] = [] - for (let i = 0; i < segments; i++) { - const angle = (2 * Math.PI * i) / segments - pts.push(new Cesium.Cartesian2(Math.cos(angle) * radius, Math.sin(angle) * radius)) - } - return pts +const SRC_REGION = 'editor-region' +const LYR_REGION_F = 'editor-region-fill' +const LYR_REGION_L = 'editor-region-line' +const LYR_SIM_RAILS = 'sim-rails-3d' + +function emptyFC(): GeoJSON.FeatureCollection { return { type: 'FeatureCollection', features: [] } } +function setData(map: Map, src: string, data: GeoJSON.GeoJSON) { + (map.getSource(src) as GeoJSONSource | undefined)?.setData(data) } -const RAIL_SHAPE = buildCircleShape(0.075, 8) // 7.5 cm radius = 15 cm diameter - -// ── Inner scene (needs viewer context) ──────────────────────────────────────── +// ── Inner scene (needs map context) ────────────────────────────────────────── interface SceneProps { challengeId: string | undefined @@ -87,9 +89,11 @@ interface SceneProps { } function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadCoaster }: SceneProps) { - const viewer = useCesiumViewer() + const map = useMapLibreMap() const navigate = useNavigate() + const simRailLayerRef = useRef(null) + const authUser = useAuthStore(s => s.user) const currentUsername = authUser?.profile?.preferred_username as string | undefined @@ -104,148 +108,107 @@ function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadC const [coasterListKey, setCoasterListKey] = useState(0) const [isRideMode, setIsRideMode] = useState(false) const [rideCursor, setRideCursor] = useState(null) + const [pathHover, setPathHover] = useState<{ x: number; y: number; pct: number } | null>(null) - const path = useCoasterPath(viewer, showPath, showAnchors) - const accel = useAccelerationStrips(viewer, path.pathPts, path.mode === 'strip', showStrips) - const terrain = useTerrainCapture(viewer, simResult) + const path = useCoasterPath(map, showPath, showAnchors) + const accel = useAccelerationStrips(map, path.pathPts, path.mode === 'strip', showStrips) + const terrain = useTerrainCapture(map, simResult) - // Auto-load a preloaded coaster (viewer mode) + // Auto-load preloaded coaster (viewer mode) useEffect(() => { if (preloadCoaster) handleLoad(preloadCoaster) // eslint-disable-next-line react-hooks/exhaustive-deps }, [preloadCoaster]) - // Exit ride mode whenever the sim result changes; clear cursor on exit + // Exit ride mode when sim result changes useEffect(() => { setIsRideMode(false); setRideCursor(null) }, [simResult]) - // Suspend Cesium while Three.js ride view is active: stop the render loop AND - // disable the globe so Cesium's terrain web-workers stop posting decode results - // back to the main thread (those async completions caused periodic ~5 s spikes). - useEffect(() => { - viewer.useDefaultRenderLoop = !isRideMode - viewer.scene.globe.enabled = !isRideMode - }, [isRideMode, viewer]) - - // Refs for simulation result entities (cleared on each new run / unmount) - const simEntitiesRef = useRef([]) - const simPrimitivesRef = useRef([]) - - // ── Fly to challenge region ─────────────────────────────────────────────── + // ── Set up region + sim-rail sources ────────────────────────────────────── useEffect(() => { - if (!challenge) return - const coords = challenge.region.coordinates[0] - const positions = coords.map(([lon, lat]) => Cesium.Cartesian3.fromDegrees(lon, lat)) - const sphere = Cesium.BoundingSphere.fromPoints(positions, new Cesium.BoundingSphere()) - // Add 25 % padding so the polygon doesn't butt right up against the frame - // edges, then let Cesium compute the exact range (range = 0 = auto-fit). - const padded = new Cesium.BoundingSphere(sphere.center, sphere.radius * 1.25) + map.addSource(SRC_REGION, { type: 'geojson', data: emptyFC() }) + map.addLayer({ id: LYR_REGION_F, type: 'fill', source: SRC_REGION, + paint: { 'fill-color': '#06b6d4', 'fill-opacity': 0.04 } }) + map.addLayer({ id: LYR_REGION_L, type: 'line', source: SRC_REGION, + paint: { 'line-color': '#06b6d4', 'line-opacity': 0.45, 'line-width': 2 } }) - viewer.camera.flyToBoundingSphere(padded, { - offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-60), 0), - duration: 1.2, - }) - }, [challenge, viewer]) - - // ── Challenge boundary polygon ──────────────────────────────────────────── - - const regionEntityRef = useRef(null) - useEffect(() => { - if (regionEntityRef.current) { - viewer.entities.remove(regionEntityRef.current) - regionEntityRef.current = null - } - if (!challenge) return - - const coords = challenge.region.coordinates[0] - const positions = coords.map(([lon, lat]) => Cesium.Cartesian3.fromDegrees(lon, lat)) - - regionEntityRef.current = viewer.entities.add({ - polygon: { - hierarchy: new Cesium.PolygonHierarchy(positions), - material: Cesium.Color.CYAN.withAlpha(0.04), - outline: true, - outlineColor: Cesium.Color.CYAN.withAlpha(0.45), - outlineWidth: 2, - heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, - }, - }) + const handle = createCustom3DLayer(LYR_SIM_RAILS, map) + simRailLayerRef.current = handle return () => { - if (regionEntityRef.current && !viewer.isDestroyed()) { - viewer.entities.remove(regionEntityRef.current) - regionEntityRef.current = null - } + handle.destroy() + simRailLayerRef.current = null + safeRemoveLayers(map, [LYR_REGION_F, LYR_REGION_L], [SRC_REGION]) } - }, [challenge, viewer]) + }, [map]) - // ── Render simulation result rails ──────────────────────────────────────── + // ── Fly to challenge region ──────────────────────────────────────────────── useEffect(() => { - // Clear previous simulation entities - if (!viewer.isDestroyed()) { - simEntitiesRef.current.forEach(e => viewer.entities.remove(e)) - simPrimitivesRef.current.forEach(p => viewer.scene.primitives.remove(p)) + if (!challenge) return + + const coords = challenge.region.coordinates[0] as [number, number][] + const lons = coords.map(c => c[0]) + const lats = coords.map(c => c[1]) + map.fitBounds( + [[Math.min(...lons), Math.min(...lats)], [Math.max(...lons), Math.max(...lats)]], + { padding: 60, pitch: 55, bearing: 0, duration: 1200 }, + ) + + setData(map, SRC_REGION, { type: 'Feature', geometry: challenge.region, properties: {} }) + }, [challenge, map]) + + // ── Simulation result rails (custom 3D layer at absolute altitude) ──────── + + useEffect(() => { + if (!simResult) { + simRailLayerRef.current?.update([]) + return } - simEntitiesRef.current = [] - simPrimitivesRef.current = [] - if (!simResult) return - - const toC3 = ([lon, lat, alt]: [number, number, number]) => - Cesium.Cartesian3.fromDegrees(lon, lat, alt) - - const r1Pts = simResult.rail_1.map(toC3) - const r2Pts = simResult.rail_2.map(toC3) - - const rail1 = viewer.entities.add({ - polylineVolume: { - positions: r1Pts, - shape: RAIL_SHAPE, - material: Cesium.Color.fromCssColorString('#ef4444'), - cornerType: Cesium.CornerType.ROUNDED, + simRailLayerRef.current?.update([ + { + pts: simResult.rail_1 as [number, number, number][], + color: 0xef4444, + radiusMeters: 0.12, }, - }) - const rail2 = viewer.entities.add({ - polylineVolume: { - positions: r2Pts, - shape: RAIL_SHAPE, - material: Cesium.Color.fromCssColorString('#ef4444'), - cornerType: Cesium.CornerType.ROUNDED, + { + pts: simResult.rail_2 as [number, number, number][], + color: 0xef4444, + radiusMeters: 0.12, }, - }) - simEntitiesRef.current = [rail1, rail2] + ]) + }, [simResult]) - // Load GLB model if available - if (simResult.model_url) { - const [lon0, lat0, alt0] = simResult.origin - const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame( - Cesium.Cartesian3.fromDegrees(lon0, lat0, alt0), + // ── Path hover tooltip ──────────────────────────────────────────────────── + + useEffect(() => { + if (path.pathPts.length < 2) { setPathHover(null); return } + + const arcs = computeArcLengths(path.pathPts) + + function onMove(e: { point: { x: number; y: number }; lngLat: { lng: number; lat: number } }) { + const { x, y } = e.point + const hits = map.queryRenderedFeatures( + [{ x: x - 4, y: y - 4 }, { x: x + 4, y: y + 4 }], + { layers: ['coaster-path-lyr'] }, ) - Cesium.Model.fromGltfAsync({ url: simResult.model_url, modelMatrix }) - .then(model => { - if (viewer.isDestroyed()) return - viewer.scene.primitives.add(model) - simPrimitivesRef.current.push(model as unknown as Cesium.Primitive) - }) - .catch(err => console.error('GLB model load failed:', err)) + if (hits.length === 0) { setPathHover(null); return } + const { frac } = snapToPath([e.lngLat.lng, e.lngLat.lat], path.pathPts, arcs) + setPathHover({ x, y, pct: frac * 100 }) } - return () => { - if (viewer.isDestroyed()) return - simEntitiesRef.current.forEach(e => viewer.entities.remove(e)) - simPrimitivesRef.current.forEach(p => viewer.scene.primitives.remove(p)) - simEntitiesRef.current = [] - simPrimitivesRef.current = [] - } - }, [simResult, viewer]) + map.on('mousemove', onMove) + return () => { map.off('mousemove', onMove) } + }, [map, path.pathPts]) // ── Load / Save handlers ───────────────────────────────────────────────── function handleLoad(coaster: SavedCoaster) { - const anchorPoints = coaster.anchors.map(a => ({ + const anchorPoints: AnchorPoint[] = coaster.anchors.map(a => ({ id: a.id, - position: Cesium.Cartesian3.fromDegrees(a.lon, a.lat, a.terrainAlt), + lngLat: [a.lon, a.lat] as [number, number], + terrainHeight: a.terrainAlt, heightOffset: a.heightOffset, })) path.loadAnchors(anchorPoints) @@ -256,16 +219,13 @@ function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadC async function handleSave() { if (!challengeId || path.anchors.length < 2) return - const storedAnchors = path.anchors.map(a => { - const carto = Cesium.Cartographic.fromCartesian(effectivePosition(a)) - return { - id: a.id, - lon: Cesium.Math.toDegrees(carto.longitude), - lat: Cesium.Math.toDegrees(carto.latitude), - terrainAlt: carto.height, - heightOffset: a.heightOffset, - } - }) + const storedAnchors = path.anchors.map(a => ({ + id: a.id, + lon: a.lngLat[0], + lat: a.lngLat[1], + terrainAlt: a.terrainHeight, + heightOffset: a.heightOffset, + })) await saveCoaster(challengeId, { name: coasterName.trim(), anchors: storedAnchors, @@ -282,19 +242,7 @@ function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadC setSimError(null) try { - // Send only the anchor control points (not the pre-sampled Bezier - // polyline). The backend fits its own smooth B-spline with analytical - // derivatives, which avoids the curvature spikes that arise from - // finite-differencing a piecewise-linear polyline approximation. - const geoPath = path.anchors.map(anchor => { - const pos = effectivePosition(anchor) - const carto = Cesium.Cartographic.fromCartesian(pos) - return [ - Cesium.Math.toDegrees(carto.longitude), - Cesium.Math.toDegrees(carto.latitude), - carto.height, - ] as [number, number, number] - }) + const geoPath = path.anchors.map(anchor => effectiveLngLatAlt(anchor)) const result = await simulateCoaster({ path: geoPath, @@ -322,7 +270,7 @@ function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadC return ( <> - {/* ── Three.js ride renderer (fullscreen, mounts over Cesium) ─────── */} + {/* ── Three.js ride renderer (fullscreen, mounts over map) ──────────── */} {isRideMode && simResult && terrain.captureData && ( {readonly ? 'Viewer' : 'Coaster Editor'} + {challengeId && ( + + )}
{/* ── Mode toolbar (hidden while riding) ──────────────────────────── */} @@ -434,9 +391,7 @@ function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadC )} {/* ── Simulation error ─────────────────────────────────────────────── */} - {simError && ( -
{simError}
- )} + {simError &&
{simError}
} {/* ── Diagnostics strip ────────────────────────────────────────────── */} {diag && !simError && ( @@ -507,25 +462,25 @@ function CoasterEditorScene({ challengeId, challenge, readonly = false, preloadC /> )} - {/* ── Right panel column (acceleration strips + coaster list) ────── */} - {!isRideMode && ( + {/* ── Right panel column (acceleration strips) ─────────────────────── */} + {!isRideMode && accel.strips.length > 0 && (
- {accel.strips.length > 0 && ( - - )} - {challengeId && ( - - )} + +
+ )} + + {/* ── Path hover tooltip ──────────────────────────────────────────── */} + {!isRideMode && pathHover && ( +
+ {pathHover.pct.toFixed(1)}%
)} diff --git a/web/src/coaster/CoasterListPanel.module.css b/web/src/coaster/CoasterListPanel.module.css index a82e96d..17b7016 100644 --- a/web/src/coaster/CoasterListPanel.module.css +++ b/web/src/coaster/CoasterListPanel.module.css @@ -118,3 +118,45 @@ color: rgba(255, 255, 255, 0.25); padding: 6px 0 2px; } + +/* ── Top-bar menu mode ───────────────────────────────────────────────────────── */ + +.menuWrapper { + position: relative; + flex-shrink: 0; +} + +.menuToggle { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 8px; + color: rgba(255, 255, 255, 0.75); + font-size: 13px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s; +} +.menuToggle:hover { + background: rgba(255, 255, 255, 0.12); + color: #fff; +} + +.menuDropdown { + position: absolute; + top: calc(100% + 6px); + right: 0; + z-index: 400; + width: 260px; + background: rgba(8, 8, 12, 0.92); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 12px; + padding: 6px 12px 10px; + overflow: hidden; +} diff --git a/web/src/coaster/CoasterListPanel.tsx b/web/src/coaster/CoasterListPanel.tsx index bf4a3e4..d0b1b21 100644 --- a/web/src/coaster/CoasterListPanel.tsx +++ b/web/src/coaster/CoasterListPanel.tsx @@ -8,9 +8,11 @@ interface Props { currentUsername: string | undefined onLoad: (coaster: SavedCoaster) => void refreshKey: number + /** Render as a top-bar dropdown instead of a sidebar panel */ + menuMode?: boolean } -export function CoasterListPanel({ challengeId, currentUsername, onLoad, refreshKey }: Props) { +export function CoasterListPanel({ challengeId, currentUsername, onLoad, refreshKey, menuMode }: Props) { const [open, setOpen] = useState(false) const [coasters, setCoasters] = useState([]) @@ -23,6 +25,46 @@ export function CoasterListPanel({ challengeId, currentUsername, onLoad, refresh setCoasters(prev => prev.filter(c => c.id !== id)) } + if (menuMode) { + return ( +
+ + {open && ( +
+ {coasters.length === 0 ? ( +

No coasters yet.

+ ) : ( + coasters.map(c => { + const isOwn = c.creator_username === currentUsername + return ( +
+
+ {c.name || 'Unnamed coaster'} + + @{c.creator_username} + +
+ + {isOwn && ( + + )} +
+ ) + }) + )} +
+ )} +
+ ) + } + return (