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
@@ -4,7 +4,7 @@
4
4
  #endif
5
5
 
6
6
  #include "CesiumEngine.hpp"
7
- #include "GeoidConverter.hpp"
7
+ #include "TerrainHeight.hpp"
8
8
 
9
9
  #include <iostream>
10
10
 
@@ -37,7 +37,6 @@
37
37
  #include <algorithm>
38
38
  #include <cmath>
39
39
  #include <cstring>
40
- #include <limits>
41
40
  #include <thread>
42
41
 
43
42
 
@@ -213,6 +212,21 @@ void CesiumEngine::shutdown() {
213
212
  creditSystem_.reset();
214
213
  }
215
214
 
215
+ void CesiumEngine::drainAsyncWork() {
216
+ // Worker tasks post main-thread continuations and return. Those
217
+ // continuations (Ion raster-overlay loads in particular) can queue more
218
+ // worker tasks, so pump both sides until the pool stays idle.
219
+ for (int pass = 0; pass < 16; ++pass) {
220
+ if (taskProcessor_) {
221
+ taskProcessor_->waitUntilIdle();
222
+ }
223
+ asyncSystem_.dispatchMainThreadTasks();
224
+ }
225
+ if (taskProcessor_) {
226
+ taskProcessor_->waitUntilIdle();
227
+ }
228
+ }
229
+
216
230
  void CesiumEngine::updateConfig(const EngineConfig& config) {
217
231
  // Only token / asset id changes force a full tileset rebuild. Everything
218
232
  // else is mutated in place via tileset_->getOptions() to avoid the very
@@ -404,26 +418,31 @@ void CesiumEngine::applyImageryOverlay(int64_t targetAssetId) {
404
418
  lastDrawSignature_ = 0;
405
419
  stableSigFrames_ = 0;
406
420
  cachedDraws_.clear();
407
- // floor cache is keyed on signature — drop it too so the next frame
408
- // re-scans before establishing a new entry.
409
- floorCacheValid_ = false;
421
+ heightCache_.valid = false;
410
422
  }
411
423
 
412
424
  void CesiumEngine::destroyTileset() {
413
- currentOverlay_ = nullptr;
414
- currentImageryAssetId_ = 1;
415
- tileset_.reset();
416
- if (taskProcessor_) {
417
- taskProcessor_->waitUntilIdle();
425
+ // Detach the imagery overlay while this engine still owns it. Dropping
426
+ // currentOverlay_ before in-flight IonRasterOverlay tasks finish frees the
427
+ // overlay out from under their destructors (heap corruption).
428
+ if (tileset_ && currentOverlay_) {
429
+ tileset_->getOverlays().remove(currentOverlay_);
418
430
  }
419
- // every TileGPUResources* baked into cachedDraws_ has just been freed.
420
- // Clearing the cache guarantees no dangling pointer survives into the next
421
- // updateFrame and forces a clean slow-path rebuild.
431
+ drainAsyncWork();
432
+
433
+ // Drop draw-cache pointers before the tileset frees TileGPUResources.
422
434
  lastDrawSignature_ = 0;
423
435
  stableSigFrames_ = 0;
424
436
  cachedDraws_.clear();
425
- // P5: the cached value was keyed on TileGPUResources we just freed.
426
- floorCacheValid_ = false;
437
+ clearResidentTerrain();
438
+
439
+ tileset_.reset();
440
+ currentImageryAssetId_ = 1;
441
+ // Tileset destruction schedules further async cleanup. Drain that while
442
+ // this engine still owns the overlay, then release it. Dropping the
443
+ // overlay first frees it under those continuations.
444
+ drainAsyncWork();
445
+ currentOverlay_ = nullptr;
427
446
  }
428
447
 
429
448
  void CesiumEngine::setImageryAssetId(int64_t assetId) {
@@ -445,6 +464,126 @@ inline uint64_t fnv1a64Mix(uint64_t h, uint64_t v) {
445
464
  }
446
465
  } // namespace
447
466
 
467
+ void CesiumEngine::clearResidentTerrain() {
468
+ residentTiles_.clear();
469
+ residentSignature_ = 0;
470
+ heightCache_.valid = false;
471
+ }
472
+
473
+ bool CesiumEngine::sampleTerrainEllipsoidMeters(double latitudeDeg,
474
+ double longitudeDeg,
475
+ float& outEllipsoidMeters) {
476
+ if (residentTiles_.empty() || !std::isfinite(latitudeDeg) || !std::isfinite(longitudeDeg)) {
477
+ return false;
478
+ }
479
+
480
+ // ~0.5 m cell. A hover reuses the sample; moving to the next cell rescans.
481
+ constexpr double kCellDeg = 4.5e-6;
482
+ const int latCell = static_cast<int>(std::floor(latitudeDeg / kCellDeg));
483
+ const int lonCell = static_cast<int>(std::floor(longitudeDeg / kCellDeg));
484
+ if (heightCache_.valid &&
485
+ heightCache_.signature == residentSignature_ &&
486
+ heightCache_.latCell == latCell &&
487
+ heightCache_.lonCell == lonCell) {
488
+ outEllipsoidMeters = heightCache_.height;
489
+ return true;
490
+ }
491
+
492
+ const double latRad = glm::radians(latitudeDeg);
493
+ const double lonRad = glm::radians(longitudeDeg);
494
+
495
+ const ResidentTerrainTile* tried[8] = {};
496
+ int triedCount = 0;
497
+
498
+ const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
499
+ const glm::dvec3 origin = ellipsoid.cartographicToCartesian(
500
+ CesiumGeospatial::Cartographic(lonRad, latRad, 0.0));
501
+ const glm::dmat4 enuToEcef =
502
+ CesiumGeospatial::GlobeTransforms::eastNorthUpToFixedFrame(origin, ellipsoid);
503
+ const glm::dvec3 east = glm::normalize(glm::dvec3(enuToEcef[0]));
504
+ const glm::dvec3 north = glm::normalize(glm::dvec3(enuToEcef[1]));
505
+
506
+ auto contains = [&](const ResidentTerrainTile& tile) {
507
+ if (latRad < tile.south || latRad > tile.north) return false;
508
+ if (tile.west <= tile.east) return lonRad >= tile.west && lonRad <= tile.east;
509
+ return lonRad >= tile.west || lonRad <= tile.east;
510
+ };
511
+
512
+ auto sampleTile = [&](const ResidentTerrainTile& tile, float& outHeight) {
513
+ if (!tile.res) return false;
514
+ bool found = false;
515
+ float best = 0.0f;
516
+ for (const auto& prim : tile.res->primitives) {
517
+ const size_t nVerts = prim.localPositions.size();
518
+ if (nVerts == 0 || prim.altitudes.size() != nVerts) continue;
519
+ const size_t nTri = prim.indices.size() / 3;
520
+ for (size_t t = 0; t < nTri; ++t) {
521
+ const uint32_t i0 = prim.indices[t * 3];
522
+ const uint32_t i1 = prim.indices[t * 3 + 1];
523
+ const uint32_t i2 = prim.indices[t * 3 + 2];
524
+ if (i0 >= nVerts || i1 >= nVerts || i2 >= nVerts) continue;
525
+
526
+ auto project = [&](uint32_t i, double& e, double& n) {
527
+ const glm::dvec3 world = glm::dvec3(prim.localPositions[i]) + prim.rtcCenter;
528
+ const glm::dvec3 delta = world - origin;
529
+ e = glm::dot(delta, east);
530
+ n = glm::dot(delta, north);
531
+ };
532
+ double e0, n0, e1, n1, e2, n2;
533
+ project(i0, e0, n0);
534
+ project(i1, e1, n1);
535
+ project(i2, e2, n2);
536
+
537
+ float h = 0.0f;
538
+ if (!horizontalTriangleHeight(0.0, 0.0,
539
+ e0, n0, prim.altitudes[i0],
540
+ e1, n1, prim.altitudes[i1],
541
+ e2, n2, prim.altitudes[i2],
542
+ h)) {
543
+ continue;
544
+ }
545
+ if (!found || h > best) {
546
+ best = h;
547
+ found = true;
548
+ }
549
+ }
550
+ }
551
+ if (!found) return false;
552
+ outHeight = best;
553
+ return true;
554
+ };
555
+
556
+ // Finest containing tile first. If its mesh misses the point (a crack
557
+ // between tiles), try the next-finest. At most a handful of LODs overlap.
558
+ for (int attempt = 0; attempt < 8; ++attempt) {
559
+ const ResidentTerrainTile* best = nullptr;
560
+ for (const auto& tile : residentTiles_) {
561
+ if (!contains(tile)) continue;
562
+ bool skipped = false;
563
+ for (int i = 0; i < triedCount; ++i) {
564
+ if (tried[i] == &tile) { skipped = true; break; }
565
+ }
566
+ if (skipped) continue;
567
+ if (!best || tile.geometricError < best->geometricError) best = &tile;
568
+ }
569
+ if (!best) break;
570
+
571
+ float height = 0.0f;
572
+ if (sampleTile(*best, height)) {
573
+ heightCache_.valid = true;
574
+ heightCache_.tile = best->res;
575
+ heightCache_.latCell = latCell;
576
+ heightCache_.lonCell = lonCell;
577
+ heightCache_.signature = residentSignature_;
578
+ heightCache_.height = height;
579
+ outEllipsoidMeters = height;
580
+ return true;
581
+ }
582
+ if (triedCount < 8) tried[triedCount++] = best;
583
+ }
584
+ return false;
585
+ }
586
+
448
587
  void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
449
588
  result.localPositions.clear();
450
589
  result.altitudes.clear();
@@ -475,6 +614,7 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
475
614
  lastDrawSignature_ = 0;
476
615
  stableSigFrames_ = 0;
477
616
  cachedDraws_.clear();
617
+ clearResidentTerrain();
478
618
  lifecycle_.advanceFrame();
479
619
  return;
480
620
  }
@@ -604,116 +744,35 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
604
744
  stableSigFrames_ > tunables::kMaxFramesInFlight &&
605
745
  !cachedDraws_.empty();
606
746
 
607
- // ── Terrain floor tracking (for minAltitudeAboveTerrain clamp) ───────────
608
- // Computed once before the tile loop and updated per-tile inside it.
609
- // Only active when the feature is enabled to avoid any overhead otherwise.
610
- const bool trackTerrainFloor = (config_.minAltitudeAboveTerrain > 0.0f);
611
- const CameraParams camParams = trackTerrainFloor ? camera_.getParams() : CameraParams{};
612
- const double camLatRad = glm::radians(camParams.latitude);
613
- const double camLonRad = glm::radians(camParams.longitude);
614
-
615
- // Expected ground ellipsoid height at camera location (from geoid).
616
- // Used to filter out elevated structures (buildings) during floor sampling.
617
- const double expectedGroundEllipsoid = trackTerrainFloor
618
- ? mslToEllipsoidMeters(camParams.latitude, camParams.longitude, 0.0)
619
- : 0.0;
620
-
621
- float bestTerrainFloor = terrainFloorEllipsoidM_; // carry previous value if no tile matches
622
- double bestHorizDistSq = std::numeric_limits<double>::max();
623
- bool floorScanFound = false;
624
-
625
- // Horizontal search radius for floor sampling.
626
- // Increased to 200m to ensure robust sampling across terrain LODs and camera speeds.
627
- constexpr double kFloorSearchRadiusM = 200.0;
628
- constexpr double kFloorSearchRadiusSq = kFloorSearchRadiusM * kFloorSearchRadiusM;
629
-
630
- // ── floor-scan cache lookup ─────────────────────────────────────────
631
- // Skip the per-vertex scan when the camera has moved less than ~10 cm in
632
- // both lat and lon AND the visible primitive set is unchanged (same
633
- // geometry signature). The cached value is only trusted for non-zero
634
- // signatures — sig==0 means a fade or rebuild is in flight, in which case
635
- // we always re-scan and refresh the cache afterwards.
636
- bool floorScanFromCache = false;
637
- if (trackTerrainFloor && floorCacheValid_ && signature != 0ULL &&
638
- signature == floorCacheSignature_) {
639
- const double dLon = std::abs(camParams.longitude - floorCacheCamLonDeg_);
640
- const double dLat = std::abs(camParams.latitude - floorCacheCamLatDeg_);
641
- if (dLon < 1e-6 && dLat < 1e-6) {
642
- bestTerrainFloor = floorCacheValue_;
643
- floorScanFromCache = true;
644
- }
645
- }
646
- // When tracking is off, suppress the per-tile scan branches (legacy
647
- // behaviour already gated on trackTerrainFloor; this just keeps the cache
648
- // semantics aligned).
649
- const bool runFloorScan = trackTerrainFloor && !floorScanFromCache;
650
-
651
- // ENU basis at the camera — horizontal distance to each terrain vertex.
652
- glm::dvec3 floorEast(1.0, 0.0, 0.0);
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
-
658
- if (runFloorScan) {
659
- const auto& ellipsoid = CesiumGeospatial::Ellipsoid::WGS84;
660
- const glm::dmat4 enuToEcef =
661
- CesiumGeospatial::GlobeTransforms::eastNorthUpToFixedFrame(
662
- cameraPos, ellipsoid);
663
- floorEast = glm::normalize(glm::dvec3(enuToEcef[0]));
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;
747
+ // Capture the tiles this frame actually drew. The next render samples
748
+ // height from this list before updateViewGroup can release it. Bounds and
749
+ // geometric error only — no per-vertex work on the draw path.
750
+ if (noVisibleTerrain) {
751
+ clearResidentTerrain();
752
+ } else {
753
+ residentTiles_.clear();
754
+ residentTiles_.reserve(updateResult.tilesToRenderThisFrame.size());
755
+ for (const auto& tile : updateResult.tilesToRenderThisFrame) {
756
+ if (!tile) continue;
757
+ const auto* rc = tile->getContent().getRenderContent();
758
+ if (!rc) continue;
759
+ const auto* res = static_cast<const TileGPUResources*>(rc->getRenderResources());
760
+ if (!res || res->primitives.empty()) continue;
761
+ const CesiumGeospatial::BoundingRegion* region =
762
+ Cesium3DTilesSelection::getBoundingRegionFromBoundingVolume(tile->getBoundingVolume());
763
+ if (!region) continue;
764
+ const auto& rect = region->getRectangle();
765
+ ResidentTerrainTile entry;
766
+ entry.res = res;
767
+ entry.geometricError = tile->getGeometricError();
768
+ entry.west = rect.getWest();
769
+ entry.south = rect.getSouth();
770
+ entry.east = rect.getEast();
771
+ entry.north = rect.getNorth();
772
+ residentTiles_.push_back(entry);
716
773
  }
774
+ residentSignature_ = signature;
775
+ if (heightCache_.signature != residentSignature_) heightCache_.valid = false;
717
776
  }
718
777
 
719
778
  // ── fast path ────────────────────────────────────────────────────────
@@ -726,92 +785,9 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
726
785
  // detect this via result.geometrySignature and use their cached slot data.
727
786
  if (canFastPath) {
728
787
  result.draws.reserve(cachedDraws_.size());
729
- const TileGPUResources* lastScanRes = nullptr;
730
788
  for (const auto& c : cachedDraws_) {
731
789
  lifecycle_.stampTileUsed(const_cast<TileGPUResources*>(c.res));
732
790
 
733
- // Floor scan (run once per res so multi-primitive tiles aren't scanned
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.
737
- if (runFloorScan && c.res != lastScanRes) {
738
- lastScanRes = c.res;
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
-
761
- for (const auto& prim : c.res->primitives) {
762
- if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
763
- const size_t vc = prim.localPositions.size();
764
-
765
- for (size_t vi = 0; vi < vc; ++vi) {
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;
780
- }
781
- }
782
- }
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
- }
797
- }
798
- }
799
- }
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;
811
- }
812
- }
813
- }
814
-
815
791
  const glm::dvec3 rtcCamRel = c.rtcCenter - glm::dvec3(cameraPos);
