react-native-cesium 1.2.12 → 1.2.14

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.
@@ -147,7 +147,17 @@ void CameraIntegrator::extrapolate(
147
147
  if (maxRateAbs > 0.0) {
148
148
  s.velocity = clampAbs(s.velocity, maxRateAbs);
149
149
  }
150
- s.value += s.velocity * dt;
150
+
151
+ // While measurements are still arriving (gesture / sensor stream), advance
152
+ // along the learned velocity for smooth inter-frame motion. Once they stop
153
+ // (fling decay finished, finger lifted), do not coast on that velocity —
154
+ // only demand-pull toward the last set value. Coasting past demand is what
155
+ // users perceive as the camera "springing back" after a fling.
156
+ const bool measurementsStopped =
157
+ silence > grace && s.tLastMeas.time_since_epoch().count() > 0;
158
+ if (!measurementsStopped) {
159
+ s.value += s.velocity * dt;
160
+ }
151
161
  s.tState = now;
152
162
 
153
163
  // Demand pull — guarantees convergence to the last demanded value for
@@ -155,7 +165,7 @@ void CameraIntegrator::extrapolate(
155
165
  // when measurements have stopped (silence > grace); continuous gesture or
156
166
  // sensor streams always deliver a new update before the grace window
157
167
  // expires, so this term is never active for them.
158
- if (silence > grace && tunables::kDemandPullTauSec > 1.0e-6) {
168
+ if (measurementsStopped && tunables::kDemandPullTauSec > 1.0e-6) {
159
169
  s.value += (1.0 - std::exp(-dt / tunables::kDemandPullTauSec))
160
170
  * (demand - s.value);
161
171
  }
@@ -193,8 +203,20 @@ void CameraIntegrator::setAltitude(double altitudeMeters) {
193
203
  }
194
204
 
195
205
  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
+
196
218
  updateAlphaBeta(alt_, altitudeMeters, now,
197
- tunables::kAlphaAlt, tunables::kBetaAlt, false);
219
+ adaptiveAlpha, tunables::kBetaAlt, false);
198
220
  demand_.altitude = altitudeMeters;
199
221
  }
200
222
 
@@ -341,9 +363,20 @@ CameraParams CameraIntegrator::step() {
341
363
  }
342
364
 
343
365
  void CameraIntegrator::clampActualAltitude(double newAltMsl) {
366
+ const auto now = Clock::now();
344
367
  std::lock_guard<std::mutex> g(mutex_);
345
- alt_.value = newAltMsl;
346
- alt_.velocity = 0.0;
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) {
377
+ alt_.velocity = 0.0;
378
+ }
379
+
347
380
  // Do not touch demand_.altitude — getDemand() should still report what the
348
381
  // user asked for.
349
382
  }
@@ -6,6 +6,7 @@
6
6
  #include "CesiumEngine.hpp"
7
7
  #include "GeoidConverter.hpp"
8
8
 
9
+ #include <iostream>
9
10
 
10
11
  #include <Cesium3DTilesContent/registerAllTileContentTypes.h>
11
12
  #include <Cesium3DTilesSelection/BoundingVolume.h>
@@ -36,6 +37,7 @@
36
37
  #include <algorithm>
37
38
  #include <cmath>
38
39
  #include <cstring>
40
+ #include <limits>
39
41
  #include <thread>
40
42
 
41
43
 
@@ -609,25 +611,21 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
609
611
  const CameraParams camParams = trackTerrainFloor ? camera_.getParams() : CameraParams{};
610
612
  const double camLatRad = glm::radians(camParams.latitude);
611
613
  const double camLonRad = glm::radians(camParams.longitude);
612
-
614
+
613
615
  // Expected ground ellipsoid height at camera location (from geoid).
614
616
  // Used to filter out elevated structures (buildings) during floor sampling.
615
617
  const double expectedGroundEllipsoid = trackTerrainFloor
616
618
  ? mslToEllipsoidMeters(camParams.latitude, camParams.longitude, 0.0)
617
619
  : 0.0;
618
-
620
+
619
621
  float bestTerrainFloor = terrainFloorEllipsoidM_; // carry previous value if no tile matches
620
622
  double bestHorizDistSq = std::numeric_limits<double>::max();
