react-native-cesium 1.2.18 → 1.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +11 -8
  2. package/android/CMakeLists.txt +1 -0
  3. package/android/build.gradle +1 -1
  4. package/android/gradle.properties +4 -4
  5. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +44 -18
  6. package/android/src/main/cpp/CesiumBridgeAndroid.h +9 -0
  7. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +3 -0
  8. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +9 -0
  9. package/cpp/engine/CameraIntegrator.cpp +82 -47
  10. package/cpp/engine/CameraIntegrator.hpp +27 -8
  11. package/cpp/engine/CesiumEngine.cpp +183 -312
  12. package/cpp/engine/CesiumEngine.hpp +41 -28
  13. package/cpp/engine/EngineTunables.hpp +6 -5
  14. package/cpp/engine/GeoidConverter.cpp +32 -11
  15. package/cpp/engine/GeoidConverter.hpp +41 -0
  16. package/cpp/engine/GltfToMesh.cpp +14 -5
  17. package/cpp/engine/ResourcePreparer.cpp +3 -3
  18. package/cpp/engine/ResourcePreparer.hpp +1 -1
  19. package/cpp/engine/TerrainFloor.hpp +70 -0
  20. package/cpp/engine/TerrainHeight.hpp +113 -0
  21. package/cpp/engine/test/CameraIntegratorTest.cpp +264 -0
  22. package/cpp/engine/test/GeoidConverterTest.cpp +77 -0
  23. package/cpp/engine/test/build_and_run.sh +20 -6
  24. package/ios/CesiumBridge.h +5 -0
  25. package/ios/CesiumBridge.mm +61 -32
  26. package/ios/HybridCesiumView.swift +11 -0
  27. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +34 -9
  28. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  29. package/nitrogen/generated/android/c++/JCameraState.hpp +1 -1
  30. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +1 -1
  31. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +9 -0
  32. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +2 -0
  33. package/nitrogen/generated/android/c++/JQuaternion.hpp +1 -1
  34. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +182 -131
  35. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.hpp +6 -1
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +1 -1
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +1 -1
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CameraState.kt +0 -2
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CesiumMetrics.kt +0 -2
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +8 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +1 -1
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewManager.kt +26 -10
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewStateUpdater.kt +2 -2
  44. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +15 -0
  45. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +7 -0
  46. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +217 -161
  47. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +1 -0
  48. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +24 -0
  49. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +2 -0
  50. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +2 -0
  51. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +36 -335
  52. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +110 -77
  53. package/nitrogen/generated/shared/json/CesiumViewConfig.json +1 -0
  54. package/package.json +12 -12
  55. package/scripts/cesium/config.mjs +1 -1
  56. package/scripts/cesium/vcpkg-triplets/arm64-ios-simulator.cmake +3 -0
  57. package/scripts/cesium/vcpkg-triplets/arm64-ios.cmake +4 -0
  58. package/src/specs/cesium-view.nitro.ts +34 -9
package/README.md CHANGED
@@ -184,7 +184,7 @@ npm run build --prefix node_modules/react-native-cesium
184
184
 
185
185
  **Yarn/npm workspaces:** if `react-native-cesium` is a workspace package in your monorepo, use your tool’s workspace runner, e.g. `yarn workspace react-native-cesium run update` (exact syntax depends on your workspace layout).
186
186
 
187
- - `update` checks out **Cesium Native `v0.59.0`** into `vendor/cesium-native` under the package (next to its other files; typically ignored in app repos).
187
+ - `update` checks out **Cesium Native `v0.65.0`** into `vendor/cesium-native` under the package (next to its other files; typically ignored in app repos).
188
188
  - `build` runs `scripts/cesium/build.mjs` (**CMake**, **vcpkg** under `vendor/vcpkg` unless **`VCPKG_ROOT`** is set, **Ninja** on macOS) and writes **`vendor/ios/CesiumNative.xcframework`** and/or **`vendor/android`** depending on **`CESIUM_BUILD_ONLY`**.
189
189
 
190
190
  Then install pods (iOS):
@@ -294,6 +294,7 @@ When you pass a callback ref to `CesiumView`, wrap it with `callback(...)` from
294
294
 
295
295
  | Prop | Type | Default | Description |
296
296
  | --- | --- | --- | --- |
297
+ | `altitudeDatum` | `"msl"` \| `"ellipsoid"` | `"msl"` | Vertical datum of every public altitude: `initialCamera`, `setAltitude`, `teleport`, and the altitude returned by `getActualCamera`, `getDemandCamera`, and `onActualCamera`. `"msl"` is metres above mean sea level. `"ellipsoid"` is WGS84 ellipsoidal height (Android `Location.getAltitude()`). Ellipsoidal heights are converted to mean sea level with EGM96 before the camera integrator, and converted back on read. Switching the prop does not move the camera: later reads use the new unit, and the next altitude or position write is interpreted in that unit. Leave this at `"msl"` for gesture and scripted altitudes. Use `"ellipsoid"` for a GPS ellipsoidal height, and call `setPosition` and `setAltitude` one after the other on the same thread. |
297
298
  | `onMetrics` | `(metrics: CesiumMetrics) => void` | `undefined` | Receives throttled runtime stats and credits text. |
