Commit 1d3a78f
feat(flight-simulator): interactive free-flight camera over terrain and 3D layers (#1455)
* feat(flight-simulator): interactive free-flight camera over terrain (#1454)
Every camera path in GeoLibre was scripted and discrete — `flyTo` from Set
View, story-map chapters, the keyframe Camera Tour recorder. This adds the
continuous one: an aircraft state integrated each animation frame, with the
MapLibre camera placed to match, so the user steers instead of declaring a
destination.
MapLibre has no `setFreeCameraOptions` (a Mapbox API added after the fork).
Its equivalent is `calculateCameraOptionsFromCameraLngLatAltRotation()`,
which converts camera lng/lat/altitude plus orientation into the
`CameraOptions` that `jumpTo` accepts. That conversion divides by
`cos(pitch)`, so a pitch of exactly 90° (level with the horizon) sends the
derived center and zoom to infinity — the camera is capped just short of it
and level flight looks slightly downward.
- `flight-simulator-physics.ts` is map-free: a coordinated-turn model (bank
drives turn rate, nose attitude drives climb rate, throttle drives
airspeed), unit-tested without a MapLibre instance or a GPU.
- `flight-simulator.ts` owns the map while flying: the rAF loop, held-key
tracking, terrain collision via `queryTerrainElevation`, and restoring the
pitch ceiling, center clamping, and all eight interaction handlers on exit.
MapLibre's own `keyboard` handler must be suspended or it double-handles
the arrow keys.
- Camera jumps carry a `flightCameraToken` so the viewport history, the
store's view sync, and collaboration presence skip them — following the
story presenter's existing `storyCameraToken` precedent. Without it a
flight buries the back/forward stack under ~60 entries a second and
overwrites the project's saved view.
- HUD with airspeed, altitude MSL/AGL, heading, an artificial horizon, and a
terrain warning; imperial or metric. Settings persist with the project,
but never in a flying state — seizing the camera and keyboard is an
explicit user action.
Verified against real terrain over Mount Rainier: the DEM reads 4,379 m at
the summit against a true 4,392 m (0.3%), airspeed settles at exactly the
modelled 162 m/s at 60% throttle, bank pins at the 70° limit and self-levels
at 40°/s, and the terrain floor holds the aircraft at the configured 25 m AGL.
Translated into all 15 non-English catalogs.
* Address CodeRabbit review feedback
- flight-simulator: bail out of `tick()` on `!running` and re-check before
re-arming the rAF loop. `publishHud` notifies subscribers synchronously, so a
`stop()` re-entered from a listener cancels a `rafId` that `tick()` has
already nulled — the cancel was a no-op and the loop kept driving the camera
after the map had been handed back.
- flight-simulator: reset the throttle to the cruise default in `start()`. Every
other per-flight field was re-seeded, so a flight that ended at full power
restarted showing full throttle while the airspeed had been re-seeded to idle.
- flight-simulator: `clampNumber` now coerces only numbers and non-empty numeric
strings. `Number(null)`, `Number("")` and `Number([])` are all 0 — finite, so
those persisted values clamped to the range minimum instead of falling back to
the documented default.
- flight-simulator: `reattachFlightSimulator` is a no-op when the map instance is
unchanged. Rebinding tears the engine down and hands the map back, ending a
live flight; reattach exists to bind to a *new* map, so keying on map identity
is the precise condition. (CodeRabbit's premise that remote collaboration
drives this was wrong — collab snapshots call `applyProjectToStore`, which does
not bump `projectGeneration`; only `newProject`/`loadProject` do. The guard is
still worth having for those.)
- useViewportHistory: clear the pending restore count when a flight `moveend`
arrives, so a restore ease that a flight frame interrupted cannot leave a
counter behind to swallow the next ordinary `moveend`.
Adds 5 tests. The three engine cases were each checked against a reverted fix to
confirm they actually fail without it. Plugin line coverage 91.9% -> 97.3%.
* fix(flight-simulator): improve terrain handling and stability
* style: auto-format (ruff + oxfmt) [pre-commit.ci]
* test(flight-simulator): cover missing map reattachment
* feat(flight-simulator): enable terrain during flight
* style: auto-format (ruff + oxfmt) [pre-commit.ci]
* Address CodeRabbit review feedback
- Compare invalid-terrain output by value to catch input mutation.
- Assert replacement-map teardown begins from an active flight.
* Address CodeRabbit review feedback
- preserve pre-existing custom terrain during flight mode
- add regression coverage for custom terrain sources
* Align flight controls with Google Earth
- make Page Up and Page Down respond to taps and holds
- match Google Earth arrow-key pitch direction
- add regression coverage for throttle and pitch controls
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>1 parent 9257b8a commit 1d3a78f
32 files changed
Lines changed: 3244 additions & 4 deletions
File tree
- apps/geolibre-desktop/src
- components
- layout
- toolbar
- panels
- hooks
- i18n/locales
- lib
- packages
- map/src
- plugins/src
- plugins
- tests
Lines changed: 11 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
125 | 126 | | |
126 | 127 | | |
127 | 128 | | |
| 129 | + | |
128 | 130 | | |
129 | 131 | | |
130 | 132 | | |
| |||
1056 | 1058 | | |
1057 | 1059 | | |
1058 | 1060 | | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
1059 | 1064 | | |
1060 | 1065 | | |
1061 | 1066 | | |
| |||
2046 | 2051 | | |
2047 | 2052 | | |
2048 | 2053 | | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
2049 | 2060 | | |
2050 | 2061 | | |
2051 | 2062 | | |
| |||
Lines changed: 10 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
| 148 | + | |
148 | 149 | | |
149 | 150 | | |
150 | 151 | | |
| |||
227 | 228 | | |
228 | 229 | | |
229 | 230 | | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
230 | 240 | | |
231 | 241 | | |
232 | 242 | | |
| |||
Lines changed: 325 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
0 commit comments