621
623
  bool floorScanFound = false;
622
624
 
623
625
  // Horizontal search radius for floor sampling.
624
- constexpr double kFloorSearchRadiusM = 50.0;
626
+ // Increased to 200m to ensure robust sampling across terrain LODs and camera speeds.
627
+ constexpr double kFloorSearchRadiusM = 200.0;
625
628
  constexpr double kFloorSearchRadiusSq = kFloorSearchRadiusM * kFloorSearchRadiusM;
626
- // Only accept vertices within this tolerance of expected ground (filters out buildings).
627
- constexpr double kGroundToleranceM = 10.0;
628
- // 3D fallback: if no ground-level vertices found, use closest 3D vertex.
629
- double bestDist3D = std::numeric_limits<double>::max();
630
- float bestDist3DFloor = 0.0f;
631
629
 
632
630
  // ── floor-scan cache lookup ─────────────────────────────────────────
633
631
  // Skip the per-vertex scan when the camera has moved less than ~10 cm in
@@ -653,6 +651,10 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
653
651
  // ENU basis at the camera — horizontal distance to each terrain vertex.
654
652
  glm::dvec3 floorEast(1.0, 0.0, 0.0);
655
653
  glm::dvec3 floorNorth(0.0, 1.0, 0.0);
654
+ glm::dvec3 floorScanPos = cameraPos; // Always scan directly below camera
655
+ glm::dvec3 floorScanAheadPos = cameraPos; // Also scan ahead if moving
656
+ bool useLookAhead = false;
657
+
656
658
  if (runFloorScan) {
657
659
  const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
658
660
  const glm::dmat4 enuToEcef =
@@ -660,6 +662,58 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
660
662
  cameraPos, ellipsoid);
661
663
  floorEast = glm::normalize(glm::dvec3(enuToEcef[0]));
662
664
  floorNorth = glm::normalize(glm::dvec3(enuToEcef[1]));
665
+
666
+ // ── Look-ahead terrain sampling ──────────────────────────────────────
667
+ // Sample terrain BOTH below camera AND ahead. Use the maximum height to
668
+ // prevent clipping through terrain between current position and look-ahead.
669
+ if (floorCacheValid_) {
670
+ // Convert previous lat/lon to ECEF to get movement vector
671
+ const double prevLonRad = floorCacheCamLonDeg_ * M_PI / 180.0;
672
+ const double prevLatRad = floorCacheCamLatDeg_ * M_PI / 180.0;
673
+ const glm::dvec3 prevCamPos = ellipsoid.cartographicToCartesian(
674
+ CesiumGeospatial::Cartographic(prevLonRad, prevLatRad, camParams.altitude));
675
+
676
+ // Calculate horizontal movement since last frame
677
+ const glm::dvec3 movement3D = cameraPos - prevCamPos;
678
+ const double moveEast = glm::dot(movement3D, floorEast);
679
+ const double moveNorth = glm::dot(movement3D, floorNorth);
680
+ const double horizMovement = std::sqrt(moveEast * moveEast + moveNorth * moveNorth);
681
+
682
+ // Look ahead by 5x recent movement (minimum 10m, max 100m)
683
+ constexpr double kMinLookAhead = 10.0; // meters
684
+ constexpr double kMaxLookAhead = 100.0; // meters
685
+ constexpr double kLookAheadMultiplier = 5.0;
686
+
687
+ double lookAheadDist = std::clamp(
688
+ horizMovement * kLookAheadMultiplier,
689
+ kMinLookAhead,
690
+ kMaxLookAhead);
691
+
692
+ if (horizMovement > 0.1) { // Only look ahead if actually moving
693
+ // Project sampling point forward in direction of movement
694
+ const glm::dvec3 moveDir = glm::normalize(
695
+ floorEast * moveEast + floorNorth * moveNorth);
696
+ floorScanAheadPos = cameraPos + moveDir * lookAheadDist;
697
+ useLookAhead = true;
698
+ }
699
+ }
700
+ }
701
+
702
+ // Convert floor scan positions to lat/lon for tile bounds checking
703
+ // Camera position (always used)
704
+ double floorScanLonRad = camLonRad;
705
+ double floorScanLatRad = camLatRad;
706
+
707
+ // Look-ahead position (used when moving)
708
+ double floorScanAheadLonRad = camLonRad;
709
+ double floorScanAheadLatRad = camLatRad;
710
+ if (runFloorScan && useLookAhead) {
711
+ const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
712
+ const auto scanCartographic = ellipsoid.cartesianToCartographic(floorScanAheadPos);
713
+ if (scanCartographic) {
714
+ floorScanAheadLonRad = scanCartographic->longitude;
715
+ floorScanAheadLatRad = scanCartographic->latitude;
716
+ }
663
717
  }