816
792
  const glm::dmat4 translateD = glm::translate(glm::dmat4(1.0), rtcCamRel);
817
793
 
@@ -845,20 +821,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
845
821
  result.cameraLon = paramsFP.longitude;
846
822
  result.cameraAlt = paramsFP.altitude;
847
823
 
848
- if (trackTerrainFloor && (floorScanFound || floorScanFromCache)) {
849
- terrainFloorEllipsoidM_ = bestTerrainFloor;
850
- terrainFloorValid_ = true;
851
- // P5: refresh the floor cache on a fresh scan; skip when we hit the
852
- // cache (the stored value already reflects this signature).
853
- if (runFloorScan && signature != 0ULL) {
854
- floorCacheValid_ = true;
855
- floorCacheSignature_ = signature;
856
- floorCacheCamLonDeg_ = camParams.longitude;
857
- floorCacheCamLatDeg_ = camParams.latitude;
858
- floorCacheValue_ = bestTerrainFloor;
859
- }
860
- }
861
-
862
824
  lifecycle_.advanceFrame();
863
825
  return;
864
826
  }
@@ -894,74 +856,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
894
856
  static_cast<float>(rect.getSouth()),
895
857
  static_cast<float>(rect.getEast()),
896
858
  static_cast<float>(rect.getNorth()));
