react-native-cesium 1.2.0 → 1.2.2

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 (34) hide show
  1. package/README.md +2 -2
  2. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +25 -2
  3. package/android/src/main/cpp/CesiumBridgeAndroid.h +1 -0
  4. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +2 -0
  5. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +3 -0
  6. package/cpp/engine/CameraSmoother.cpp +7 -35
  7. package/cpp/engine/CameraSmoother.hpp +4 -8
  8. package/cpp/engine/CameraTargetState.cpp +112 -21
  9. package/cpp/engine/CameraTargetState.hpp +40 -58
  10. package/cpp/engine/CesiumEngine.cpp +32 -0
  11. package/cpp/engine/CesiumEngine.hpp +16 -0
  12. package/ios/CesiumBridge.h +1 -0
  13. package/ios/CesiumBridge.mm +24 -2
  14. package/ios/HybridCesiumView.swift +4 -0
  15. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +7 -0
  16. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  17. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +9 -0
  18. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +2 -0
  19. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +4 -0
  20. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +6 -0
  21. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +7 -0
  22. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +5 -0
  23. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +1 -0
  24. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +24 -0
  25. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +2 -0
  26. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +2 -0
  27. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +11 -0
  28. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +1 -0
  29. package/nitrogen/generated/shared/json/CesiumViewConfig.json +1 -0
  30. package/package.json +1 -1
  31. package/scripts/cesium/clone.mjs +1 -1
  32. package/scripts/cesium/config.mjs +8 -0
  33. package/scripts/cesium/ensure-native.mjs +163 -17
  34. package/src/specs/cesium-view.nitro.ts +7 -0
package/README.md CHANGED
@@ -399,11 +399,11 @@ Avoid assuming **main-thread-only** vs **JS-thread-only** labels beyond the abov
399
399
 
400
400
  #### Smoothing of incoming camera updates
401
401
 
402
- Every value you pass to `setCamera` / `setCameraQuaternion` is treated as a **demand** target — what you would *like* the camera to be — rather than a per-frame command. The native render thread runs an exponential filter every frame that converges the displayed camera onto your demand target, which means a coarse 1 Hz position feed (e.g. external GPS) does **not** teleport the view once a second. Instead it eases between samples.
402
+ Every value you pass to `setCamera` / `setCameraQuaternion` is treated as a **demand** target — what you would *like* the camera to be — rather than a per-frame command. The native render thread interpolates the displayed camera toward your demand target every frame, which means a coarse 1 Hz position feed (e.g. external GPS) does **not** teleport the view once a second. Instead it moves at constant speed — exactly like looking out the window of the vehicle being tracked.
403
403
 
404
404
  | DoF | Smoothing |
405
405
  | --- | --- |