664
718
 
665
719
  // ── fast path ────────────────────────────────────────────────────────
@@ -678,65 +732,82 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
678
732
 
679
733
  // Floor scan (run once per res so multi-primitive tiles aren't scanned
680
734
  // twice). Skipped when the P5 cache hit above produced a usable value.
735
+ // Sample terrain at BOTH camera position and look-ahead position.
736
+ // Find lowest terrain at each position, then use the MAXIMUM of the two.
681
737
  if (runFloorScan && c.res != lastScanRes) {
682
738
  lastScanRes = c.res;
683
- const bool inside =
684
- (c.wmTileBounds.x <= static_cast<float>(camLonRad) &&
685
- static_cast<float>(camLonRad) <= c.wmTileBounds.z) &&
686
- (c.wmTileBounds.y <= static_cast<float>(camLatRad) &&
687
- static_cast<float>(camLatRad) <= c.wmTileBounds.w);
688
- if (inside) {
739
+
740
+ // Check if camera position is in tile bounds
741
+ const bool camInside =
742
+ (c.wmTileBounds.x <= static_cast<float>(floorScanLonRad) &&
743
+ static_cast<float>(floorScanLonRad) <= c.wmTileBounds.z) &&
744
+ (c.wmTileBounds.y <= static_cast<float>(floorScanLatRad) &&
745
+ static_cast<float>(floorScanLatRad) <= c.wmTileBounds.w);
746
+
747
+ // Check if look-ahead position is in tile bounds (when moving)
748
+ const bool aheadInside = useLookAhead &&
749
+ (c.wmTileBounds.x <= static_cast<float>(floorScanAheadLonRad) &&
750
+ static_cast<float>(floorScanAheadLonRad) <= c.wmTileBounds.z) &&
751
+ (c.wmTileBounds.y <= static_cast<float>(floorScanAheadLatRad) &&
752
+ static_cast<float>(floorScanAheadLatRad) <= c.wmTileBounds.w);
753
+
754
+ if (camInside || aheadInside) {
755
+ // Track lowest terrain at each position separately
756
+ float terrainAtCam = std::numeric_limits<float>::max();
757
+ float terrainAhead = std::numeric_limits<float>::max();
758
+ bool foundAtCam = false;
759
+ bool foundAhead = false;
760
+
689
761
  for (const auto& prim : c.res->primitives) {
690
762
  if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
691
763
  const size_t vc = prim.localPositions.size();
764
+
692
765
  for (size_t vi = 0; vi < vc; ++vi) {
693
- const glm::dvec3 vECEF =
694
- glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
695
- const glm::dvec3 delta = vECEF - cameraPos;
696
- const double de = glm::dot(delta, floorEast);
697
- const double dn = glm::dot(delta, floorNorth);
698
- const double horizSq = de * de + dn * dn;
699
- const float vAlt = prim.altitudes[vi];
700
-
701
- // Primary: horizontal search within radius, only accept vertices near ground level.
702
- // This filters out elevated structures (buildings) in urban areas.
703
- if (horizSq <= kFloorSearchRadiusSq) {
704
- const double heightAboveGround = static_cast<double>(vAlt) - expectedGroundEllipsoid;
705
- if (heightAboveGround >= -kGroundToleranceM &&
706
- heightAboveGround <= kGroundToleranceM) {
707
- if (!floorScanFound || vAlt < bestTerrainFloor) {
708
- bestTerrainFloor = vAlt;
709
- bestHorizDistSq = horizSq;
710
- floorScanFound = true;
766
+ const glm::dvec3 vECEF = glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
767
+ const float vAlt = prim.altitudes[vi];
768
+
769
+ // Sample terrain at camera position - find LOWEST
770
+ if (camInside) {
771
+ const glm::dvec3 deltaCam = vECEF - floorScanPos;
772
+ const double deCam = glm::dot(deltaCam, floorEast);
773
+ const double dnCam = glm::dot(deltaCam, floorNorth);
774
+ const double horizSqCam = deCam * deCam + dnCam * dnCam;
775
+
776
+ if (horizSqCam <= kFloorSearchRadiusSq) {
777
+ if (!foundAtCam || vAlt < terrainAtCam) {
778
+ terrainAtCam = vAlt;
779
+ foundAtCam = true;
711
780
  }
712
781
  }
713
782
  }
714
-
715
- // Fallback: track closest 3D vertex for sparse/ocean scenarios.
716
- const double dist3D = glm::length(delta);
717
- if (dist3D < bestDist3D) {
718
- bestDist3D = dist3D;
719
- bestDist3DFloor = vAlt;
783
+
784
+ // Sample terrain at look-ahead position - find LOWEST
785
+ if (aheadInside) {
786
+ const glm::dvec3 deltaAhead = vECEF - floorScanAheadPos;
787
+ const double deAhead = glm::dot(deltaAhead, floorEast);
788
+ const double dnAhead = glm::dot(deltaAhead, floorNorth);
789
+ const double horizSqAhead = deAhead * deAhead + dnAhead * dnAhead;
790
+
791
+ if (horizSqAhead <= kFloorSearchRadiusSq) {
792
+ if (!foundAhead || vAlt < terrainAhead) {
793
+ terrainAhead = vAlt;
794
+ foundAhead = true;
795
+ }
796
+ }
720
797
  }
721
798
  }
722
799
  }
723
- }
724
- // Fallback: if no ground-level vertices found, use expected ground from geoid.
725
- // This handles urban areas where terrain tiles only contain building structures.
726
- if (!floorScanFound) {
727
- if (bestDist3D < std::numeric_limits<double>::max()) {
728
- // Use closest 3D vertex only if it's near ground level
729
- const double heightAboveGround = static_cast<double>(bestDist3DFloor) - expectedGroundEllipsoid;
730
- if (heightAboveGround >= -kGroundToleranceM &&
731
- heightAboveGround <= kGroundToleranceM) {
732
- bestTerrainFloor = bestDist3DFloor;
733
- floorScanFound = true;
734
- }
735
- }
736
- // Ultimate fallback: use geoid-based ground estimate
737
- if (!floorScanFound) {
738
- bestTerrainFloor = static_cast<float>(expectedGroundEllipsoid);
739
- floorScanFound = true;
800
+
801
+ // Use the MAXIMUM of the two terrain samples (prevents clipping through either)
802
+ if (foundAtCam && foundAhead) {
803
+ bestTerrainFloor = std::max(terrainAtCam, terrainAhead);
804
+ floorScanFound = true;
805
+ } else if (foundAtCam) {
806
+ bestTerrainFloor = terrainAtCam;
807
+ floorScanFound = true;
808
+ } else if (foundAhead) {
809
+ bestTerrainFloor = terrainAhead;
810
+ floorScanFound = true;
740
811
  }
741
812
  }
742
813
  }
@@ -824,62 +895,71 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
824
895
  static_cast<float>(rect.getEast()),
825
896
  static_cast<float>(rect.getNorth()));
826
897
 
827
- // ── Terrain floor: find nearest vertex to camera nadir ────────────
828
- // Suppressed when the cache returned a usable value for the
829
- // current (signature, camLon, camLat) tuple.
830
- if (runFloorScan &&
831
- rect.contains(CesiumGeospatial::Cartographic(camLonRad, camLatRad, 0.0))) {
898
+ // ── Terrain floor: sample at both camera and look-ahead positions ────
899
+ // Find lowest terrain at each position, then use the MAXIMUM of the two.
900
+ // Suppressed when the cache returned a usable value.
901
+ const bool camInside = runFloorScan &&
902
+ rect.contains(CesiumGeospatial::Cartographic(floorScanLonRad, floorScanLatRad, 0.0));
903
+ const bool aheadInside = runFloorScan && useLookAhead &&
904
+ rect.contains(CesiumGeospatial::Cartographic(floorScanAheadLonRad, floorScanAheadLatRad, 0.0));
905
+
906
+ if (camInside || aheadInside) {
907
+ // Track lowest terrain at each position separately
908
+ float terrainAtCam = std::numeric_limits<float>::max();
909
+ float terrainAhead = std::numeric_limits<float>::max();
910
+ bool foundAtCam = false;
911
+ bool foundAhead = false;
912
+
832
913
  for (const auto& prim : res->primitives) {
833
914
  if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
834
915
  const size_t vertexCount = prim.localPositions.size();
916
+
835
917
  for (size_t vi = 0; vi < vertexCount; ++vi) {
836
- const glm::dvec3 vECEF =
837
- glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
838
- const glm::dvec3 delta = vECEF - cameraPos;
839
- const double de = glm::dot(delta, floorEast);
840
- const double dn = glm::dot(delta, floorNorth);
841
- const double horizSq = de * de + dn * dn;
842
- const float vAlt = prim.altitudes[vi];
843
-
844
- // Primary: horizontal search within radius, only accept vertices near ground level.
845
- // This filters out elevated structures (buildings) in urban areas.
846
- if (horizSq <= kFloorSearchRadiusSq) {
847
- const double heightAboveGround = static_cast<double>(vAlt) - expectedGroundEllipsoid;
848
- if (heightAboveGround >= -kGroundToleranceM &&
849
- heightAboveGround <= kGroundToleranceM) {
850
- if (!floorScanFound || vAlt < bestTerrainFloor) {
851
- bestTerrainFloor = vAlt;
852
- bestHorizDistSq = horizSq;
853
- floorScanFound = true;
918
+ const glm::dvec3 vECEF = glm::dvec3(prim.localPositions[vi]) + prim.rtcCenter;
919
+ const float vAlt = prim.altitudes[vi];
920
+
921
+ // Sample terrain at camera position - find LOWEST
922
+ if (camInside) {
923
+ const glm::dvec3 deltaCam = vECEF - floorScanPos;
924
+ const double deCam = glm::dot(deltaCam, floorEast);
925
+ const double dnCam = glm::dot(deltaCam, floorNorth);
926
+ const double horizSqCam = deCam * deCam + dnCam * dnCam;
927
+
928
+ if (horizSqCam <= kFloorSearchRadiusSq) {
929
+ if (!foundAtCam || vAlt < terrainAtCam) {
930
+ terrainAtCam = vAlt;
931
+ foundAtCam = true;
854
932
  }
855
933
  }
856
934
  }
857
-
858
- // Fallback: track closest 3D vertex for sparse/ocean scenarios.
859
- const double dist3D = glm::length(delta);
860
- if (dist3D < bestDist3D) {
861
- bestDist3D = dist3D;
862
- bestDist3DFloor = vAlt;
935
+
936
+ // Sample terrain at look-ahead position - find LOWEST
937
+ if (aheadInside) {
938
+ const glm::dvec3 deltaAhead = vECEF - floorScanAheadPos;
939
+ const double deAhead = glm::dot(deltaAhead, floorEast);
940
+ const double dnAhead = glm::dot(deltaAhead, floorNorth);
941
+ const double horizSqAhead = deAhead * deAhead + dnAhead * dnAhead;
942
+
943
+ if (horizSqAhead <= kFloorSearchRadiusSq) {
944
+ if (!foundAhead || vAlt < terrainAhead) {
945
+ terrainAhead = vAlt;
946
+ foundAhead = true;
947
+ }
948
+ }
863
949
  }
864
950
  }
865
951
  }
866
- // Fallback: if no ground-level vertices found, use expected ground from geoid.
867
- // This handles urban areas where terrain tiles only contain building structures.
868
- if (!floorScanFound) {
869
- if (bestDist3D < std::numeric_limits<double>::max()) {
870
- // Use closest 3D vertex only if it's near ground level
871
- const double heightAboveGround = static_cast<double>(bestDist3DFloor) - expectedGroundEllipsoid;
872
- if (heightAboveGround >= -kGroundToleranceM &&
873
- heightAboveGround <= kGroundToleranceM) {
874
- bestTerrainFloor = bestDist3DFloor;
875
- floorScanFound = true;
876
- }
877
- }
878
- // Ultimate fallback: use geoid-based ground estimate
879
- if (!floorScanFound) {
880
- bestTerrainFloor = static_cast<float>(expectedGroundEllipsoid);
881
- floorScanFound = true;
882
- }
952
+
953
+ // Use the MAXIMUM of the two terrain samples (prevents clipping through either)
954
+ if (foundAtCam && foundAhead) {
955
+ bestTerrainFloor = std::max(terrainAtCam, terrainAhead);
956
+ floorScanFound = true;
957
+ } else if (foundAtCam) {
958
+ bestTerrainFloor = terrainAtCam;
959
+ floorScanFound = true;
960
+ } else if (foundAhead) {
961
+ bestTerrainFloor = terrainAhead;
962
+ floorScanFound = true;
883
963
  }
884
964
  }
885
965
  }
@@ -383,19 +383,29 @@
383
383
  // `nowSeconds` is still used below for the FPS-EMA dt only.
384
384
  auto actual = _integrator->step();
385
385
 
386
- // ── Terrain floor clamp ───────────────────────────────────────────────
387
- // Uses terrain floor from the previous frame (one-frame lag is
388
- // imperceptible at 60 fps). Skipped when minAltitudeAboveTerrain == 0.
386
+ // ── Terrain floor following using integrator's alpha-beta smoothing ───
387
+ // Set demand altitude to terrain floor, let the integrator's alpha-beta
388
+ // tracker smoothly interpolate. The tracker already provides adaptive
389
+ // exponential easing (alpha=0.6, beta=0.05 in EngineTunables.hpp).
389
390
  const float minAbove = _engine->getConfig().minAltitudeAboveTerrain;
390
391
  if (minAbove > 0.0f && _engine->terrainFloorKnown()) {
391
392
  const double geoidN =
392
393
  reactnativecesium::mslToEllipsoidMeters(actual.latitude, actual.longitude, 0.0);
393
- const double floorMsl =
394
- static_cast<double>(_engine->terrainFloorEllipsoidMeters()) - geoidN;
394
+ const double floorEllipsoid = static_cast<double>(_engine->terrainFloorEllipsoidMeters());
395
+ const double floorMsl = floorEllipsoid - geoidN;
395
396
  const double minMsl = floorMsl + static_cast<double>(minAbove);
397
+
398
+ // Continuously update demand to follow terrain
399
+ const double demandAlt = _integrator->getDemand().altitude;
400
+ if (demandAlt <= 0.0) {
401
+ _integrator->setAltitude(minMsl);
402
+ actual = _integrator->getActual();
403
+ }
404
+
405
+ // Safety floor: hard clamp if already below terrain (shouldn't happen with look-ahead)
396
406
  if (actual.altitude < minMsl) {
397
407
  _integrator->clampActualAltitude(minMsl);
398
- actual.altitude = minMsl;
408
+ actual = _integrator->getActual();
399
409
  }
400
410
  }
401
411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -64,7 +64,7 @@
64
64
  "node": ">=18"
65
65
  },
66
66
  "dependencies": {
67
- "react-native-nitro-modules": "^0.35.7"
67
+ "react-native-nitro-modules": "^0.35.9"
68
68
  },
69
69
  "publishConfig": {
70
70
  "access": "public",
@@ -77,7 +77,7 @@
77
77
  "@types/jest": "^29.5.14",
78
78
  "@types/react": "19.2.14",
79
79
  "conventional-changelog-conventionalcommits": "^9.3.1",
80
- "nitrogen": "0.35.7",
80
+ "nitrogen": "0.35.9",
81
81
  "react": "19.2.3",
82
82
  "react-native": "0.85.3",
83
83
  "react-native-builder-bob": "^0.41.0",
@@ -5,4 +5,4 @@
5
5
  */
6
6
 
7
7
  /** Git tag of CesiumGS/cesium-native to clone and build. */
8
- export const CESIUM_NATIVE_REF = 'v0.60.0'
8
+ export const CESIUM_NATIVE_REF = 'v0.61.0'