897
-
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
-
913
- for (const auto& prim : res->primitives) {
914
- if (prim.altitudes.empty() || prim.localPositions.empty()) continue;
915
- const size_t vertexCount = prim.localPositions.size();
916
-
917
- for (size_t vi = 0; vi < vertexCount; ++vi) {
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;
932
- }
933
- }
934
- }
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
- }
949
- }
950
- }
951
- }
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;
963
- }
964
- }
965
859
  }
966
860
 
967
861
  for (const auto& prim : res->primitives) {
@@ -1209,29 +1103,6 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
1209
1103
  result.cameraLon = params.longitude;
1210
1104
  result.cameraAlt = params.altitude;
1211
1105
 
1212
- if (trackTerrainFloor && (floorScanFound || floorScanFromCache)) {
1213
- terrainFloorEllipsoidM_ = bestTerrainFloor;
1214
- terrainFloorValid_ = true;
1215
- // refresh the floor cache on a fresh scan (slow path always runs a
1216
- // scan unless the cache short-circuit fired above). Only memoise when
1217
- // the geometry signature is stable enough to key on.
1218
- if (runFloorScan && signature != 0ULL) {
1219
- floorCacheValid_ = true;
1220
- floorCacheSignature_ = signature;
1221
- floorCacheCamLonDeg_ = camParams.longitude;
1222
- floorCacheCamLatDeg_ = camParams.latitude;
1223
- floorCacheValue_ = bestTerrainFloor;
1224
- } else if (!runFloorScan) {
1225
- // Cache hit on the slow path is unusual but possible (e.g. fade
1226
- // started this frame after a stable cache was warm). Nothing to do
1227
- // — terrainFloorEllipsoidM_ is already the cached value.
1228
- } else {
1229
- // Cannot key on signature=0; invalidate so the next stable frame
1230
- // re-populates the cache deterministically.
1231
- floorCacheValid_ = false;
1232
- }
1233
- }
1234
-
1235
1106
  lifecycle_.advanceFrame();
1236
1107
  }
1237
1108