298
299
 
299
300
  **Optional performance / quality knobs**
@@ -348,7 +349,7 @@ type CameraState = {
348
349
  | --- | --- |------------------------------|-------------------------------------------------------------------------------------------------------|
349
350
  | `latitude` | `number` | `-90..90` | Camera latitude in degrees. |
350
351
  | `longitude` | `number` | `-180..180` | Camera longitude in degrees. |
351
- | `altitude` | `number` | Meters above sea level (MSL) | Camera height in meters. |
352
+ | `altitude` | `number` | Metres | Camera height. Mean sea level unless `altitudeDatum` is `"ellipsoid"`, in which case this is WGS84 ellipsoidal height. Writes and reads use the same unit. |
352
353
  | `heading` | `number` | Degrees | Compass direction the camera faces. Example: `0` faces north, `90` faces east. |
353
354
  | `pitch` | `number` | Degrees | Tilt angle. Negative values look downward toward terrain; positive values tilt up toward horizon/sky. |
354
355
  | `roll` | `number` | Degrees | Bank/rotation around forward axis. Positive right bank, negative left bank. |
@@ -385,15 +386,15 @@ Each axis (position, altitude, heading, attitude, viewCorrection, verticalFov) h
385
386
 
386
387
  | Method | Signature | Description |
387
388
  | --- | --- | --- |
388
- | `setPosition` | `(latitude: number, longitude: number) => void` | New geographic position **demand** (degrees). Each call stamps the measurement with the native steady-clock time of arrival and runs one α-β update against the predicted state at that instant. Latitude is clamped to ±90 on extrapolation; longitude is continuous (no wrap normalisation) so shortest-arc residual maths stays correct across the antimeridian. |
389
- | `setAltitude` | `(altitudeMeters: number) => void` | New altitude demand (metres MSL). |
389
+ | `setPosition` | `(latitude: number, longitude: number) => void` | New geographic position **demand** (degrees). Each call stamps the measurement with the native steady-clock time of arrival and runs one α-β update against the predicted state at that instant. Latitude is clamped to ±90 on extrapolation; longitude is continuous (no wrap normalisation) so shortest-arc residual maths stays correct across the antimeridian. When `altitudeDatum` is `"ellipsoid"`, a horizontal move keeps the demanded ellipsoidal height and recomputes mean sea level at the new point. `"msl"` leaves altitude unchanged. |
390
+ | `setAltitude` | `(altitudeMeters: number) => void` | New altitude demand, in metres. Mean sea level unless `altitudeDatum` is `"ellipsoid"`, in which case it is WGS84 height and is converted to mean sea level before the integrator. A later read reports the same sample back in this unit. |
390
391
  | `setHeading` | `(headingDeg: number) => void` | New heading demand (degrees; `0` = north, increasing clockwise). The integrator computes the shortest-arc residual, so feeding it `359 → 1` rotates 2° clockwise, not 358° counter-clockwise. |
391
392
  | `setAttitude` | `(pitchDeg: number, rollDeg: number) => void` | New pitch and roll demand (degrees). Bundled because almost every IMU emits them together — but you can pass the previous value for either axis if only one of them has changed. |
392
393
  | `setViewCorrection` | `(q: Quaternion) => void` | New camera-space rotation applied **after** HPR. SLERPed toward the latest demand each frame. Use for boresight calibration, screen-fixed HUD alignment, etc. Non-unit values are normalised on the native side. |
393
394
  | `setVerticalFov` | `(deg: number) => void` | New vertical FOV demand (degrees). Clamped to `20..100` on the native side. |
394
- | `teleport` | `(camera: CameraState) => void` | Hard scene jump — atomically resets every DoF (value, velocity, demand) to `camera`. Bypasses the integrator entirely. Use for `Fly to coordinate` actions or seeding from saved state. |
395
- | `getActualCamera` | `() => Promise<CameraState>` | Most recently rendered camera (post-integration). This is what the user is currently looking at and what a HUD overlay should mirror. Differs from `getDemandCamera()` while a glide is in progress or when the terrain-floor clamp has raised the altitude. |
396
- | `getDemandCamera` | `() => Promise<CameraState>` | What the consumer last asked for (per-DoF demand). Useful for diagnostics, or for showing the requested camera alongside the rendered one. |
395
+ | `teleport` | `(camera: CameraState) => void` | Hard scene jump — atomically resets every DoF (value, velocity, demand) to `camera`. Bypasses the integrator entirely. Use for `Fly to coordinate` actions or seeding from saved state. `camera.altitude` uses the unit selected by `altitudeDatum`. |
396
+ | `getActualCamera` | `() => Promise<CameraState>` | Most recently rendered camera (post-integration). This is what the user is currently looking at and what a HUD overlay should mirror. Differs from `getDemandCamera()` while a glide is in progress or when the terrain-floor clamp has raised the altitude. Altitude is in the unit selected by `altitudeDatum`. |
397
+ | `getDemandCamera` | `() => Promise<CameraState>` | What the consumer last asked for (per-DoF demand). Useful for diagnostics, or for showing the requested camera alongside the rendered one. Altitude is in the unit selected by `altitudeDatum`. |
397
398
  | `getViewCorrection` | `() => Promise<Quaternion>` | Current view-correction quaternion (smoothed toward the latest demand). Identity (`w=1, x=y=z=0`) if you have never called `setViewCorrection`. |
398
399
 
399
400
  #### Threading: setters vs getters
@@ -415,7 +416,7 @@ Avoid assuming **main-thread-only** vs **JS-thread-only** labels beyond the abov
415
416
 
416
417
  The integrator tracks two camera states simultaneously:
417
418
 
418
- - **Demand** — what the consumer asked for, exactly as it was passed to the setters. `getDemandCamera()` returns this.
419
+ - **Demand** — what the consumer asked for. `getDemandCamera()` returns this, with altitude in the unit selected by `altitudeDatum`.
419
420
  - **Actual** — what the integrator extrapolated to and what was passed to the GPU on the last frame. `getActualCamera()` returns this.
420
421
 
421
422
  For a steady GPS feed with no terrain clamp the two values converge within a few frames. They diverge when:
@@ -461,6 +462,8 @@ imuSubscription.on('attitude', (att) => {
461
462
  })
462
463
  ```
463
464
 
465
+ `fix.altitudeMsl` matches the default `altitudeDatum="msl"`. Android `Location.getAltitude()` is a WGS84 ellipsoidal height: set `altitudeDatum="ellipsoid"` and pass that value to `setAltitude` instead. Call `setPosition` and `setAltitude` one after the other on the same thread so the geoid is applied at that fix.
466
+
464
467
  Each setter is synchronous, takes the integrator's mutex briefly, and is safe to call at any rate from any thread.
465
468
 
466
469
  ### `CesiumMetrics`
@@ -151,6 +151,7 @@ target_link_libraries(
151
151
  CesiumGltf
152
152
  CesiumGltfReader
153
153
  CesiumGltfContent
154
+ CesiumImage
154
155
  CesiumGeospatial
155
156
  CesiumGeometry
156
157
  CesiumAsync
@@ -5,7 +5,7 @@ buildscript {
5
5
  }
6
6
 
7
7
  dependencies {
8
- classpath "com.android.tools.build:gradle:8.8.0"
8
+ classpath "com.android.tools.build:gradle:9.2.1"
9
9
  }
10
10
  }
11
11
 
@@ -1,5 +1,5 @@
1
- ReactNativeCesium_kotlinVersion=2.1.20
2
- ReactNativeCesium_minSdkVersion=23
3
- ReactNativeCesium_targetSdkVersion=35
4
- ReactNativeCesium_compileSdkVersion=34
1
+ ReactNativeCesium_kotlinVersion=2.2.0
2
+ ReactNativeCesium_minSdkVersion=24
3
+ ReactNativeCesium_targetSdkVersion=36
4
+ ReactNativeCesium_compileSdkVersion=37
5
5
  ReactNativeCesium_ndkVersion=27.1.12297006
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include "engine/EngineTunables.hpp"
4
4
  #include "engine/GeoidConverter.hpp"
5
+ #include "engine/TerrainFloor.hpp"
5
6
  #include "vulkan/VulkanBackend.h"
6
7
 
7
8
  #include <android/native_window_jni.h>
@@ -149,13 +150,34 @@ void CesiumBridgeAndroid::updateImageryAssetId(int64_t assetId) {
149
150
 
150
151
  void CesiumBridgeAndroid::setPosition(double lat, double lon) {
151
152
  if (!initialized_ || !integrator_) return;
153
+ const bool ellipsoid =
154
+ altitudeIsEllipsoid_.load(std::memory_order_acquire);
155
+ double adjustedMsl = 0.0;
156
+ bool adjustAltitude = false;
157
+ if (ellipsoid) {
158
+ const auto demand = integrator_->getDemand();
159
+ adjustedMsl = reactnativecesium::mslAfterHorizontalMove(
160
+ demand.latitude, demand.longitude, demand.altitude, lat, lon);
161
+ adjustAltitude = std::abs(adjustedMsl - demand.altitude) >
162
+ reactnativecesium::kHorizontalMoveMslEpsilonMeters;
163
+ }
152
164
  integrator_->setPosition(lat, lon);
165
+ if (adjustAltitude) {
166
+ integrator_->setAltitude(adjustedMsl);
167
+ }
153
168
  forceRender_.store(true, std::memory_order_release);
154
169
  }
155
170
 
171
+ void CesiumBridgeAndroid::setAltitudeDatumEllipsoid(bool ellipsoid) {
172
+ altitudeIsEllipsoid_.store(ellipsoid, std::memory_order_release);
173
+ }
174
+
156
175
  void CesiumBridgeAndroid::setAltitude(double alt) {
157
176
  if (!initialized_ || !integrator_) return;
158
- integrator_->setAltitude(alt);
177
+ const auto demand = integrator_->getDemand();
178
+ integrator_->setAltitude(reactnativecesium::demandAltitudeToMslMeters(
179
+ demand.latitude, demand.longitude, alt,
180
+ altitudeIsEllipsoid_.load(std::memory_order_acquire)));
159
181
  forceRender_.store(true, std::memory_order_release);
160
182
  }
161
183
 
@@ -184,13 +206,15 @@ void CesiumBridgeAndroid::teleport(double lat, double lon, double alt,
184
206
  reactnativecesium::CameraParams p;
185
207
  p.latitude = lat;
186
208
  p.longitude = lon;
187
- p.altitude = alt;
209
+ p.altitude = reactnativecesium::demandAltitudeToMslMeters(
210
+ lat, lon, alt, altitudeIsEllipsoid_.load(std::memory_order_acquire));
188
211
  p.heading = heading;
189
212
  p.pitch = pitch;
190
213
  p.roll = roll;
191
214
  p.verticalFov = vfov;
192
215
  p.viewCorrection = integrator_->getActual().viewCorrection;
193
216
  integrator_->teleport(p);
217
+ reactnativecesium::noteTerrainSlew(*integrator_, terrainLead_);
194
218
  engine_->camera().setParams(p);
195
219
  forceRender_.store(true, std::memory_order_release);
196
220
  }
@@ -309,20 +333,9 @@ void CesiumBridgeAndroid::renderFrame(double nowSeconds) {
309
333
  // The integrator owns its own clock — see CameraIntegrator::step().
310
334
  // `nowSeconds` is still used below for the metrics-only dt EMA.
311
335
  auto actual = integrator_->step();
312
-
313
- // ── Terrain floor clamp ───────────────────────────────────────────────
314
- const float minAbove = engine_->getConfig().minAltitudeAboveTerrain;
315
- if (minAbove > 0.0f && engine_->terrainFloorKnown()) {
316
- const double geoidN =
317
- reactnativecesium::mslToEllipsoidMeters(actual.latitude, actual.longitude, 0.0);
318
- const double floorMsl =
319
- static_cast<double>(engine_->terrainFloorEllipsoidMeters()) - geoidN;
320
- const double minMsl = floorMsl + static_cast<double>(minAbove);
321
- if (actual.altitude < minMsl) {
322
- integrator_->clampActualAltitude(minMsl);
323
- actual.altitude = minMsl;
324
- }
325
- }
336
+ actual = reactnativecesium::applyTerrainFloor(
337
+ *engine_, *integrator_, actual, terrainLead_,
338
+ static_cast<double>(engine_->getConfig().minAltitudeAboveTerrain));
326
339
 
327
340
  engine_->camera().setParams(actual);
328
341
 
@@ -348,7 +361,11 @@ double CesiumBridgeAndroid::readActualLongitude() {
348
361
  return integrator_ ? integrator_->getActual().longitude : 0.0;
349
362
  }
350
363
  double CesiumBridgeAndroid::readActualAltitude() {
351
- return integrator_ ? integrator_->getActual().altitude : 0.0;
364
+ if (!integrator_) return 0.0;
365
+ const auto actual = integrator_->getActual();
366
+ return reactnativecesium::mslToDemandAltitudeMeters(
367
+ actual.latitude, actual.longitude, actual.altitude,
368
+ altitudeIsEllipsoid_.load(std::memory_order_acquire));
352
369
  }
353
370
  double CesiumBridgeAndroid::readActualHeading() {
354
371
  return integrator_ ? integrator_->getActual().heading : 0.0;
@@ -372,7 +389,11 @@ double CesiumBridgeAndroid::readDemandLongitude() {
372
389
  return integrator_ ? integrator_->getDemand().longitude : 0.0;
373
390
  }
374
391
  double CesiumBridgeAndroid::readDemandAltitude() {
375
- return integrator_ ? integrator_->getDemand().altitude : 0.0;
392
+ if (!integrator_) return 0.0;
393
+ const auto demand = integrator_->getDemand();
394
+ return reactnativecesium::mslToDemandAltitudeMeters(
395
+ demand.latitude, demand.longitude, demand.altitude,
396
+ altitudeIsEllipsoid_.load(std::memory_order_acquire));
376
397
  }
377
398
  double CesiumBridgeAndroid::readDemandHeading() {
378
399
  return integrator_ ? integrator_->getDemand().heading : 0.0;
@@ -463,6 +484,11 @@ Java_com_margelo_nitro_reactnativecesium_CesiumBridgeJNI_nativeSetPosition(
463
484
  getBridge(ptr)->setPosition(lat, lon);
464
485
  }
465
486
  JNIEXPORT void JNICALL
487
+ Java_com_margelo_nitro_reactnativecesium_CesiumBridgeJNI_nativeSetAltitudeDatumEllipsoid(
488
+ JNIEnv*, jobject, jlong ptr, jboolean ellipsoid) {
489
+ getBridge(ptr)->setAltitudeDatumEllipsoid(ellipsoid == JNI_TRUE);
490
+ }
491
+ JNIEXPORT void JNICALL
466
492
  Java_com_margelo_nitro_reactnativecesium_CesiumBridgeJNI_nativeSetAltitude(
467
493
  JNIEnv*, jobject, jlong ptr, jdouble alt) {
468
494
  getBridge(ptr)->setAltitude(alt);
@@ -8,6 +8,7 @@
8
8
  #include "engine/CameraIntegrator.hpp"
9
9
  #include "engine/CesiumEngine.hpp"
10
10
  #include "engine/MetricsAggregator.hpp"
11
+ #include "engine/TerrainFloor.hpp"
11
12
 
12
13
  struct ANativeWindow;
13
14
 
@@ -47,6 +48,10 @@ struct CesiumBridgeAndroid {
47
48
 
48
49
  // ── Per-DoF camera demand setters ──────────────────────────────────────
49
50
  void setPosition(double lat, double lon);
51
+ // When true, altitudes passed to setAltitude/teleport are WGS84 ellipsoidal
52
+ // heights. They are converted to MSL before the integrator, and altitude
53
+ // readback is converted back. A horizontal move keeps that ellipsoidal height.
54
+ void setAltitudeDatumEllipsoid(bool ellipsoid);
50
55
  void setAltitude(double alt);
51
56
  void setHeading(double headingDeg);
52
57
  void setAttitude(double pitchDeg, double rollDeg);
@@ -137,6 +142,9 @@ private:
137
142
  int viewportWidth_ = 0;
138
143
  int viewportHeight_ = 0;
139
144
  bool initialized_ = false;
145
+ // Written from the render thread when the prop changes; read from the
146
+ // caller thread inside setAltitude / setPosition / teleport / readback.
147
+ std::atomic<bool> altitudeIsEllipsoid_{false};
140
148
 
141
149
  // Force-render flag — set on every demand change so the render loop knows
142
150
  // to dispatch at least one frame.
@@ -144,5 +152,6 @@ private:
144
152
 
145
153
  reactnativecesium::EngineConfig config_;
146
154
  reactnativecesium::MetricsAggregator metrics_;
155
+ reactnativecesium::TerrainLeadState terrainLead_{};
147
156
  double lastTickSeconds_ = 0.0;
148
157
  };
@@ -27,6 +27,8 @@ class CesiumBridgeJNI {
27
27
 
28
28
  // ── Per-DoF camera setters ───────────────────────────────────────────────
29
29
  fun setPosition(lat: Double, lon: Double) = nativeSetPosition(nativePtr, lat, lon)
30
+ fun setAltitudeDatumEllipsoid(ellipsoid: Boolean) =
31
+ nativeSetAltitudeDatumEllipsoid(nativePtr, ellipsoid)
30
32
  fun setAltitude(alt: Double) = nativeSetAltitude(nativePtr, alt)
31
33
  fun setHeading(headingDeg: Double) = nativeSetHeading(nativePtr, headingDeg)
32
34
  fun setAttitude(pitchDeg: Double, rollDeg: Double) = nativeSetAttitude(nativePtr, pitchDeg, rollDeg)
@@ -107,6 +109,7 @@ class CesiumBridgeJNI {
107
109
  private external fun nativeUpdateImageryAssetId(ptr: Long, assetId: Long)
108
110
 
109
111
  private external fun nativeSetPosition(ptr: Long, lat: Double, lon: Double)
112
+ private external fun nativeSetAltitudeDatumEllipsoid(ptr: Long, ellipsoid: Boolean)
110
113
  private external fun nativeSetAltitude(ptr: Long, alt: Double)
111
114
  private external fun nativeSetHeading(ptr: Long, headingDeg: Double)
112
115
  private external fun nativeSetAttitude(ptr: Long, pitchDeg: Double, rollDeg: Double)
@@ -105,6 +105,14 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
105
105
  override var ionImageryAssetId: Double = 1.0
106
106
  set(value) { field = value; postToRender { bridge?.updateImageryAssetId(value.toLong()) } }
107
107
 
108
+ /** `"msl"` (default) or `"ellipsoid"`. Ellipsoid heights are converted to MSL. */
109
+ override var altitudeDatum: String? = "msl"
110
+ set(value) {
111
+ field = value
112
+ val ellipsoid = value == "ellipsoid"
113
+ postToRender { bridge?.setAltitudeDatumEllipsoid(ellipsoid) }
114
+ }
115
+
108
116
  // ── Optional perf / quality knobs ────────────────────────────────────────
109
117
  override var maximumCachedMiB: Double? = null
110
118
  set(value) { field = value; if (value != null) postToRender { bridge?.setMaximumCachedMiB(value.toInt()) } }
@@ -519,6 +527,7 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
519
527
  // sqliteCacheMaxRows and taskProcessorThreads are applied before init() in
520
528
  // surfaceCreated; calling them here (after buildEngine) has no effect.
521
529
  minAltitudeAboveTerrain?.let { b.setMinAltitudeAboveTerrain(it.toFloat()) }
530
+ b.setAltitudeDatumEllipsoid(altitudeDatum == "ellipsoid")
522
531
  }
523
532
 
524
533
  /**
@@ -110,7 +110,7 @@ void CameraIntegrator::updateAlphaBeta(
110
110
  }
111
111
 
112
112
  void CameraIntegrator::extrapolate(
113
- AlphaBeta& s, TimePoint now, double maxRateAbs, double demand) {
113
+ AlphaBeta& s, TimePoint now, double maxRateAbs, double demand, bool wrapAngle) {
114
114
  double dt = std::chrono::duration<double>(now - s.tState).count();
115
115
  if (dt <= 0.0) {
116
116
  s.tState = now;
@@ -166,8 +166,9 @@ void CameraIntegrator::extrapolate(
166
166
  // sensor streams always deliver a new update before the grace window
167
167
  // expires, so this term is never active for them.
168
168
  if (measurementsStopped && tunables::kDemandPullTauSec > 1.0e-6) {
169
- s.value += (1.0 - std::exp(-dt / tunables::kDemandPullTauSec))
170
- * (demand - s.value);
169
+ const double delta = wrapAngle ? shortAngleDeltaDeg(s.value, demand)
170
+ : (demand - s.value);
171
+ s.value += (1.0 - std::exp(-dt / tunables::kDemandPullTauSec)) * delta;
171
172
  }
172
173
  }
173
174
 
@@ -203,21 +204,11 @@ void CameraIntegrator::setAltitude(double altitudeMeters) {
203
204
  }
204
205
 
205
206
  altitudeMeters = std::min(altitudeMeters, kMaxAltMeters);
206
-
207
- // Adaptive alpha based on altitude error magnitude:
208
- // - Large changes (pinch gesture, cliffs) → fast (α ≈ 0.7)
209
- // - Small changes (gentle terrain) → smooth (α ≈ 0.3)
210
- const double error = std::abs(altitudeMeters - alt_.value);
211
- constexpr double kMinAlpha = 0.3; // Smooth for small changes
212
- constexpr double kMaxAlpha = 0.7; // Fast for large changes
213
- constexpr double kErrorScale = 20.0; // 20m error → max alpha
214
-
215
- const double adaptiveAlpha = kMinAlpha + (kMaxAlpha - kMinAlpha) *
216
- std::min(error / kErrorScale, 1.0);
217
-
207
+
218
208
  updateAlphaBeta(alt_, altitudeMeters, now,
219
- adaptiveAlpha, tunables::kBetaAlt, false);
209
+ tunables::kAlphaAlt, tunables::kBetaAlt, false);
220
210
  demand_.altitude = altitudeMeters;
211
+ enforceTerrainFloor();
221
212
  }
222
213
 
223
214
  void CameraIntegrator::setHeading(double headingDeg) {
@@ -261,13 +252,8 @@ void CameraIntegrator::setViewCorrection(const glm::dquat& q) {
261
252
  }
262
253
 
263
254
  void CameraIntegrator::setVerticalFov(double degrees) {
264
- const auto now = Clock::now();
265
255
  std::lock_guard<std::mutex> g(mutex_);
266
-
267
- const double residual = degrees - vfov_.value;
268
- vfov_.value += tunables::kAlphaVfov * residual;
269
- vfov_.velocity = 0.0;
270
- vfov_.tState = now;
256
+ demand_.verticalFov = degrees;
271
257
  }
272
258
 
273
259
  void CameraIntegrator::teleport(const CameraParams& target) {
@@ -305,6 +291,12 @@ void CameraIntegrator::teleport(const CameraParams& target) {
305
291
  if (demand_.verticalFov <= 0.0) {
306
292
  demand_.verticalFov = vfov_.value;
307
293
  }
294
+
295
+ // A slew lands on the requested altitude. The next rendered frame samples
296
+ // the terrain at the new position and establishes a new floor.
297
+ terrainFloorActive_ = false;
298
+ terrainFloorMsl_ = 0.0;
299
+ lastStep_ = now;
308
300
  }
309
301
 
310
302
  CameraParams CameraIntegrator::step() {
@@ -328,13 +320,27 @@ CameraParams CameraIntegrator::step() {
328
320
  latLonCapDegSec = glm::degrees(groundCap / kEarthMeanRadiusM);
329
321
  }
330
322
 
331
- extrapolate(lat_, now, latLonCapDegSec, demand_.latitude);
332
- extrapolate(lon_, now, latLonCapDegSec, demand_.longitude);
333
- extrapolate(alt_, now, climbCap, demand_.altitude);
334
- extrapolate(hdg_, now, yawCap, demand_.heading);
335
- extrapolate(pitch_, now, pitchCap, demand_.pitch);
336
- extrapolate(roll_, now, rollCap, demand_.roll);
337
- extrapolate(vfov_, now, 0.0, demand_.verticalFov);
323
+ extrapolate(lat_, now, latLonCapDegSec, demand_.latitude, false);
324
+ extrapolate(lon_, now, latLonCapDegSec, demand_.longitude, true);
325
+ extrapolate(alt_, now, climbCap, demand_.altitude, false);
326
+ extrapolate(hdg_, now, yawCap, demand_.heading, true);
327
+ extrapolate(pitch_, now, pitchCap, demand_.pitch, true);
328
+ extrapolate(roll_, now, rollCap, demand_.roll, true);
329
+
330
+ double frameDt = 0.0;
331
+ if (lastStep_.time_since_epoch().count() > 0) {
332
+ frameDt = std::chrono::duration<double>(now - lastStep_).count();
333
+ if (frameDt < 0.0) frameDt = 0.0;
334
+ if (frameDt > tunables::kMaxFrameDtSec) frameDt = tunables::kMaxFrameDtSec;
335
+ }
336
+ lastStep_ = now;
337
+
338
+ if (frameDt > 0.0 && tunables::kVfovTauSec > 1.0e-6) {
339
+ const double blend = 1.0 - std::exp(-frameDt / tunables::kVfovTauSec);
340
+ vfov_.value += blend * (demand_.verticalFov - vfov_.value);
341
+ vfov_.velocity = 0.0;
342
+ vfov_.tState = now;
343
+ }
338
344
 
339
345
  hdg_.value = wrap360(hdg_.value);
340
346
  lat_.value = std::clamp(lat_.value, -90.0, 90.0);
@@ -343,12 +349,19 @@ CameraParams CameraIntegrator::step() {
343
349
  // continuous value for short-arc maths and the camera transform doesn't
344
350
  // care.
345
351
 
346
- // viewCorrection: SLERP toward the latest target.
352
+ // viewCorrection: time-based SLERP toward the latest target.
347
353
  glm::dquat cur = glm::normalize(viewCorrActual_);
348
354
  glm::dquat tgt = glm::normalize(viewCorrTarget_);
349
355
  if (glm::dot(cur, tgt) < 0.0) tgt = -tgt;
350
- viewCorrActual_ = glm::normalize(
351
- glm::slerp(cur, tgt, tunables::kAlphaViewCorr));
356
+ double slerpT = 1.0;
357
+ if (tunables::kViewCorrTauSec > 1.0e-6) {
358
+ slerpT = (frameDt > 0.0)
359
+ ? (1.0 - std::exp(-frameDt / tunables::kViewCorrTauSec))
360
+ : 0.0;
361
+ }
362
+ viewCorrActual_ = glm::normalize(glm::slerp(cur, tgt, slerpT));
363
+
364
+ enforceTerrainFloor();
352
365
 
353
366
  CameraParams out;
354
367
  out.latitude = lat_.value;
@@ -362,23 +375,40 @@ CameraParams CameraIntegrator::step() {
362
375
  return out;
363
376
  }
364
377
 
365
- void CameraIntegrator::clampActualAltitude(double newAltMsl) {
366
- const auto now = Clock::now();
378
+ void CameraIntegrator::setTerrainFloor(double floorMsl) {
367
379
  std::lock_guard<std::mutex> g(mutex_);
368
-
369
- // Instead of snapping, feed the clamped altitude through the alpha-beta
370
- // tracker for smooth interpolation. This matches how heading/pitch/roll work.
371
- updateAlphaBeta(alt_, newAltMsl, now,
372
- tunables::kAlphaAlt, tunables::kBetaAlt, false);
373
-
374
- // Only clamp DOWNWARD velocity to prevent descending through terrain.
375
- // Allow upward velocity for smooth ascents over rising terrain.
376
- if (alt_.velocity < 0.0) {
380
+ if (!std::isfinite(floorMsl)) return;
381
+ terrainFloorActive_ = true;
382
+ terrainFloorMsl_ = std::min(floorMsl, kMaxAltMeters);
383
+ enforceTerrainFloor();
384
+ }
385
+
386
+ void CameraIntegrator::clearTerrainFloor() {
387
+ std::lock_guard<std::mutex> g(mutex_);
388
+ terrainFloorActive_ = false;
389
+ terrainFloorMsl_ = 0.0;
390
+ }
391
+
392
+ void CameraIntegrator::clampActualAltitude(double newAltMsl) {
393
+ setTerrainFloor(newAltMsl);
394
+ }
395
+
396
+ void CameraIntegrator::enforceTerrainFloor() {
397
+ if (!terrainFloorActive_) return;
398
+
399
+ const bool inContact =
400
+ demand_.altitude <= terrainFloorMsl_ + tunables::kEpsAltitudeMeters;
401
+ if (inContact) {
402
+ // Contour contact: the floor sample owns altitude, both directions.
403
+ // Residual vertical velocity would carry the camera into the next face.
404
+ alt_.value = terrainFloorMsl_;
377
405
  alt_.velocity = 0.0;
406
+ return;
407
+ }
408
+ if (alt_.value < terrainFloorMsl_) {
409
+ alt_.value = terrainFloorMsl_;
410
+ if (alt_.velocity < 0.0) alt_.velocity = 0.0;
378
411
  }
379
-
380
- // Do not touch demand_.altitude — getDemand() should still report what the
381
- // user asked for.
382
412
  }
383
413
 
384
414
  void CameraIntegrator::setRateCaps(const CameraRateCaps& caps) {
@@ -410,7 +440,12 @@ bool CameraIntegrator::isActive() const {
410
440
  if (std::abs(demand_.latitude - lat_.value) > tunables::kEpsLatLon) return true;
411
441
  if (std::abs(shortAngleDeltaDeg(lon_.value, demand_.longitude))
412
442
  > tunables::kEpsLatLon) return true;
413
- if (std::abs(demand_.altitude - alt_.value) > tunables::kEpsAltitudeMeters) return true;
443
+ if (terrainFloorActive_ &&
444
+ demand_.altitude <= terrainFloorMsl_ + tunables::kEpsAltitudeMeters) {
445
+ if (std::abs(alt_.value - terrainFloorMsl_) > tunables::kEpsAltitudeMeters) return true;
446
+ } else if (std::abs(demand_.altitude - alt_.value) > tunables::kEpsAltitudeMeters) {
447
+ return true;
448
+ }
414
449
  if (angleAbsDeltaDeg(demand_.heading, hdg_.value) > tunables::kEpsAngleDeg) return true;
415
450
  if (angleAbsDeltaDeg(demand_.pitch, pitch_.value) > tunables::kEpsAngleDeg) return true;
416
451
  if (angleAbsDeltaDeg(demand_.roll, roll_.value) > tunables::kEpsAngleDeg) return true;
@@ -73,11 +73,18 @@ public:
73
73
  // values and an instant runaway.
74
74
  CameraParams step();
75
75
 
76
- // Terrain-floor write-back hook. Replaces the *actual* altitude value with
77
- // `newAltMsl` and zeroes vertical velocity so the integrator does not
78
- // immediately try to descend back through the floor. The *demand* altitude
79
- // is left untouched — getDemand().altitude continues to reflect what the
80
- // consumer asked for.
76
+ // Terrain floor in metres MSL. While active, the rendered altitude is never
77
+ // below this value. When the demand altitude is at or below the floor the
78
+ // camera rides it exactly (uphill and downhill), so a later setAltitude
79
+ // under the surface cannot pull the rendered camera back inside. Demand
80
+ // itself is left untouched.
81
+ void setTerrainFloor(double floorMsl);
82
+
83
+ // Drops the floor so the next slew or the next frame can establish a new
84
+ // one. Used by teleport().
85
+ void clearTerrainFloor();
86
+
87
+ // Sets the terrain floor to `newAltMsl`. Same as setTerrainFloor.
81
88
  void clampActualAltitude(double newAltMsl);
82
89
 
83
90
  // Runtime override of the rate caps (driven by view props). 0 = uncapped
@@ -122,9 +129,14 @@ private:
122
129
  double alpha, double beta, bool wrapAngle);
123
130
 
124
131
  // Advance the running estimate to `now`, clamp velocity to `maxRateAbs`
125
- // (per second; 0 = uncapped). For angles, the resulting value is also
126
- // normalised to its canonical range by the caller as needed.
127
- static void extrapolate(AlphaBeta& s, TimePoint now, double maxRateAbs, double demand);
132
+ // (per second; 0 = uncapped). `wrapAngle` makes the silence demand-pull
133
+ // take the short arc (longitude, heading, pitch, roll).
134
+ static void extrapolate(AlphaBeta& s, TimePoint now, double maxRateAbs,
135
+ double demand, bool wrapAngle);
136
+
137
+ // Snap the rendered altitude onto the floor when the camera is in contact
138
+ // or currently below it. Demand is not modified.
139
+ void enforceTerrainFloor();
128
140
 
129
141
  mutable std::mutex mutex_;
130
142
 
@@ -141,6 +153,13 @@ private:
141
153
 
142
154
  // Runtime overrides; 0 = use compile-time defaults from EngineTunables.
143
155
  CameraRateCaps caps_{};
156
+
157
+ // Last step() timestamp. Drives the dt-based fov blend and view-correction
158
+ // slerp. Epoch means "no step yet".
159
+ TimePoint lastStep_{};
160
+
161
+ bool terrainFloorActive_ = false;
162
+ double terrainFloorMsl_ = 0.0;
144
163
  };
145
164
 
146
165
  } // namespace reactnativecesium