406
- | `latitude` / `longitude` | **Adaptive** exponential filter. Time constant `τ = clamp(α · ewmaInterval, τmin, τmax)` where `ewmaInterval` tracks the cadence of your `setCamera` calls. ~60 Hz gestures collapse `τ` to the floor (≤1 frame of trail, indistinguishable from the previous "snap" behaviour); ~1 Hz GPS feeds expand `τ` so each sample is ~95 % converged before the next arrives. Cross-planet jumps (Δ > ~0.5°) bypass the smoother and snap, so a deliberate teleport stays instant. Antimeridian crossings always rotate the short way. |
406
+ | `latitude` / `longitude` | **Linear interpolation** at constant speed between position fixes. When a new lat/lon arrives, the native engine records the previous target as the "from" position and linearly lerps toward the new target over the EWMA inter-arrival interval (e.g. a 1 Hz GPS feed gives a 1 s travel time, so the camera moves at exactly the vehicle's speed between fixes). At 60 Hz gestures the interval collapses to ~16 ms making the interpolation indistinguishable from an instant snap. Cross-planet jumps (Δ > ~0.5°) bypass interpolation so deliberate teleports remain instant. Antimeridian crossings always rotate the short way. |
407
407
  | `altitude` | Exponential, fixed `τ ≈ 40 ms`. |
408
408
  | `heading` / `pitch` / `roll` | Exponential on the shortest-arc angular delta, fixed `τ ≈ 20–33 ms`. |
409
409
  | `viewCorrection` (quaternion) | SLERP-based exponential, fixed `τ ≈ 20 ms`, hemisphere-corrected so it always takes the short rotation. |
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include "engine/CameraSmoother.hpp"
4
4
  #include "engine/EngineTunables.hpp"
5
+ #include "engine/GeoidConverter.hpp"
5
6
  #include "vulkan/VulkanBackend.h"
6
7
 
7
8
  #include <android/native_window_jni.h>
@@ -198,6 +199,10 @@ void CesiumBridgeAndroid::setTaskProcessorThreads(int32_t v) {
198
199
  // initialize().
199
200
  config_.taskProcessorThreads = std::max(0, v);
200
201
  }
202
+ void CesiumBridgeAndroid::setMinAltitudeAboveTerrain(float v) {
203
+ config_.minAltitudeAboveTerrain = std::max(0.0f, v);
204
+ if (initialized_) applyEngineConfig();
205
+ }
201
206
 
202
207
  void CesiumBridgeAndroid::markNeedsRender() { target_.requestForceRender(); }
203
208
 
@@ -239,8 +244,22 @@ void CesiumBridgeAndroid::renderFrame(double dt) {
239
244
 
240
245
  const auto cur = engine_->camera().getParams();
241
246
  const auto tgt = target_.snapshot();
242
- const auto smooth = reactnativecesium::CameraSmoother::step(
243
- cur, tgt, dt, target_.recentIntervalSec());
247
+ auto smooth = reactnativecesium::CameraSmoother::step(cur, tgt, dt);
248
+
249
+ // ── Terrain floor clamp ───────────────────────────────────────────────
250
+ const float minAbove = engine_->getConfig().minAltitudeAboveTerrain;
251
+ if (minAbove > 0.0f) {
252
+ const double geoidOffset =
253
+ reactnativecesium::mslToEllipsoidMeters(smooth.latitude, smooth.longitude, 0.0);
254
+ const double camEllipsoid =
255
+ reactnativecesium::mslToEllipsoidMeters(smooth.latitude, smooth.longitude, smooth.altitude);
256
+ const double minEllipsoid =
257
+ static_cast<double>(engine_->terrainFloorEllipsoidMeters()) + minAbove;
258
+ if (camEllipsoid < minEllipsoid) {
259
+ smooth.altitude = minEllipsoid - geoidOffset;
260
+ }
261
+ }
262
+
244
263
  engine_->camera().setParams(smooth);
245
264
 
246
265
  // If we converged, clear the dirty flag so the next idle vsync can early-out.
@@ -540,5 +559,9 @@ JNIEXPORT void JNICALL
540
559
  Java_com_margelo_nitro_reactnativecesium_CesiumBridgeJNI_nativeSetTaskProcessorThreads(JNIEnv*, jobject, jlong ptr, jint v) {
541
560
  getBridge(ptr)->setTaskProcessorThreads(v);
542
561
  }
562
+ JNIEXPORT void JNICALL
563
+ Java_com_margelo_nitro_reactnativecesium_CesiumBridgeJNI_nativeSetMinAltitudeAboveTerrain(JNIEnv*, jobject, jlong ptr, jfloat v) {
564
+ getBridge(ptr)->setMinAltitudeAboveTerrain(v);
565
+ }
543
566
 
544
567
  } // extern "C"
@@ -52,6 +52,7 @@ struct CesiumBridgeAndroid {
52
52
  void setLodTransitionLength(double v);
53
53
  void setSqliteCacheMaxRows(int32_t v);
54
54
  void setTaskProcessorThreads(int32_t v);
55
+ void setMinAltitudeAboveTerrain(float v);
55
56
 
56
57
  void markNeedsRender();
57
58
  bool shouldRenderNextFrame();
@@ -82,6 +82,7 @@ class CesiumBridgeJNI {
82
82
  fun setLodTransitionLength(v: Double) = nativeSetLodTransitionLength(nativePtr, v)
83
83
  fun setSqliteCacheMaxRows(v: Int) = nativeSetSqliteCacheMaxRows(nativePtr, v)
84
84
  fun setTaskProcessorThreads(v: Int) = nativeSetTaskProcessorThreads(nativePtr, v)
85
+ fun setMinAltitudeAboveTerrain(v: Float) = nativeSetMinAltitudeAboveTerrain(nativePtr, v)
85
86
 
86
87
  private external fun nativeCreate(): Long
87
88
  private external fun nativeInit(ptr: Long, surface: Surface, w: Int, h: Int, cacheDir: String, cacertPath: String)
@@ -144,4 +145,5 @@ class CesiumBridgeJNI {
144
145
  private external fun nativeSetLodTransitionLength(ptr: Long, v: Double)
145
146
  private external fun nativeSetSqliteCacheMaxRows(ptr: Long, v: Int)
146
147
  private external fun nativeSetTaskProcessorThreads(ptr: Long, v: Int)
148
+ private external fun nativeSetMinAltitudeAboveTerrain(ptr: Long, v: Float)
147
149
  }
@@ -120,6 +120,8 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
120
120
  set(value) { field = value; if (value != null) postToRender { bridge?.setSqliteCacheMaxRows(value.toInt()) } }
121
121
  override var taskProcessorThreads: Double? = null
122
122
  set(value) { field = value; if (value != null) postToRender { bridge?.setTaskProcessorThreads(value.toInt()) } }
123
+ override var minAltitudeAboveTerrain: Double? = null
124
+ set(value) { field = value; if (value != null) postToRender { bridge?.setMinAltitudeAboveTerrain(value.toFloat()) } }
123
125
 
124
126
  override var onMetrics: ((metrics: CesiumMetrics) -> Unit)? = null
125
127
 
@@ -476,6 +478,7 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
476
478
  lodTransitionLength?.let { b.setLodTransitionLength(it) }
477
479
  sqliteCacheMaxRows?.let { b.setSqliteCacheMaxRows(it.toInt()) }
478
480
  taskProcessorThreads?.let { b.setTaskProcessorThreads(it.toInt()) }
481
+ minAltitudeAboveTerrain?.let { b.setMinAltitudeAboveTerrain(it.toFloat()) }
479
482
  }
480
483
 
481
484
  /**
@@ -2,7 +2,6 @@
2
2
 
3
3
  #include "EngineTunables.hpp"
4
4
 
5
- #include <algorithm>
6
5
  #include <cmath>
7
6
 
8
7
  namespace reactnativecesium {
@@ -24,40 +23,18 @@ inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
24
23
  return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
25
24
  }
26
25
 
27
- inline double wrapLonDeltaDeg(double a, double b) {
28
- return std::fmod(b - a + 540.0, 360.0) - 180.0;
29
- }
30
-
31
26
  } // namespace
32
27
 
33
28
  CameraParams CameraSmoother::step(
34
- const CameraParams& current, const CameraParams& target, double dt,
35
- double recentIntervalSec) {
29
+ const CameraParams& current, const CameraParams& target, double dt) {
36
30
  CameraParams next = current;
37
31
 
38
- // ── lat/lon: adaptive exponential smoothing ─────────────────────────────
39
- // τ scales with the cadence of incoming setCamera calls so gestures feel
40
- // instant (τ at the floor) while sparse GPS-rate updates ease across each
41
- // interval (τ near the ceiling).
42
- const double tau = std::clamp(
43
- tunables::kSmoothPositionAlpha * recentIntervalSec,
44
- tunables::kSmoothPositionTauMin,
45
- tunables::kSmoothPositionTauMax);
46
- const double aPos = 1.0 - std::exp(-dt / tau);
47
-
48
- const double dLat = target.latitude - current.latitude;
49
- const double dLon = wrapLonDeltaDeg(current.longitude, target.longitude);
50
- const bool snapThrough =
51
- std::abs(dLat) > tunables::kPosSnapThroughDeg ||
52
- std::abs(dLon) > tunables::kPosSnapThroughDeg;
53
-
54
- if (snapThrough) {
55
- next.latitude = target.latitude;
56
- next.longitude = target.longitude;
57
- } else {
58
- next.latitude = current.latitude + aPos * dLat;
59
- next.longitude = lerpAngleDeg(current.longitude, target.longitude, aPos);
60
- }
32
+ // lat/lon: snap directly to the target.
33
+ // CameraTargetState::snapshot() already produces a linearly-interpolated
34
+ // position that advances at constant speed between position fixes, so
35
+ // snapping here gives smooth, constant-velocity motion with no ease-in/out.
36
+ next.latitude = target.latitude;
37
+ next.longitude = target.longitude;
61
38
 
62
39
  const double aAlt = 1.0 - std::exp(-tunables::kSmoothAltitude * dt);
63
40
  const double aHdg = 1.0 - std::exp(-tunables::kSmoothHeading * dt);
@@ -76,11 +53,6 @@ CameraParams CameraSmoother::step(
76
53
  next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
77
54
 
78
55
  // Eventually-exact convergence: snap once the residual is sub-epsilon.
79
- if (std::abs(target.latitude - next.latitude) <= tunables::kEpsLatLon)
80
- next.latitude = target.latitude;
81
- if (std::abs(wrapLonDeltaDeg(next.longitude, target.longitude))
82
- <= tunables::kEpsLatLon)
83
- next.longitude = target.longitude;
84
56
  if (std::abs(target.altitude - next.altitude) <= tunables::kEpsAltitudeMeters)
85
57
  next.altitude = target.altitude;
86
58
  if (angleDeltaAbsDeg(next.heading, target.heading) <= tunables::kEpsAngleDeg)
@@ -8,12 +8,9 @@ namespace reactnativecesium {
8
8
  // returns the next camera state for a frame of duration `dt` seconds.
9
9
  //
10
10
  // Per-DoF smoothing rules:
11
- // - lat / lon: adaptive exponential smoothing `1 - exp(-dt/τ)` where
12
- // τ = clamp(α · recentIntervalSec, τmin, τmax). High update rates (60 Hz
13
- // gestures) drive τ to the floor so the camera tracks the finger without
14
- // perceptible lag; sparse updates (1 Hz GPS) widen τ so each sample
15
- // interpolates smoothly across its interval. Snap-through guard for
16
- // genuine teleports keeps cross-planet `setCamera` jumps instant.
11
+ // - lat / lon: snapped directly to target. The target itself is already a
12
+ // linearly-interpolated position produced by CameraTargetState::snapshot(),
13
+ // so snapping here gives constant-velocity motion between position fixes.
17
14
  // - altitude / heading / pitch / roll / viewCorrection: exponential decay
18
15
  // `1 - exp(-k * dt)` per DoF with k taken from EngineTunables.
19
16
  // - When the delta drops below the matching epsilon, the value is snapped
@@ -22,8 +19,7 @@ class CameraSmoother {
22
19
  public:
23
20
  static CameraParams step(const CameraParams& current,
24
21
  const CameraParams& target,
25
- double dt,
26
- double recentIntervalSec);
22
+ double dt);
27
23
  };
28
24
 
29
25
  } // namespace reactnativecesium
@@ -1,18 +1,130 @@
1
1
  #include "CameraTargetState.hpp"
2
2
 
3
+ #include <algorithm>
3
4
  #include <cmath>
4
5
 
5
6
  namespace reactnativecesium {
6
7
 
7
8
  namespace {
9
+
8
10
  inline double angleDeltaAbsDeg(double a, double b) {
9
11
  return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
10
12
  }
13
+
11
14
  inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
12
15
  return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
13
16
  }
17
+
18
+ // Shortest signed angular delta for longitude, wrapped to [-180, 180].
19
+ inline double lonDeltaDeg(double from, double to) {
20
+ return std::fmod(to - from + 540.0, 360.0) - 180.0;
21
+ }
22
+
14
23
  } // namespace
15
24
 
25
+ // ── Public writers ────────────────────────────────────────────────────────────
26
+
27
+ void CameraTargetState::setAll(const CameraParams& target) {
28
+ {
29
+ std::lock_guard<std::mutex> g(mutex_);
30
+ target_ = target;
31
+ // Deliberately do NOT touch interpolation state: setAll is engine-init only
32
+ // and the first frame must snap to the requested position immediately.
33
+ }
34
+ dirty_.store(true, std::memory_order_release);
35
+ forceRender_.store(true, std::memory_order_release);
36
+ }
37
+
38
+ void CameraTargetState::setHpr(double lat, double lon, double alt,
39
+ double heading, double pitch, double roll) {
40
+ {
41
+ std::lock_guard<std::mutex> g(mutex_);
42
+
43
+ const bool posChanged =
44
+ std::abs(lat - target_.latitude) > tunables::kEpsLatLon ||
45
+ std::abs(lon - target_.longitude) > tunables::kEpsLatLon;
46
+
47
+ if (posChanged) {
48
+ const auto now = std::chrono::steady_clock::now();
49
+
50
+ if (!hasInterpolation_) {
51
+ // First runtime lat/lon update: seed posFrom = destination so that
52
+ // lerp(dest, dest, t) = dest for all t — clean snap, no startup glide.
53
+ posFromLat_ = lat;
54
+ posFromLon_ = lon;
55
+ ewmaIntervalSec_ = tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha;
56
+ posUpdateTime_ = now;
57
+ hasInterpolation_ = true;
58
+ } else {
59
+ // Subsequent update: record where we're moving FROM (the previous
60
+ // target), update the EWMA interval, and reset the clock.
61
+ posFromLat_ = target_.latitude;
62
+ posFromLon_ = target_.longitude;
63
+
64
+ const double dt = std::chrono::duration<double>(now - posUpdateTime_).count();
65
+ if (dt > 0.0) {
66
+ ewmaIntervalSec_ = ewmaIntervalSec_ +
67
+ tunables::kEwmaIntervalAlpha * (dt - ewmaIntervalSec_);
68
+ }
69
+ posUpdateTime_ = now;
70
+ }
71
+ }
72
+
73
+ target_.latitude = lat;
74
+ target_.longitude = lon;
75
+ target_.altitude = alt;
76
+ target_.heading = heading;
77
+ target_.pitch = pitch;
78
+ target_.roll = roll;
79
+ }
80
+ dirty_.store(true, std::memory_order_release);
81
+ forceRender_.store(true, std::memory_order_release);
82
+ }
83
+
84
+ void CameraTargetState::setViewCorrection(const glm::dquat& q) {
85
+ {
86
+ std::lock_guard<std::mutex> g(mutex_);
87
+ target_.viewCorrection = q;
88
+ // Position cadence intentionally NOT updated — viewCorrection changes
89
+ // carry no lat/lon information and must not affect the interpolation window.
90
+ }
91
+ dirty_.store(true, std::memory_order_release);
92
+ forceRender_.store(true, std::memory_order_release);
93
+ }
94
+
95
+ // ── Snapshot (render-thread read) ─────────────────────────────────────────────
96
+
97
+ CameraParams CameraTargetState::snapshot() const {
98
+ const auto now = std::chrono::steady_clock::now();
99
+ std::lock_guard<std::mutex> g(mutex_);
100
+
101
+ CameraParams result = target_;
102
+
103
+ if (hasInterpolation_ && ewmaIntervalSec_ > 0.0) {
104
+ const double dLat = target_.latitude - posFromLat_;
105
+ const double dLon = lonDeltaDeg(posFromLon_, target_.longitude);
106
+
107
+ // Snap-through guard: genuine teleports (setCamera(NewYork → Tokyo)) bypass
108
+ // the interpolation so they remain instant rather than gliding across the planet.
109
+ const bool snapThrough =
110
+ std::abs(dLat) > tunables::kPosSnapThroughDeg ||
111
+ std::abs(dLon) > tunables::kPosSnapThroughDeg;
112
+
113
+ if (!snapThrough) {
114
+ const double elapsed =
115
+ std::chrono::duration<double>(now - posUpdateTime_).count();
116
+ const double t = std::min(elapsed / ewmaIntervalSec_, 1.0);
117
+
118
+ result.latitude = posFromLat_ + t * dLat;
119
+ result.longitude = posFromLon_ + t * dLon;
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ // ── Epsilon check (shared) ────────────────────────────────────────────────────
127
+
16
128
  bool CameraTargetState::deltaExceedsEpsilon(
17
129
  const CameraParams& a, const CameraParams& b) {
18
130
  return std::abs(a.latitude - b.latitude) > tunables::kEpsLatLon
@@ -25,25 +137,4 @@ bool CameraTargetState::deltaExceedsEpsilon(
25
137
  > tunables::kEpsQuatDot;
26
138
  }
27
139
 
28
- void CameraTargetState::noteUpdateCadence() noexcept {
29
- const auto now = std::chrono::steady_clock::now();
30
- if (!hasLastUpdate_) {
31
- // Seed the EWMA with τmax so the first sample after a quiet period uses
32
- // the full smoothing budget rather than the floor (5 ms ≈ snap).
33
- ewmaIntervalSec_.store(tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha,
34
- std::memory_order_release);
35
- lastUpdateTime_ = now;
36
- hasLastUpdate_ = true;
37
- return;
38
- }
39
-
40
- const double dt = std::chrono::duration<double>(now - lastUpdateTime_).count();
41
- lastUpdateTime_ = now;
42
- if (dt <= 0.0) return; // back-to-back writes from the same frame: ignore.
43
-
44
- const double prev = ewmaIntervalSec_.load(std::memory_order_relaxed);
45
- const double next = prev + tunables::kEwmaIntervalAlpha * (dt - prev);
46
- ewmaIntervalSec_.store(next, std::memory_order_release);
47
- }
48
-
49
140
  } // namespace reactnativecesium
@@ -21,54 +21,43 @@ namespace reactnativecesium {
21
21
  //
22
22
  // `dirty_` and `forceRender_` are independent atomic flags so the read side can
23
23
  // poll without taking the lock when the camera has not moved.
24
+ //
25
+ // Lat/lon interpolation
26
+ // ─────────────────────
27
+ // When setHpr is called with a new lat/lon, `posFrom` (old target) and
28
+ // `posUpdateTime` are recorded inside the mutex. snapshot() then linearly
29
+ // interpolates posFrom → target over `ewmaIntervalSec` (EWMA of inter-arrival
30
+ // times). This produces constant-velocity motion between position fixes —
31
+ // realistic for any vehicle/GPS source — without any changes to the public API.
32
+ //
33
+ // Special cases handled in snapshot():
34
+ // - t >= 1 : camera has reached the target, returns target_ directly.
35
+ // - Snap-through guard: deltas > kPosSnapThroughDeg bypass interpolation so
36
+ // deliberate teleports (setCamera(NewYork → Tokyo)) remain instant.
37
+ // - First setHpr for a given lat/lon: posFrom is set to the *destination*
38
+ // so lerp(dest, dest, t) = dest — no startup glide.
39
+ // - setAll (initialisation only): does not touch interpolation state; always
40
+ // snaps directly.
24
41
  class CameraTargetState {
25
42
  public:
26
43
  CameraTargetState() = default;
27
44
 
28
- // Replace the entire target (does not touch viewCorrection if you want to
29
- // preserve it — use setHpr instead).
30
- void setAll(const CameraParams& target) {
31
- {
32
- std::lock_guard<std::mutex> g(mutex_);
33
- target_ = target;
34
- }
35
- noteUpdateCadence();
36
- dirty_.store(true, std::memory_order_release);
37
- forceRender_.store(true, std::memory_order_release);
38
- }
45
+ // Replace the entire target. Used only for engine initialisation; does NOT
46
+ // update position-interpolation state so the first rendered frame snaps.
47
+ void setAll(const CameraParams& target);
39
48
 
40
49
  // Update lat/lon/alt + heading/pitch/roll, leaving viewCorrection unchanged.
50
+ // When lat/lon actually changes, updates the linear-interpolation bookkeeping.
41
51
  void setHpr(double lat, double lon, double alt,
42
- double heading, double pitch, double roll) {
43
- {
44
- std::lock_guard<std::mutex> g(mutex_);
45
- target_.latitude = lat;
46
- target_.longitude = lon;
47
- target_.altitude = alt;
48
- target_.heading = heading;
49
- target_.pitch = pitch;
50
- target_.roll = roll;
51
- }
52
- noteUpdateCadence();
53
- dirty_.store(true, std::memory_order_release);
54
- forceRender_.store(true, std::memory_order_release);
55
- }
52
+ double heading, double pitch, double roll);
56
53
 
57
- void setViewCorrection(const glm::dquat& q) {
58
- {
59
- std::lock_guard<std::mutex> g(mutex_);
60
- target_.viewCorrection = q;
61
- }
62
- noteUpdateCadence();
63
- dirty_.store(true, std::memory_order_release);
64
- forceRender_.store(true, std::memory_order_release);
65
- }
54
+ void setViewCorrection(const glm::dquat& q);
66
55
 
67
- // Snapshot the demand target (read side; one mutex per frame).
68
- CameraParams snapshot() const {
69
- std::lock_guard<std::mutex> g(mutex_);
70
- return target_;
71
- }
56
+ // Snapshot the demand target for this render frame. Lat/lon are linearly
57
+ // interpolated between the previous target (posFrom) and the current target
58
+ // over the EWMA inter-arrival interval. All other DoFs are returned as-is
59
+ // (the smoother handles their exponential curves).
60
+ CameraParams snapshot() const;
72
61
 
73
62
  // Lightweight, lock-free check used by the render thread to decide whether
74
63
  // it should bother taking a full snapshot at all.
@@ -91,31 +80,24 @@ public:
91
80
  // whether to render this frame.
92
81
  static bool deltaExceedsEpsilon(const CameraParams& a, const CameraParams& b);
93
82
 
94
- // EWMA of the inter-arrival interval (seconds) of demand updates. Used by
95
- // CameraSmoother to scale the lat/lon time constant: high update rate ⇒
96
- // tiny τ (snappy gestures); low update rate ⇒ larger τ (smooth GPS catch-up).
97
- // Lock-free read on the render-thread hot path.
98
- double recentIntervalSec() const noexcept {
99
- return ewmaIntervalSec_.load(std::memory_order_acquire);
100
- }
101
-
102
83
  private:
103
- // Folds the time since the previous demand update into ewmaIntervalSec_.
104
- // Called under (or after) the target_ mutex; the EWMA itself is atomic so
105
- // the read side never blocks. Writes are uncontended (only JS-thread).
106
- void noteUpdateCadence() noexcept;
107
-
108
84
  mutable std::mutex mutex_;
109
85
  CameraParams target_;
110
86
  std::atomic<bool> dirty_{false};
111
87
  std::atomic<bool> forceRender_{true};
112
88
 
113
- // Cadence tracker. lastUpdateTime_ is only touched on the writer (JS)
114
- // thread, so it does not need synchronisation; ewmaIntervalSec_ is read
115
- // by the render thread without taking the mutex.
116
- std::chrono::steady_clock::time_point lastUpdateTime_{};
117
- bool hasLastUpdate_{false};
118
- std::atomic<double> ewmaIntervalSec_{0.0};
89
+ // ── Lat/lon linear interpolation state (all protected by mutex_) ──────────
90
+ // posFromLat_/posFromLon_: the target position when the last update arrived
91
+ // (i.e. where we are smoothly moving FROM).
92
+ // posUpdateTime_: wall-clock time of the last lat/lon change.
93
+ // ewmaIntervalSec_: EWMA of inter-arrival times of lat/lon changes.
94
+ // hasInterpolation_: true once we have valid posFrom + posUpdateTime
95
+ // data (false until the second distinct lat/lon value).
96
+ double posFromLat_{0.0};
97
+ double posFromLon_{0.0};
98
+ std::chrono::steady_clock::time_point posUpdateTime_{};
99
+ double ewmaIntervalSec_{0.0};
100
+ bool hasInterpolation_{false};
119
101
  };
120
102
 
121
103
  } // namespace reactnativecesium
@@ -455,6 +455,16 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
455
455
  }
456
456
  }
457
457
 
458
+ // ── Terrain floor tracking (for minAltitudeAboveTerrain clamp) ───────────
459
+ // Computed once before the tile loop and updated per-tile inside it.
460
+ // Only active when the feature is enabled to avoid any overhead otherwise.
461
+ const bool trackTerrainFloor = (config_.minAltitudeAboveTerrain > 0.0f);
462
+ const CameraParams camParams = trackTerrainFloor ? camera_.getParams() : CameraParams{};
463
+ const double camLatRad = glm::radians(camParams.latitude);
464
+ const double camLonRad = glm::radians(camParams.longitude);
465
+ float bestTerrainFloor = terrainFloorEllipsoidM_; // carry previous value if no tile matches
466
+ double bestTerrainDist = std::numeric_limits<double>::max();
467
+
458
468
  for (const auto& tile : updateResult.tilesToRenderThisFrame) {
459
469
  if (!tile) continue;
460
470
 
@@ -478,6 +488,24 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
478
488
  static_cast<float>(rect.getSouth()),
479
489
  static_cast<float>(rect.getEast()),
480
490
  static_cast<float>(rect.getNorth()));
491
+
492
+ // ── Terrain floor: find nearest vertex to camera nadir ────────────
493
+ if (trackTerrainFloor &&
494
+ rect.contains(CesiumGeospatial::Cartographic(camLonRad, camLatRad, 0.0))) {
495
+ for (const auto& prim : res->primitives) {
496
+ if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
497
+ const size_t vertexCount = prim.localPositions.size();
498
+ for (size_t vi = 0; vi < vertexCount; ++vi) {
499
+ const glm::dvec3 vECEF =
500
+ glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
501
+ const double dist = glm::length(vECEF - cameraPos);
502
+ if (dist < bestTerrainDist) {
503
+ bestTerrainDist = dist;
504
+ bestTerrainFloor = prim.altitudes[vi];
505
+ }
506
+ }
507
+ }
508
+ }
481
509
  }
482
510
 
483
511
  for (const auto& prim : res->primitives) {
@@ -553,6 +581,10 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
553
581
  result.cameraLon = params.longitude;
554
582
  result.cameraAlt = params.altitude;
555
583
 
584
+ if (trackTerrainFloor) {
585
+ terrainFloorEllipsoidM_ = bestTerrainFloor;
586
+ }
587
+
556
588
  lifecycle_.advanceFrame();
557
589
  }
558
590
 
@@ -78,6 +78,11 @@ struct EngineConfig {
78
78
  // 0 means "auto-detect" via std::thread::hardware_concurrency() clamped to
79
79
  // [2, 8]. Override only for benchmarking / regression testing.
80
80
  int32_t taskProcessorThreads = 0;
81
+
82
+ // ── Camera constraints ───────────────────────────────────────────────
83
+ // Minimum altitude above the loaded terrain surface (metres MSL).
84
+ // 0 = no constraint (default). Typical use: 2.0 to prevent going underground.
85
+ float minAltitudeAboveTerrain = 0.0f;
81
86
  };
82
87
 
83
88
  class CesiumEngine {
@@ -112,6 +117,13 @@ public:
112
117
  bool tilesetReady() const { return tileset_ != nullptr; }
113
118
  int64_t currentImageryAssetId() const { return currentImageryAssetId_; }
114
119
 
120
+ const EngineConfig& getConfig() const { return config_; }
121
+
122
+ // Ellipsoid height (metres) of the terrain vertex nearest to the camera nadir,
123
+ // updated each frame from the loaded tile mesh. 0.0 until the first terrain
124
+ // tile covering the camera position has been rendered.
125
+ float terrainFloorEllipsoidMeters() const { return terrainFloorEllipsoidM_; }
126
+
115
127
  private:
116
128
  void createTileset();
117
129
  void destroyTileset();
@@ -149,6 +161,10 @@ private:
149
161
  GlobeCamera camera_;
150
162
  TileLifecycleManager lifecycle_;
151
163
 
164
+ // Ellipsoid height of the terrain surface at the camera nadir, updated each
165
+ // frame. Initialised to 0 (sea level) until a terrain tile is first loaded.
166
+ float terrainFloorEllipsoidM_ = 0.0f;
167
+
152
168
  // Pre-computed fallback ellipsoid geometry (ECEF, absolute).
153
169
  std::vector<glm::dvec3> ellipsoidPositions_;
154
170
  std::vector<uint32_t> ellipsoidIndices_;
@@ -61,6 +61,7 @@ NS_ASSUME_NONNULL_BEGIN
61
61
  - (void)setLodTransitionLength:(double)v;
62
62
  - (void)setSqliteCacheMaxRows:(int32_t)v;
63
63
  - (void)setTaskProcessorThreads:(int32_t)v;
64
+ - (void)setMinAltitudeAboveTerrain:(float)v;
64
65
 
65
66
  /// Throttled metrics consumed by the Swift hybrid view.
66
67
  @property (nonatomic, readonly) double metricsFps;
@@ -5,6 +5,7 @@
5
5
  #include "engine/CameraTargetState.hpp"
6
6
  #include "engine/CesiumEngine.hpp"
7
7
  #include "engine/EngineTunables.hpp"
8
+ #include "engine/GeoidConverter.hpp"
8
9
  #include "engine/MetricsAggregator.hpp"
9
10
  #include "metal/MetalBackend.h"
10
11
 
@@ -237,6 +238,11 @@
237
238
  - (void)setTaskProcessorThreads:(int32_t)v {
238
239
  _config.taskProcessorThreads = std::max<int32_t>(0, v);
239
240
  }
241
+ - (void)setMinAltitudeAboveTerrain:(float)v {
242
+ _config.minAltitudeAboveTerrain = std::max(0.0f, v);
243
+ if (_engine) _engine->updateConfig(_config);
244
+ _target.requestForceRender();
245
+ }
240
246
 
241
247
  - (void)setMsaaSampleCount:(int)samples {
242
248
  if (_metalBackend) _metalBackend->setMsaaSampleCount(samples);
@@ -271,8 +277,24 @@
271
277
 
272
278
  const auto cur = _engine->camera().getParams();
273
279
  const auto tgt = _target.snapshot();
274
- const auto smooth = reactnativecesium::CameraSmoother::step(
275
- cur, tgt, dt, _target.recentIntervalSec());
280
+ auto smooth = reactnativecesium::CameraSmoother::step(cur, tgt, dt);
281
+
282
+ // ── Terrain floor clamp ───────────────────────────────────────────────
283
+ // Uses terrain floor from the previous frame (one-frame lag is
284
+ // imperceptible at 60 fps). Skipped when minAltitudeAboveTerrain == 0.
285
+ const float minAbove = _engine->getConfig().minAltitudeAboveTerrain;
286
+ if (minAbove > 0.0f) {
287
+ const double geoidOffset =
288
+ reactnativecesium::mslToEllipsoidMeters(smooth.latitude, smooth.longitude, 0.0);
289
+ const double camEllipsoid =
290
+ reactnativecesium::mslToEllipsoidMeters(smooth.latitude, smooth.longitude, smooth.altitude);
291
+ const double minEllipsoid =
292
+ static_cast<double>(_engine->terrainFloorEllipsoidMeters()) + minAbove;
293
+ if (camEllipsoid < minEllipsoid) {
294
+ smooth.altitude = minEllipsoid - geoidOffset;
295
+ }
296
+ }
297
+
276
298
  _engine->camera().setParams(smooth);
277
299
 
278
300
  if (!reactnativecesium::CameraTargetState::deltaExceedsEpsilon(smooth, tgt)) {
@@ -101,6 +101,9 @@ class HybridCesiumView: HybridCesiumViewSpec {
101
101
  var taskProcessorThreads: Double? {
102
102
  didSet { if let v = taskProcessorThreads { postToRender { [weak self] in self?.bridge?.setTaskProcessorThreads(Int32(v)) } } }
103
103
  }
104
+ var minAltitudeAboveTerrain: Double? {
105
+ didSet { if let v = minAltitudeAboveTerrain { postToRender { [weak self] in self?.bridge?.setMinAltitudeAboveTerrain(Float(v)) } } }
106
+ }
104
107
 
105
108
  // MARK: - Render-thread dispatch helper
106
109
 
@@ -319,6 +322,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
319
322
  if let v = lodTransitionLength { bridge?.setLodTransitionLength(v) }
320
323
  if let v = sqliteCacheMaxRows { bridge?.setSqliteCacheMaxRows(Int32(v)) }
321
324
  if let v = taskProcessorThreads { bridge?.setTaskProcessorThreads(Int32(v)) }
325
+ if let v = minAltitudeAboveTerrain { bridge?.setMinAltitudeAboveTerrain(Float(v)) }
322
326
  }
323
327
 
324
328
  /// Pushes camera state into native bridge only when values changed.
@@ -67,6 +67,13 @@ export interface CesiumViewProps extends HybridViewProps {
67
67
  sqliteCacheMaxRows?: number;
68
68
  /** Worker thread count. 0 = auto (hardware_concurrency-1, clamped 2..8). */
69
69
  taskProcessorThreads?: number;
70
+ /**
71
+ * Minimum altitude above the loaded terrain surface, in metres MSL.
72
+ * Default 0 (no constraint). Set to e.g. 2 to prevent the camera from going underground.
73
+ * The clamp is applied every frame in native, based on the altitude of the nearest
74
+ * terrain mesh vertex to the camera nadir.
75
+ */
76
+ minAltitudeAboveTerrain?: number;
70
77
  onMetrics?: (metrics: CesiumMetrics) => void;
71
78
  }
72
79
  export interface CesiumViewMethods extends HybridViewMethods {
@@ -1 +1 @@
1
- {"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AAEnC,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,+DAA+D;IAC/D,aAAa,EAAE,OAAO,CAAA;IACtB,YAAY,EAAE,OAAO,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,aAAa,EAAE,WAAW,CAAA;IAC1B,cAAc,EAAE,OAAO,CAAA;IACvB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,4BAA4B,EAAE,MAAM,CAAA;IACpC,sBAAsB,EAAE,MAAM,CAAA;IAC9B,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IAGzB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,mEAAmE;IACnE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gFAAgF;IAChF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,oEAAoE;IACpE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,2EAA2E;IAC3E,6BAA6B,CAAC,EAAE,OAAO,CAAA;IACvC,4FAA4F;IAC5F,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,8EAA8E;IAC9E,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mDAAmD;IACnD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAE7B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CAC7C;AAED,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,+CAA+C;IAC/C,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC;;;OAGG;IACH,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1E,4FAA4F;IAC5F,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAA;CACzC;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAA"}
1
+ {"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AAEnC,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,+DAA+D;IAC/D,aAAa,EAAE,OAAO,CAAA;IACtB,YAAY,EAAE,OAAO,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,aAAa,EAAE,WAAW,CAAA;IAC1B,cAAc,EAAE,OAAO,CAAA;IACvB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,4BAA4B,EAAE,MAAM,CAAA;IACpC,sBAAsB,EAAE,MAAM,CAAA;IAC9B,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IAGzB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,mEAAmE;IACnE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gFAAgF;IAChF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,oEAAoE;IACpE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,2EAA2E;IAC3E,6BAA6B,CAAC,EAAE,OAAO,CAAA;IACvC,4FAA4F;IAC5F,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,8EAA8E;IAC9E,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mDAAmD;IACnD,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAEhC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CAC7C;AAED,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,+CAA+C;IAC/C,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC;;;OAGG;IACH,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1E,4FAA4F;IAC5F,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAA;CACzC;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAA"}
@@ -247,6 +247,15 @@ namespace margelo::nitro::reactnativecesium {
247
247
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JDouble> /* taskProcessorThreads */)>("setTaskProcessorThreads");
248
248
  method(_javaPart, taskProcessorThreads.has_value() ? jni::JDouble::valueOf(taskProcessorThreads.value()) : nullptr);
249
249
  }
250
+ std::optional<double> JHybridCesiumViewSpec::getMinAltitudeAboveTerrain() {
251
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JDouble>()>("getMinAltitudeAboveTerrain");
252
+ auto __result = method(_javaPart);
253
+ return __result != nullptr ? std::make_optional(__result->value()) : std::nullopt;
254
+ }
255
+ void JHybridCesiumViewSpec::setMinAltitudeAboveTerrain(std::optional<double> minAltitudeAboveTerrain) {
256
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JDouble> /* minAltitudeAboveTerrain */)>("setMinAltitudeAboveTerrain");
257
+ method(_javaPart, minAltitudeAboveTerrain.has_value() ? jni::JDouble::valueOf(minAltitudeAboveTerrain.value()) : nullptr);
258
+ }
250
259
  std::optional<std::function<void(const CesiumMetrics& /* metrics */)>> JHybridCesiumViewSpec::getOnMetrics() {
251
260
  static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JFunc_void_CesiumMetrics::javaobject>()>("getOnMetrics_cxx");
252
261
  auto __result = method(_javaPart);
@@ -92,6 +92,8 @@ namespace margelo::nitro::reactnativecesium {
92
92
  void setSqliteCacheMaxRows(std::optional<double> sqliteCacheMaxRows) override;
93
93
  std::optional<double> getTaskProcessorThreads() override;
94
94
  void setTaskProcessorThreads(std::optional<double> taskProcessorThreads) override;
95
+ std::optional<double> getMinAltitudeAboveTerrain() override;
96
+ void setMinAltitudeAboveTerrain(std::optional<double> minAltitudeAboveTerrain) override;
95
97
  std::optional<std::function<void(const CesiumMetrics& /* metrics */)>> getOnMetrics() override;
96
98
  void setOnMetrics(const std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>& onMetrics) override;
97
99
 
@@ -121,6 +121,10 @@ void JHybridCesiumViewStateUpdater::updateViewProps(jni::alias_ref<jni::JClass>
121
121
  hybridView->setTaskProcessorThreads(props->taskProcessorThreads.value);
122
122
  props->taskProcessorThreads.isDirty = false;
123
123
  }
124
+ if (props->minAltitudeAboveTerrain.isDirty) {
125
+ hybridView->setMinAltitudeAboveTerrain(props->minAltitudeAboveTerrain.value);
126
+ props->minAltitudeAboveTerrain.isDirty = false;
127
+ }
124
128
  if (props->onMetrics.isDirty) {
125
129
  hybridView->setOnMetrics(props->onMetrics.value);
126
130
  props->onMetrics.isDirty = false;
@@ -153,6 +153,12 @@ abstract class HybridCesiumViewSpec: HybridView() {
153
153
  @set:Keep
154
154
  abstract var taskProcessorThreads: Double?
155
155
 
156
+ @get:DoNotStrip
157
+ @get:Keep
158
+ @set:DoNotStrip
159
+ @set:Keep
160
+ abstract var minAltitudeAboveTerrain: Double?
161
+
156
162
  abstract var onMetrics: ((metrics: CesiumMetrics) -> Unit)?
157
163
 
158
164
  private var onMetrics_cxx: Func_void_CesiumMetrics?
@@ -212,6 +212,13 @@ namespace margelo::nitro::reactnativecesium {
212
212
  inline void setTaskProcessorThreads(std::optional<double> taskProcessorThreads) noexcept override {
213
213
  _swiftPart.setTaskProcessorThreads(taskProcessorThreads);
214
214
  }
215
+ inline std::optional<double> getMinAltitudeAboveTerrain() noexcept override {
216
+ auto __result = _swiftPart.getMinAltitudeAboveTerrain();
217
+ return __result;
218
+ }
219
+ inline void setMinAltitudeAboveTerrain(std::optional<double> minAltitudeAboveTerrain) noexcept override {
220
+ _swiftPart.setMinAltitudeAboveTerrain(minAltitudeAboveTerrain);
221
+ }
215
222
  inline std::optional<std::function<void(const CesiumMetrics& /* metrics */)>> getOnMetrics() noexcept override {
216
223
  auto __result = _swiftPart.getOnMetrics();
217
224
  return __result;
@@ -184,6 +184,11 @@ using namespace margelo::nitro::reactnativecesium::views;
184
184
  swiftPart.setTaskProcessorThreads(newViewProps.taskProcessorThreads.value);
185
185
  newViewProps.taskProcessorThreads.isDirty = false;
186
186
  }
187
+ // minAltitudeAboveTerrain: optional
188
+ if (newViewProps.minAltitudeAboveTerrain.isDirty) {
189
+ swiftPart.setMinAltitudeAboveTerrain(newViewProps.minAltitudeAboveTerrain.value);
190
+ newViewProps.minAltitudeAboveTerrain.isDirty = false;
191
+ }
187
192
  // onMetrics: optional
188
193
  if (newViewProps.onMetrics.isDirty) {
189
194
  swiftPart.setOnMetrics(newViewProps.onMetrics.value);
@@ -31,6 +31,7 @@ public protocol HybridCesiumViewSpec_protocol: HybridObject, HybridView {
31
31
  var lodTransitionLength: Double? { get set }
32
32
  var sqliteCacheMaxRows: Double? { get set }
33
33
  var taskProcessorThreads: Double? { get set }
34
+ var minAltitudeAboveTerrain: Double? { get set }
34
35
  var onMetrics: ((_ metrics: CesiumMetrics) -> Void)? { get set }
35
36
 
36
37
  // Methods
@@ -508,6 +508,30 @@ open class HybridCesiumViewSpec_cxx {
508
508
  }
509
509
  }
510
510
 
511
+ public final var minAltitudeAboveTerrain: bridge.std__optional_double_ {
512
+ @inline(__always)
513
+ get {
514
+ return { () -> bridge.std__optional_double_ in
515
+ if let __unwrappedValue = self.__implementation.minAltitudeAboveTerrain {
516
+ return bridge.create_std__optional_double_(__unwrappedValue)
517
+ } else {
518
+ return .init()
519
+ }
520
+ }()
521
+ }
522
+ @inline(__always)
523
+ set {
524
+ self.__implementation.minAltitudeAboveTerrain = { () -> Double? in
525
+ if bridge.has_value_std__optional_double_(newValue) {
526
+ let __unwrapped = bridge.get_std__optional_double_(newValue)
527
+ return __unwrapped
528
+ } else {
529
+ return nil
530
+ }
531
+ }()
532
+ }
533
+ }
534
+
511
535
  public final var onMetrics: bridge.std__optional_std__function_void_const_CesiumMetrics_____metrics______ {
512
536
  @inline(__always)
513
537
  get {
@@ -56,6 +56,8 @@ namespace margelo::nitro::reactnativecesium {
56
56
  prototype.registerHybridSetter("sqliteCacheMaxRows", &HybridCesiumViewSpec::setSqliteCacheMaxRows);
57
57
  prototype.registerHybridGetter("taskProcessorThreads", &HybridCesiumViewSpec::getTaskProcessorThreads);
58
58
  prototype.registerHybridSetter("taskProcessorThreads", &HybridCesiumViewSpec::setTaskProcessorThreads);
59
+ prototype.registerHybridGetter("minAltitudeAboveTerrain", &HybridCesiumViewSpec::getMinAltitudeAboveTerrain);
60
+ prototype.registerHybridSetter("minAltitudeAboveTerrain", &HybridCesiumViewSpec::setMinAltitudeAboveTerrain);
59
61
  prototype.registerHybridGetter("onMetrics", &HybridCesiumViewSpec::getOnMetrics);
60
62
  prototype.registerHybridSetter("onMetrics", &HybridCesiumViewSpec::setOnMetrics);
61
63
  prototype.registerHybridMethod("getCameraState", &HybridCesiumViewSpec::getCameraState);
@@ -97,6 +97,8 @@ namespace margelo::nitro::reactnativecesium {
97
97
  virtual void setSqliteCacheMaxRows(std::optional<double> sqliteCacheMaxRows) = 0;
98
98
  virtual std::optional<double> getTaskProcessorThreads() = 0;
99
99
  virtual void setTaskProcessorThreads(std::optional<double> taskProcessorThreads) = 0;
100
+ virtual std::optional<double> getMinAltitudeAboveTerrain() = 0;
101
+ virtual void setMinAltitudeAboveTerrain(std::optional<double> minAltitudeAboveTerrain) = 0;
100
102
  virtual std::optional<std::function<void(const CesiumMetrics& /* metrics */)>> getOnMetrics() = 0;
101
103
  virtual void setOnMetrics(const std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>& onMetrics) = 0;
102
104
 
@@ -236,6 +236,16 @@ namespace margelo::nitro::reactnativecesium::views {
236
236
  throw std::runtime_error(std::string("CesiumView.taskProcessorThreads: ") + exc.what());
237
237
  }
238
238
  }()),
239
+ minAltitudeAboveTerrain([&]() -> CachedProp<std::optional<double>> {
240
+ try {
241
+ const react::RawValue* rawValue = rawProps.at("minAltitudeAboveTerrain", nullptr, nullptr);
242
+ if (rawValue == nullptr) return sourceProps.minAltitudeAboveTerrain;
243
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
244
+ return CachedProp<std::optional<double>>::fromRawValue(*runtime, value, sourceProps.minAltitudeAboveTerrain);
245
+ } catch (const std::exception& exc) {
246
+ throw std::runtime_error(std::string("CesiumView.minAltitudeAboveTerrain: ") + exc.what());
247
+ }
248
+ }()),
239
249
  onMetrics([&]() -> CachedProp<std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>> {
240
250
  try {
241
251
  const react::RawValue* rawValue = rawProps.at("onMetrics", nullptr, nullptr);
@@ -280,6 +290,7 @@ namespace margelo::nitro::reactnativecesium::views {
280
290
  case hashString("lodTransitionLength"): return true;
281
291
  case hashString("sqliteCacheMaxRows"): return true;
282
292
  case hashString("taskProcessorThreads"): return true;
293
+ case hashString("minAltitudeAboveTerrain"): return true;
283
294
  case hashString("onMetrics"): return true;
284
295
  case hashString("hybridRef"): return true;
285
296
  default: return false;
@@ -65,6 +65,7 @@ namespace margelo::nitro::reactnativecesium::views {
65
65
  CachedProp<std::optional<double>> lodTransitionLength;
66
66
  CachedProp<std::optional<double>> sqliteCacheMaxRows;
67
67
  CachedProp<std::optional<double>> taskProcessorThreads;
68
+ CachedProp<std::optional<double>> minAltitudeAboveTerrain;
68
69
  CachedProp<std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>> onMetrics;
69
70
  CachedProp<std::optional<std::function<void(const std::shared_ptr<HybridCesiumViewSpec>& /* ref */)>>> hybridRef;
70
71
 
@@ -25,6 +25,7 @@
25
25
  "lodTransitionLength": true,
26
26
  "sqliteCacheMaxRows": true,
27
27
  "taskProcessorThreads": true,
28
+ "minAltitudeAboveTerrain": true,
28
29
  "onMetrics": true,
29
30
  "hybridRef": true
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro (iOS/Metal today; Android planned)",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -6,13 +6,13 @@ import { execFileSync } from 'node:child_process'
6
6
  import { existsSync, mkdirSync } from 'node:fs'
7
7
  import { dirname, join } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
+ import { CESIUM_NATIVE_REF as ref } from './config.mjs'
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url))
11
12
  const root = join(__dirname, '..', '..')
12
13
  const vendor = join(root, 'vendor')
13
14
  const dest = join(vendor, 'cesium-native')
14
15
  const repo = 'https://github.com/CesiumGS/cesium-native.git'
15
- const ref = 'v0.60.0'
16
16
 
17
17
  function run(cmd, args, opts = {}) {
18
18
  execFileSync(cmd, args, { stdio: 'inherit', ...opts })
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared configuration for Cesium Native build scripts.
4
+ * Update CESIUM_NATIVE_REF here when bumping the pinned cesium-native version.
5
+ */
6
+
7
+ /** Git tag of CesiumGS/cesium-native to clone and build. */
8
+ export const CESIUM_NATIVE_REF = 'v0.60.0'
@@ -1,20 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * If Cesium Native vendor output is missing for the requested platform(s), run
4
- * `npm|yarn run update` then `CESIUM_BUILD_ONLY=… npm|yarn run build`.
3
+ * Ensure Cesium Native vendor output is present and built for the pinned ref.
5
4
  *
6
- * Invoked from CocoaPods (podspec prepare_command) and Android Gradle (preBuild).
5
+ * Fast-path (in order):
6
+ * 1. Vendor dir already exists and was built for the current ref → nothing to do.
7
+ * 2. Global cache (~/.cache/react-native-cesium/<ref>/<platform>/) has a matching
8
+ * build → restore from cache without recompiling.
9
+ * 3. Otherwise → clone source and build, then populate the global cache.
7
10
  *
8
- * Opt out (CI without toolchains): REACT_NATIVE_CESIUM_SKIP_NATIVE_BUILD=1 — fails fast if artifacts are still missing.
11
+ * Invoked from CocoaPods (react_native_cesium_ensure_native in Podfile pre_install)
12
+ * and Android Gradle (preBuild task).
13
+ *
14
+ * Opt out (CI without toolchains): REACT_NATIVE_CESIUM_SKIP_NATIVE_BUILD=1 —
15
+ * fails fast if artifacts are still missing.
9
16
  */
10
17
  import { spawnSync } from 'node:child_process'
11
- import { existsSync } from 'node:fs'
18
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
19
+ import { homedir } from 'node:os'
12
20
  import { dirname, join, resolve } from 'node:path'
13
21
  import { fileURLToPath } from 'node:url'
22
+ import { CESIUM_NATIVE_REF } from './config.mjs'
14
23
 
15
24
  const __dirname = dirname(fileURLToPath(import.meta.url))
16
25
  const root = resolve(join(__dirname, '..', '..'))
17
26
 
27
+ // Marker files whose existence confirms a successful build.
18
28
  const MARKER_IOS = join(root, 'vendor', 'ios', 'CesiumNative.xcframework', 'Info.plist')
19
29
  const MARKER_ANDROID = join(
20
30
  root,
@@ -26,6 +36,100 @@ const MARKER_ANDROID = join(
26
36
  'cesium-nativeConfig.cmake'
27
37
  )
28
38
 
39
+ // Stamp files that record which cesium-native ref the vendor dir was built from.
40
+ const REF_STAMP_IOS = join(root, 'vendor', 'ios', '.cesium-native-ref')
41
+ const REF_STAMP_ANDROID = join(root, 'vendor', 'android', '.cesium-native-ref')
42
+
43
+ // ─── Global cache ────────────────────────────────────────────────────────────
44
+ // Keyed by cesium-native ref so builds for different versions coexist.
45
+ // Survives node_modules being cleared between pod installs.
46
+
47
+ function globalCacheBase() {
48
+ const xdg = process.env.XDG_CACHE_HOME?.trim()
49
+ return xdg
50
+ ? join(xdg, 'react-native-cesium')
51
+ : join(homedir(), '.cache', 'react-native-cesium')
52
+ }
53
+
54
+ function cacheDir(platform, ref) {
55
+ return join(globalCacheBase(), ref, platform)
56
+ }
57
+
58
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
59
+
60
+ function readStamp(stampPath) {
61
+ try {
62
+ return readFileSync(stampPath, 'utf8').trim()
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ function isStale(markerPath, stampPath, ref) {
69
+ if (!existsSync(markerPath)) return true
70
+ const built = readStamp(stampPath)
71
+ if (built !== ref) {
72
+ console.log(
73
+ `react-native-cesium: vendor built for ${built ?? 'unknown ref'}, current ref is ${ref} — will refresh.`
74
+ )
75
+ return true
76
+ }
77
+ return false
78
+ }
79
+
80
+ /**
81
+ * Try to restore a platform's vendor dir from the global cache.
82
+ * Returns true if the cache hit and the restore succeeded.
83
+ */
84
+ function tryRestoreFromCache(platform, ref, vendorDir, stampPath, markerPath) {
85
+ const src = cacheDir(platform, ref)
86
+ if (!existsSync(src)) return false
87
+
88
+ console.log(
89
+ `react-native-cesium: cache hit for ${platform} ${ref} — restoring from ${src} (skipping build)`
90
+ )
91
+ try {
92
+ rmSync(vendorDir, { recursive: true, force: true })
93
+ mkdirSync(vendorDir, { recursive: true })
94
+ cpSync(src, vendorDir, { recursive: true })
95
+ // Ensure stamp is present even if the cached dir predates stamp support.
96
+ writeFileSync(stampPath, ref)
97
+ if (!existsSync(markerPath)) {
98
+ console.warn(
99
+ `react-native-cesium: cache restore for ${platform} ${ref} succeeded but marker is missing: ${markerPath}\n` +
100
+ 'The cache entry may be corrupt; delete it and retry:\n' +
101
+ ` rm -rf "${src}"`
102
+ )
103
+ return false
104
+ }
105
+ return true
106
+ } catch (e) {
107
+ console.warn(
108
+ `react-native-cesium: cache restore failed (${e.message}) — will rebuild.`
109
+ )
110
+ return false
111
+ }
112
+ }
113
+
114
+ /** Populate the global cache from a freshly built vendor dir. */
115
+ function saveToCache(platform, ref, vendorDir) {
116
+ const dst = cacheDir(platform, ref)
117
+ try {
118
+ rmSync(dst, { recursive: true, force: true })
119
+ mkdirSync(dst, { recursive: true })
120
+ cpSync(vendorDir, dst, { recursive: true })
121
+ console.log(
122
+ `react-native-cesium: saved ${platform} ${ref} build to cache (${dst})`
123
+ )
124
+ } catch (e) {
125
+ console.warn(
126
+ `react-native-cesium: could not write to cache (${e.message}) — build succeeded but next run may rebuild.`
127
+ )
128
+ }
129
+ }
130
+
131
+ // ─── Script runner ───────────────────────────────────────────────────────────
132
+
29
133
  function parseArgs(argv) {
30
134
  const want = { ios: false, android: false }
31
135
  for (const a of argv) {
@@ -46,7 +150,7 @@ function useYarn() {
46
150
  function runScript(name, extraEnv) {
47
151
  const yarn = useYarn()
48
152
  const cmd = yarn ? 'yarn' : 'npm'
49
- const args = yarn ? ['run', name] : ['run', name]
153
+ const args = ['run', name]
50
154
  const env = { ...process.env, ...extraEnv }
51
155
  const r = spawnSync(cmd, args, { cwd: root, env, stdio: 'inherit' })
52
156
  if (r.error) {
@@ -59,48 +163,88 @@ function runScript(name, extraEnv) {
59
163
  }
60
164
  }
61
165
 
166
+ // ─── Main ────────────────────────────────────────────────────────────────────
167
+
62
168
  function main() {
63
169
  const skip = process.env.REACT_NATIVE_CESIUM_SKIP_NATIVE_BUILD === '1'
64
170
  const want = parseArgs(process.argv.slice(2))
171
+ const ref = CESIUM_NATIVE_REF
172
+
173
+ const vendorIos = join(root, 'vendor', 'ios')
174
+ const vendorAndroid = join(root, 'vendor', 'android')
65
175
 
66
- const needIos = want.ios && !existsSync(MARKER_IOS)
67
- const needAndroid = want.android && !existsSync(MARKER_ANDROID)
176
+ let needIos = want.ios && isStale(MARKER_IOS, REF_STAMP_IOS, ref)
177
+ let needAndroid = want.android && isStale(MARKER_ANDROID, REF_STAMP_ANDROID, ref)
68
178
 
69
179
  if (!needIos && !needAndroid) {
180
+ // Vendor is already up to date. Back-fill the global cache if it is missing so
181
+ // that future runs (after node_modules is cleared) can restore without rebuilding.
182
+ if (want.ios && !existsSync(cacheDir('ios', ref))) {
183
+ saveToCache('ios', ref, vendorIos)
184
+ }
185
+ if (want.android && !existsSync(cacheDir('android', ref))) {
186
+ saveToCache('android', ref, vendorAndroid)
187
+ }
70
188
  return
71
189
  }
72
190
 
73
191
  if (skip) {
192
+ const missing = [
193
+ needIos && ` iOS: ${MARKER_IOS}`,
194
+ needAndroid && ` Android: ${MARKER_ANDROID}`,
195
+ ]
196
+ .filter(Boolean)
197
+ .join('\n')
74
198
  console.error(
75
- 'react-native-cesium: Cesium Native vendor output is missing, but REACT_NATIVE_CESIUM_SKIP_NATIVE_BUILD=1.\n' +
199
+ 'react-native-cesium: Cesium Native vendor output is missing or stale, but REACT_NATIVE_CESIUM_SKIP_NATIVE_BUILD=1.\n' +
76
200
  'Build the native libraries on a machine with the required toolchains, or unset the variable and retry.\n' +
77
- `Expected iOS: ${MARKER_IOS}\n` +
78
- `Expected Android: ${MARKER_ANDROID}\n` +
79
- 'See the package README (Build Cesium Native locally).'
201
+ missing +
202
+ '\nSee the package README (Build Cesium Native locally).'
80
203
  )
81
204
  process.exit(1)
82
205
  }
83
206
 
84
207
  if (needIos && process.platform !== 'darwin') {
85
208
  console.error(
86
- 'react-native-cesium: iOS Cesium Native output is missing; building it requires macOS with Xcode.\n' +
209
+ 'react-native-cesium: iOS Cesium Native output is missing or stale; building it requires macOS with Xcode.\n' +
87
210
  `Expected: ${MARKER_IOS}`
88
211
  )
89
212
  process.exit(1)
90
213
  }
91
214
 
215
+ // Fast path: restore from global cache if available.
216
+ if (needIos && tryRestoreFromCache('ios', ref, vendorIos, REF_STAMP_IOS, MARKER_IOS)) {
217
+ needIos = false
218
+ }
219
+ if (
220
+ needAndroid &&
221
+ tryRestoreFromCache('android', ref, vendorAndroid, REF_STAMP_ANDROID, MARKER_ANDROID)
222
+ ) {
223
+ needAndroid = false
224
+ }
225
+
226
+ if (!needIos && !needAndroid) return
227
+
228
+ // Clone cesium-native source if not already present.
92
229
  if (!existsSync(join(root, 'vendor', 'cesium-native'))) {
93
230
  console.log('react-native-cesium: running package update (clone Cesium Native / vcpkg prep)…')
94
231
  runScript('update', {})
95
232
  }
96
233
 
97
- if (needIos && !existsSync(MARKER_IOS)) {
234
+ if (needIos) {
98
235
  console.log('react-native-cesium: building Cesium Native for iOS (this can take a long time)…')
99
236
  runScript('build', { CESIUM_BUILD_ONLY: 'ios' })
237
+ writeFileSync(REF_STAMP_IOS, ref)
238
+ saveToCache('ios', ref, vendorIos)
100
239
  }
101
- if (needAndroid && !existsSync(MARKER_ANDROID)) {
102
- console.log('react-native-cesium: building Cesium Native for Android (this can take a long time)…')
240
+
241
+ if (needAndroid) {
242
+ console.log(
243
+ 'react-native-cesium: building Cesium Native for Android (this can take a long time)…'
244
+ )
103
245
  runScript('build', { CESIUM_BUILD_ONLY: 'android' })
246
+ writeFileSync(REF_STAMP_ANDROID, ref)
247
+ saveToCache('android', ref, vendorAndroid)
104
248
  }
105
249
 
106
250
  if (needIos && !existsSync(MARKER_IOS)) {
@@ -108,7 +252,9 @@ function main() {
108
252
  process.exit(1)
109
253
  }
110
254
  if (needAndroid && !existsSync(MARKER_ANDROID)) {
111
- console.error(`react-native-cesium: Android build finished but marker missing: ${MARKER_ANDROID}`)
255
+ console.error(
256
+ `react-native-cesium: Android build finished but marker missing: ${MARKER_ANDROID}`
257
+ )
112
258
  process.exit(1)
113
259
  }
114
260
  }
@@ -77,6 +77,13 @@ export interface CesiumViewProps extends HybridViewProps {
77
77
  sqliteCacheMaxRows?: number
78
78
  /** Worker thread count. 0 = auto (hardware_concurrency-1, clamped 2..8). */
79
79
  taskProcessorThreads?: number
80
+ /**
81
+ * Minimum altitude above the loaded terrain surface, in metres MSL.
82
+ * Default 0 (no constraint). Set to e.g. 2 to prevent the camera from going underground.
83
+ * The clamp is applied every frame in native, based on the altitude of the nearest
84
+ * terrain mesh vertex to the camera nadir.
85
+ */
86
+ minAltitudeAboveTerrain?: number
80
87
 
81
88
  onMetrics?: (metrics: CesiumMetrics) => void
82
89
  }