react-native-cesium 1.2.18 → 1.2.19

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 (42) hide show
  1. package/README.md +1 -1
  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 +5 -15
  6. package/android/src/main/cpp/CesiumBridgeAndroid.h +2 -0
  7. package/cpp/engine/CameraIntegrator.cpp +82 -47
  8. package/cpp/engine/CameraIntegrator.hpp +27 -8
  9. package/cpp/engine/CesiumEngine.cpp +152 -304
  10. package/cpp/engine/CesiumEngine.hpp +38 -28
  11. package/cpp/engine/EngineTunables.hpp +6 -5
  12. package/cpp/engine/GltfToMesh.cpp +14 -5
  13. package/cpp/engine/ResourcePreparer.cpp +3 -3
  14. package/cpp/engine/ResourcePreparer.hpp +1 -1
  15. package/cpp/engine/TerrainFloor.hpp +70 -0
  16. package/cpp/engine/TerrainHeight.hpp +113 -0
  17. package/cpp/engine/test/CameraIntegratorTest.cpp +264 -0
  18. package/ios/CesiumBridge.mm +7 -27
  19. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +7 -6
  20. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  21. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +1 -1
  22. package/nitrogen/generated/android/c++/JCameraState.hpp +1 -1
  23. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +1 -1
  24. package/nitrogen/generated/android/c++/JQuaternion.hpp +1 -1
  25. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +177 -131
  26. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.hpp +6 -1
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +1 -1
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +1 -1
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CameraState.kt +0 -2
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CesiumMetrics.kt +0 -2
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +2 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +1 -1
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewManager.kt +26 -10
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewStateUpdater.kt +2 -2
  35. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +211 -161
  36. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +34 -335
  37. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +107 -77
  38. package/package.json +12 -12
  39. package/scripts/cesium/config.mjs +1 -1
  40. package/scripts/cesium/vcpkg-triplets/arm64-ios-simulator.cmake +3 -0
  41. package/scripts/cesium/vcpkg-triplets/arm64-ios.cmake +4 -0
  42. package/src/specs/cesium-view.nitro.ts +7 -6
@@ -10,6 +10,7 @@
10
10
 
11
11
  #include <cmath>
12
12
  #include <cstring>
13
+ #include <optional>
13
14
 
14
15
  namespace reactnativecesium {
15
16
 
@@ -64,12 +65,17 @@ GltfToMesh::convert(const CesiumGltf::Model& model,
64
65
 
65
66
  const CesiumGltf::PositionAccessorType posAccessor =
66
67
  CesiumGltf::getPositionAccessorView(gltf, primitive);
67
- if (posAccessor.status() != CesiumGltf::AccessorViewStatus::Valid ||
68
- posAccessor.size() <= 0)
68
+ const CesiumGltf::AccessorViewStatus posStatus =
69
+ std::visit(CesiumGltf::StatusFromAccessor{}, posAccessor);
70
+ if (posStatus != CesiumGltf::AccessorViewStatus::Valid)
71
+ return;
72
+
73
+ const int64_t vertexCount =
74
+ std::visit(CesiumGltf::CountFromAccessor{}, posAccessor);
75
+ if (vertexCount <= 0)
69
76
  return;
70
77
 
71
78
  const glm::dmat4 worldTransform = rootTransform * nodeTransform;
72
- const int64_t vertexCount = posAccessor.size();
73
79
 
74
80
  // RTC centre = translation column of the world transform.
75
81
  // After applyRtcCenter this is the tile's ECEF centre embedded by
@@ -82,8 +88,11 @@ GltfToMesh::convert(const CesiumGltf::Model& model,
82
88
  prim.altitudes.reserve(static_cast<size_t>(vertexCount));
83
89
 
84
90
  for (int64_t i = 0; i < vertexCount; ++i) {
85
- const auto& p = posAccessor[i];
86
- const glm::dvec3 gltfLocal(p.value[0], p.value[1], p.value[2]);
91
+ const std::optional<glm::dvec3> pos = std::visit(
92
+ CesiumGltf::PositionFromAccessor{i}, posAccessor);
93
+ if (!pos)
94
+ continue;
95
+ const glm::dvec3 gltfLocal = *pos;
87
96
  // Absolute ECEF in double precision.
88
97
  const glm::dvec3 ecef(worldTransform * glm::dvec4(gltfLocal, 1.0));
89
98
  // Tile-local position (small float, retains full float32 precision
@@ -1,7 +1,7 @@
1
1
  #include "ResourcePreparer.hpp"
2
2
 
3
3
  #include <Cesium3DTilesSelection/Tile.h>
4
- #include <CesiumGltf/ImageAsset.h>
4
+ #include <CesiumImage/ImageAsset.h>
5
5
  #include <CesiumGltf/Model.h>
6
6
  #include <CesiumUtility/JsonValue.h>
7
7
 
@@ -117,12 +117,12 @@ void ResourcePreparer::free(Cesium3DTilesSelection::Tile& /*tile*/,
117
117
 
118
118
  // ── Raster overlay pipeline ─────────────────────────────────────────────────
119
119
 
120
- void* ResourcePreparer::prepareRasterInLoadThread(CesiumGltf::ImageAsset& image,
120
+ void* ResourcePreparer::prepareRasterInLoadThread(CesiumImage::ImageAsset& image,
121
121
  const std::any&) {
122
122
  if (image.pixelData.empty() || image.width <= 0 || image.height <= 0) {
123
123
  return nullptr;
124
124
  }
125
- if (image.compressedPixelFormat != CesiumGltf::GpuCompressedPixelFormat::NONE) {
125
+ if (image.compressedPixelFormat != CesiumImage::GpuCompressedPixelFormat::NONE) {
126
126
  return nullptr;
127
127
  }
128
128
  if (image.bytesPerChannel != 1) return nullptr;
@@ -64,7 +64,7 @@ public:
64
64
  void free(Cesium3DTilesSelection::Tile& tile, void* pLoadThreadResult,
65
65
  void* pMainThreadResult) noexcept override;
66
66
 
67
- void* prepareRasterInLoadThread(CesiumGltf::ImageAsset& image,
67
+ void* prepareRasterInLoadThread(CesiumImage::ImageAsset& image,
68
68
  const std::any& rendererOptions) override;
69
69
  void* prepareRasterInMainThread(
70
70
  CesiumRasterOverlays::RasterOverlayTile& rasterTile,
@@ -0,0 +1,70 @@
1
+ #pragma once
2
+
3
+ #include "CameraIntegrator.hpp"
4
+ #include "CesiumEngine.hpp"
5
+ #include "GeoidConverter.hpp"
6
+ #include "TerrainHeight.hpp"
7
+
8
+ #include <algorithm>
9
+
10
+ namespace reactnativecesium {
11
+
12
+ // Previous horizontal position and whether a real mesh sample has landed.
13
+ // Shared by the iOS and Android render loops so both follow the same floor.
14
+ struct TerrainLeadState {
15
+ bool hasPrev = false;
16
+ bool latched = false;
17
+ double prevLatDeg = 0.0;
18
+ double prevLonDeg = 0.0;
19
+ };
20
+
21
+ // A position slew drops the old floor and the one-frame lead. The next
22
+ // rendered frame samples terrain at the new position.
23
+ inline void noteTerrainSlew(CameraIntegrator& integrator, TerrainLeadState& lead) {
24
+ integrator.clearTerrainFloor();
25
+ lead = {};
26
+ }
27
+
28
+ // Samples the resident mesh at the stepped camera and one frame ahead, then
29
+ // holds the rendered altitude to surface + clearance before the frame is drawn.
30
+ // Demand is not modified. Returns the camera to render.
31
+ inline CameraParams applyTerrainFloor(CesiumEngine& engine,
32
+ CameraIntegrator& integrator,
33
+ const CameraParams& stepped,
34
+ TerrainLeadState& lead,
35
+ double clearanceMeters) {
36
+ const double clearance = std::max(0.0, clearanceMeters);
37
+
38
+ double leadLat = stepped.latitude;
39
+ double leadLon = stepped.longitude;
40
+ const bool sampleLead = terrainLeadLatLon(
41
+ stepped.latitude, stepped.longitude,
42
+ lead.prevLatDeg, lead.prevLonDeg, lead.hasPrev,
43
+ leadLat, leadLon);
44
+
45
+ float nadir = 0.0f;
46
+ float ahead = 0.0f;
47
+ const bool hasNadir =
48
+ engine.sampleTerrainEllipsoidMeters(stepped.latitude, stepped.longitude, nadir);
49
+ const bool hasAhead = sampleLead &&
50
+ engine.sampleTerrainEllipsoidMeters(leadLat, leadLon, ahead);
51
+
52
+ float surface = 0.0f;
53
+ const bool haveMesh = resolveSurfaceEllipsoid(hasNadir, nadir, hasAhead, ahead, surface);
54
+ const double geoidN = mslToEllipsoidMeters(stepped.latitude, stepped.longitude, 0.0);
55
+ if (haveMesh) {
56
+ integrator.setTerrainFloor(static_cast<double>(surface) - geoidN + clearance);
57
+ lead.latched = true;
58
+ } else if (!lead.latched) {
59
+ // No mesh yet: stay at or above the ellipsoid. Once a mesh sample has
60
+ // landed, a later miss keeps the last floor instead of dropping to sea level.
61
+ integrator.setTerrainFloor(-geoidN + clearance);
62
+ }
63
+
64
+ lead.prevLatDeg = stepped.latitude;
65
+ lead.prevLonDeg = stepped.longitude;
66
+ lead.hasPrev = true;
67
+ return integrator.getActual();
68
+ }
69
+
70
+ } // namespace reactnativecesium
@@ -0,0 +1,113 @@
1
+ #pragma once
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+
6
+ namespace reactnativecesium {
7
+
8
+ // Pure terrain-floor helpers. No Cesium types, so the host harness can test
9
+ // contour rules without a tileset.
10
+
11
+ // Ignore a lead step shorter than this (hover) or longer than this (a slew,
12
+ // not one frame of travel).
13
+ inline constexpr double kTerrainLeadMinMeters = 0.05;
14
+ inline constexpr double kTerrainLeadMaxMeters = 80.0;
15
+ inline constexpr double kEarthRadiusMeters = 6371008.7714150595;
16
+
17
+ // Horizontal barycentric height. (pe, pn) and the triangle are in metres in
18
+ // the same local east/north frame. Near-vertical triangles (terrain skirts)
19
+ // have ~0 horizontal area and are rejected. On a hit, outHeight is the
20
+ // interpolated ellipsoid height in metres.
21
+ inline bool horizontalTriangleHeight(double pe, double pn,
22
+ double e0, double n0, float h0,
23
+ double e1, double n1, float h1,
24
+ double e2, double n2, float h2,
25
+ float& outHeight) {
26
+ const double v0e = e1 - e0;
27
+ const double v0n = n1 - n0;
28
+ const double v1e = e2 - e0;
29
+ const double v1n = n2 - n0;
30
+ const double v2e = pe - e0;
31
+ const double v2n = pn - n0;
32
+
33
+ const double dot00 = v0e * v0e + v0n * v0n;
34
+ const double dot01 = v0e * v1e + v0n * v1n;
35
+ const double dot11 = v1e * v1e + v1n * v1n;
36
+ const double dot02 = v0e * v2e + v0n * v2n;
37
+ const double dot12 = v1e * v2e + v1n * v2n;
38
+
39
+ // denom = (2 * signed horizontal area)^2.
40
+ const double denom = dot00 * dot11 - dot01 * dot01;
41
+ if (std::abs(denom) < 1.0e-12) return false;
42
+
43
+ const double e20 = e2 - e1;
44
+ const double n20 = n2 - n1;
45
+ const double edge2 = e20 * e20 + n20 * n20;
46
+ const double longest = std::sqrt(std::max(dot00, std::max(dot11, edge2)));
47
+ const double twiceArea = std::sqrt(std::abs(denom));
48
+ // Width of the triangle across its longest edge. Terrain skirts are
49
+ // vertical, so float error leaves them only millimetres wide.
50
+ if (longest < 1.0e-6 || twiceArea / longest < 0.05) return false;
51
+
52
+ const double inv = 1.0 / denom;
53
+ const double u = (dot11 * dot02 - dot01 * dot12) * inv;
54
+ const double v = (dot00 * dot12 - dot01 * dot02) * inv;
55
+ const double w = 1.0 - u - v;
56
+
57
+ constexpr double kEdge = -1.0e-4;
58
+ if (u < kEdge || v < kEdge || w < kEdge) return false;
59
+
60
+ outHeight = static_cast<float>(w * static_cast<double>(h0) +
61
+ u * static_cast<double>(h1) +
62
+ v * static_cast<double>(h2));
63
+ return true;
64
+ }
65
+
66
+ // Fresh each frame. The higher of the nadir sample and the one-frame lead.
67
+ // A lower lead (past a crest) does not keep the previous peak. A higher lead
68
+ // (valley turning uphill) raises the floor in this frame. No memory.
69
+ inline bool resolveSurfaceEllipsoid(bool hasNadir, float nadir,
70
+ bool hasAhead, float ahead,
71
+ float& outHeight) {
72
+ if (hasNadir && hasAhead) {
73
+ outHeight = std::max(nadir, ahead);
74
+ return true;
75
+ }
76
+ if (hasNadir) {
77
+ outHeight = nadir;
78
+ return true;
79
+ }
80
+ if (hasAhead) {
81
+ outHeight = ahead;
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ // One frame ahead of (latDeg, lonDeg) along the last step. Returns false when
88
+ // there is no distinct lead; out* are then equal to the current position.
89
+ inline bool terrainLeadLatLon(double latDeg, double lonDeg,
90
+ double prevLatDeg, double prevLonDeg,
91
+ bool hasPrev,
92
+ double& outLatDeg, double& outLonDeg) {
93
+ outLatDeg = latDeg;
94
+ outLonDeg = lonDeg;
95
+ if (!hasPrev) return false;
96
+
97
+ const double dLat = latDeg - prevLatDeg;
98
+ double dLon = lonDeg - prevLonDeg;
99
+ dLon = std::fmod(dLon + 540.0, 360.0) - 180.0;
100
+
101
+ const double latRad = latDeg * (3.14159265358979323846 / 180.0);
102
+ const double northM = dLat * (3.14159265358979323846 / 180.0) * kEarthRadiusMeters;
103
+ const double eastM =
104
+ dLon * (3.14159265358979323846 / 180.0) * kEarthRadiusMeters * std::cos(latRad);
105
+ const double dist = std::hypot(eastM, northM);
106
+ if (dist < kTerrainLeadMinMeters || dist > kTerrainLeadMaxMeters) return false;
107
+
108
+ outLatDeg = latDeg + dLat;
109
+ outLonDeg = lonDeg + dLon;
110
+ return true;
111
+ }
112
+
113
+ } // namespace reactnativecesium
@@ -11,7 +11,9 @@
11
11
 
12
12
  #include "../CameraIntegrator.hpp"
13
13
  #include "../EngineTunables.hpp"
14
+ #include "../TerrainHeight.hpp"
14
15
 
16
+ #include <algorithm>
15
17
  #include <chrono>
16
18
  #include <cmath>
17
19
  #include <cstdio>
@@ -189,6 +191,257 @@ static void testTerrainClamp() {
189
191
  CHECK(approxEq(demand.altitude, 100.0, 1e-6), "terrain_clamp.demand_preserved");
190
192
  }
191
193
 
194
+ static void followFloors(const double* floors, int count, const char* label,
195
+ bool expectRiseThenFall) {
196
+ CameraIntegrator integ;
197
+ CameraParams seed;
198
+ seed.altitude = floors[0];
199
+ integ.teleport(seed);
200
+ // A demand under every floor, plus a downward velocity sample, must not
201
+ // pull the rendered camera through the surface.
202
+ integ.setAltitude(0.0);
203
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
204
+ integ.setAltitude(0.0);
205
+
206
+ double prev = 0.0;
207
+ bool havePrev = false;
208
+ bool sawPeak = false;
209
+ for (int i = 0; i < count; ++i) {
210
+ const double floor = floors[i];
211
+ integ.setAltitude(0.0);
212
+ integ.setTerrainFloor(floor);
213
+ const CameraParams actual = integ.step();
214
+ char buf[96];
215
+ std::snprintf(buf, sizeof(buf), "%s.exact_%d", label, i);
216
+ CHECK(approxEq(actual.altitude, floor, 1e-6), buf);
217
+ CHECK(approxEq(integ.getDemand().altitude, 0.0, 1e-9), label);
218
+ if (havePrev) {
219
+ if (!expectRiseThenFall) {
220
+ std::snprintf(buf, sizeof(buf), "%s.monotonic_%d", label, i);
221
+ if (floors[i] > floors[i - 1]) {
222
+ CHECK(actual.altitude > prev - 1e-6, buf);
223
+ } else {
224
+ CHECK(actual.altitude < prev + 1e-6, buf);
225
+ }
226
+ }
227
+ if (expectRiseThenFall && floors[i] < floors[i - 1]) {
228
+ sawPeak = true;
229
+ std::snprintf(buf, sizeof(buf), "%s.descends_%d", label, i);
230
+ CHECK(actual.altitude < prev - 1e-6, buf);
231
+ }
232
+ if (expectRiseThenFall && sawPeak && floors[i] > floors[i - 1]) {
233
+ std::snprintf(buf, sizeof(buf), "%s.climbs_%d", label, i);
234
+ CHECK(actual.altitude > prev + 1e-6, buf);
235
+ }
236
+ }
237
+ prev = actual.altitude;
238
+ havePrev = true;
239
+ }
240
+ }
241
+
242
+ static void testUphillContact() {
243
+ const double floors[] = {100.0, 110.0, 120.0, 130.0};
244
+ followFloors(floors, 4, "uphill", false);
245
+ }
246
+
247
+ static void testDownhillContact() {
248
+ const double floors[] = {130.0, 120.0, 110.0, 100.0};
249
+ followFloors(floors, 4, "downhill", false);
250
+ }
251
+
252
+ static void testCrest() {
253
+ const double floors[] = {100.0, 110.0, 120.0, 110.0, 100.0};
254
+ followFloors(floors, 5, "crest", true);
255
+ }
256
+
257
+ static void testValley() {
258
+ const double floors[] = {120.0, 110.0, 100.0, 110.0, 120.0};
259
+ followFloors(floors, 5, "valley", true);
260
+
261
+ // Descent velocity must not carry the camera into the rising face.
262
+ CameraIntegrator integ;
263
+ CameraParams seed;
264
+ seed.altitude = 100.0;
265
+ integ.teleport(seed);
266
+ integ.clearTerrainFloor();
267
+ integ.setAltitude(0.0);
268
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
269
+ integ.setAltitude(0.0);
270
+ CHECK(integ.getActual().altitude < 100.0, "valley.velocity_descended");
271
+ integ.setTerrainFloor(110.0);
272
+ CHECK(approxEq(integ.getActual().altitude, 110.0, 1e-6), "valley.climb_exact");
273
+ CHECK(approxEq(integ.step().altitude, 110.0, 1e-6), "valley.climb_holds");
274
+ }
275
+
276
+ static void testSetAltitudeUnderFloor() {
277
+ CameraIntegrator integ;
278
+ CameraParams seed;
279
+ seed.altitude = 100.0;
280
+ integ.teleport(seed);
281
+ integ.setTerrainFloor(150.0);
282
+ integ.setAltitude(10.0);
283
+ CHECK(approxEq(integ.getActual().altitude, 150.0, 1e-6), "under_floor.actual");
284
+ CHECK(approxEq(integ.getDemand().altitude, 10.0, 1e-9), "under_floor.demand");
285
+ const CameraParams stepped = integ.step();
286
+ CHECK(approxEq(stepped.altitude, 150.0, 1e-6), "under_floor.after_step");
287
+ }
288
+
289
+ static void testIdleOnFloor() {
290
+ CameraIntegrator integ;
291
+ CameraParams seed;
292
+ seed.altitude = 100.0;
293
+ integ.teleport(seed);
294
+ integ.setTerrainFloor(150.0);
295
+ integ.setAltitude(40.0);
296
+ const CameraParams stepped = integ.step();
297
+ CHECK(approxEq(stepped.altitude, 150.0, 1e-6), "idle_floor.actual");
298
+ CHECK(!integ.isActive(), "idle_floor.inactive");
299
+ }
300
+
301
+ static void testFlyingAboveFloor() {
302
+ CameraIntegrator integ;
303
+ CameraParams seed;
304
+ seed.altitude = 500.0;
305
+ integ.teleport(seed);
306
+ integ.setTerrainFloor(100.0);
307
+ const CameraParams stepped = integ.step();
308
+ CHECK(approxEq(stepped.altitude, 500.0, 1e-3), "above_floor.untouched");
309
+ CHECK(approxEq(integ.getDemand().altitude, 500.0, 1e-9), "above_floor.demand");
310
+ }
311
+
312
+ static double wrap180(double deg) {
313
+ double v = std::fmod(deg + 180.0, 360.0);
314
+ if (v < 0.0) v += 360.0;
315
+ return v - 180.0;
316
+ }
317
+
318
+ static void testHeadingShortPull() {
319
+ CameraIntegrator integ;
320
+ CameraParams seed;
321
+ seed.heading = 350.0;
322
+ integ.teleport(seed);
323
+ integ.setHeading(10.0);
324
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
325
+ bool onShortArc = true;
326
+ for (int i = 0; i < 90; ++i) {
327
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
328
+ const double h = integ.step().heading;
329
+ if (!(h >= 330.0 || h <= 40.0)) onShortArc = false;
330
+ }
331
+ CHECK(onShortArc, "heading_pull.short_arc");
332
+ const double end = integ.getActual().heading;
333
+ const double err = std::abs(wrap180(end - 10.0));
334
+ CHECK(err < 2.0, "heading_pull.converged");
335
+ }
336
+
337
+ static void testLongitudeShortPull() {
338
+ CameraIntegrator integ;
339
+ CameraParams seed;
340
+ seed.longitude = 179.0;
341
+ integ.teleport(seed);
342
+ integ.setPosition(seed.latitude, -179.0);
343
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
344
+ bool onShortArc = true;
345
+ for (int i = 0; i < 90; ++i) {
346
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
347
+ const double wrapped = wrap180(integ.step().longitude);
348
+ if (!(std::abs(wrapped) > 90.0)) onShortArc = false;
349
+ }
350
+ CHECK(onShortArc, "lon_pull.short_arc");
351
+ const double err = std::abs(wrap180(integ.getActual().longitude - (-179.0)));
352
+ CHECK(err < 2.0, "lon_pull.converged");
353
+ }
354
+
355
+ static void testVerticalFovConverges() {
356
+ CameraIntegrator integ;
357
+ CameraParams seed;
358
+ seed.verticalFov = 60.0;
359
+ integ.teleport(seed);
360
+ integ.setVerticalFov(30.0);
361
+ std::this_thread::sleep_for(std::chrono::milliseconds(16));
362
+ const double first = integ.step().verticalFov;
363
+ CHECK(first < 59.0 && first > 40.0, "vfov.first_step_eases");
364
+ CameraParams mid;
365
+ for (int i = 0; i < 80; ++i) {
366
+ std::this_thread::sleep_for(std::chrono::milliseconds(16));
367
+ mid = integ.step();
368
+ }
369
+ CHECK(approxEq(mid.verticalFov, 30.0, 0.5), "vfov.converged");
370
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
371
+ const double held = integ.step().verticalFov;
372
+ CHECK(approxEq(held, 30.0, 0.5), "vfov.stays");
373
+ }
374
+
375
+ static void testLeadAndTriangle() {
376
+ float out = 0.0f;
377
+ CHECK(resolveSurfaceEllipsoid(true, 100.0f, true, 90.0f, out) && out == 100.0f,
378
+ "lead.crest_no_sticky_peak");
379
+ CHECK(resolveSurfaceEllipsoid(true, 100.0f, true, 110.0f, out) && out == 110.0f,
380
+ "lead.valley_raises");
381
+ CHECK(resolveSurfaceEllipsoid(true, 120.0f, false, 0.0f, out) && out == 120.0f,
382
+ "lead.nadir_only");
383
+ CHECK(!resolveSurfaceEllipsoid(false, 0.0f, false, 0.0f, out), "lead.neither");
384
+
385
+ double lat = 0.0, lon = 0.0;
386
+ const bool slew = terrainLeadLatLon(0.0, 1.0, 0.0, 0.0, true, lat, lon);
387
+ CHECK(!slew, "lead.slew_ignored");
388
+
389
+ const bool step = terrainLeadLatLon(0.0, 0.0001, 0.0, 0.0, true, lat, lon);
390
+ CHECK(step, "lead.one_frame");
391
+ CHECK(approxEq(lat, 0.0, 1e-12), "lead.lat");
392
+ CHECK(approxEq(lon, 0.0002, 1e-9), "lead.lon_ahead");
393
+
394
+ float h = -1.0f;
395
+ const bool hit = horizontalTriangleHeight(
396
+ 2.0, 2.0,
397
+ 0.0, 0.0, 0.0f,
398
+ 10.0, 0.0, 0.0f,
399
+ 0.0, 10.0, 30.0f,
400
+ h);
401
+ CHECK(hit && approxEq(h, 6.0, 1e-3), "tri.interpolated");
402
+
403
+ float outside = 0.0f;
404
+ CHECK(!horizontalTriangleHeight(
405
+ 20.0, 20.0,
406
+ 0.0, 0.0, 0.0f,
407
+ 10.0, 0.0, 0.0f,
408
+ 0.0, 10.0, 30.0f,
409
+ outside),
410
+ "tri.outside");
411
+
412
+ float skirt = 0.0f;
413
+ CHECK(!horizontalTriangleHeight(
414
+ 1.0, 0.0,
415
+ 0.0, 0.0, 100.0f,
416
+ 0.0, 0.0, 0.0f,
417
+ 5.0, 0.0, 100.0f,
418
+ skirt),
419
+ "tri.skirt_rejected");
420
+ // 100 m edge, 1 mm of horizontal width: a float-error skirt, not terrain.
421
+ CHECK(!horizontalTriangleHeight(
422
+ 0.0, 0.0005,
423
+ 0.0, 0.0, 100.0f,
424
+ 100.0, 0.0, 100.0f,
425
+ 0.0, 0.001, 0.0f,
426
+ skirt),
427
+ "tri.thin_skirt_rejected");
428
+
429
+ float low = 0.0f, high = 0.0f;
430
+ const bool lowHit = horizontalTriangleHeight(
431
+ 2.0, 2.0,
432
+ 0.0, 0.0, 5.0f,
433
+ 10.0, 0.0, 5.0f,
434
+ 0.0, 10.0, 5.0f,
435
+ low);
436
+ const bool highHit = horizontalTriangleHeight(
437
+ 2.0, 2.0,
438
+ 0.0, 0.0, 20.0f,
439
+ 10.0, 0.0, 20.0f,
440
+ 0.0, 10.0, 20.0f,
441
+ high);
442
+ CHECK(lowHit && highHit && std::max(low, high) == 20.0f, "tri.overlap_keeps_higher");
443
+ }
444
+
192
445
  int main() {
193
446
  testStepInput();
194
447
  testConstantVelocity();
@@ -196,6 +449,17 @@ int main() {
196
449
  testLongPause();
197
450
  testGestureEndBleed();
198
451
  testTerrainClamp();
452
+ testUphillContact();
453
+ testDownhillContact();
454
+ testCrest();
455
+ testValley();
456
+ testSetAltitudeUnderFloor();
457
+ testIdleOnFloor();
458
+ testFlyingAboveFloor();
459
+ testHeadingShortPull();
460
+ testLongitudeShortPull();
461
+ testVerticalFovConverges();
462
+ testLeadAndTriangle();
199
463
  if (g_failures == 0) {
200
464
  std::fprintf(stderr, "\nALL TESTS PASSED.\n");
201
465
  } else {
@@ -4,8 +4,8 @@
4
4
  #include "engine/CameraIntegrator.hpp"
5
5
  #include "engine/CesiumEngine.hpp"
6
6
  #include "engine/EngineTunables.hpp"
7
- #include "engine/GeoidConverter.hpp"
8
7
  #include "engine/MetricsAggregator.hpp"
8
+ #include "engine/TerrainFloor.hpp"
9
9
  #include "metal/MetalBackend.h"
10
10
 
11
11
  #include <glm/gtc/quaternion.hpp>
@@ -48,6 +48,8 @@
48
48
  // Sentinel (epoch) means "no loading seen yet".
49
49
  std::chrono::steady_clock::time_point _lastLoadingTime;
50
50
 
51
+ reactnativecesium::TerrainLeadState _terrainLead;
52
+
51
53
  NSString* _cacheDir;
52
54
  }
53
55
 
@@ -230,6 +232,7 @@
230
232
  // jump, not a boresight reset. Use setViewCorrection(identity) to clear it.
231
233
  p.viewCorrection = _integrator->getActual().viewCorrection;
232
234
  _integrator->teleport(p);
235
+ reactnativecesium::noteTerrainSlew(*_integrator, _terrainLead);
233
236
  // Apply directly to the engine so the next render uses the teleported value
234
237
  // without one frame of "almost there" interpolation.
235
238
  _engine->camera().setParams(p);
@@ -382,32 +385,9 @@
382
385
  // The integrator owns its own clock now — see CameraIntegrator::step().
383
386
  // `nowSeconds` is still used below for the FPS-EMA dt only.
384
387
  auto actual = _integrator->step();
385
-
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).
390
- const float minAbove = _engine->getConfig().minAltitudeAboveTerrain;
391
- if (minAbove > 0.0f && _engine->terrainFloorKnown()) {
392
- const double geoidN =
393
- reactnativecesium::mslToEllipsoidMeters(actual.latitude, actual.longitude, 0.0);
394
- const double floorEllipsoid = static_cast<double>(_engine->terrainFloorEllipsoidMeters());
395
- const double floorMsl = floorEllipsoid - geoidN;
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)
406
- if (actual.altitude < minMsl) {
407
- _integrator->clampActualAltitude(minMsl);
408
- actual = _integrator->getActual();
409
- }
410
- }
388
+ actual = reactnativecesium::applyTerrainFloor(
389
+ *_engine, *_integrator, actual, _terrainLead,
390
+ static_cast<double>(_engine->getConfig().minAltitudeAboveTerrain));
411
391
 
412
392
  _engine->camera().setParams(actual);
413
393
 
@@ -84,12 +84,13 @@ export interface CesiumViewProps extends HybridViewProps {
84
84
  */
85
85
  taskProcessorThreads?: number;
86
86
  /**
87
- * Minimum clearance above the loaded terrain surface, in metres (added on top of the
88
- * terrain MSL height at the camera position). Default 0 (no constraint).
89
- * Set to e.g. 2 to prevent the camera from going underground.
90
- * The clamp runs only after terrain tiles have been sampled; until then the demand
91
- * altitude is used as-is. The demand altitude is not modified — only the rendered
92
- * (actual) camera may be raised to meet this minimum.
87
+ * Clearance above the rendered terrain surface, in metres, added on top of the
88
+ * terrain height at the camera position. The floor is always on. 0 means the
89
+ * camera may sit on the surface and may not go below it.
90
+ * Until a terrain tile has been sampled, the camera is held at or above the
91
+ * WGS84 ellipsoid. The demand altitude is not modified — only the rendered
92
+ * (actual) camera is held to this minimum, and it follows the surface down
93
+ * when the demand is at or below the floor.
93
94
  */
94
95
  minAltitudeAboveTerrain?: number;
95
96
  /** Maximum yaw rate (degrees per second). 0 = uncapped. */
@@ -1 +1 @@
1
- {"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"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"}
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;;;GAGG;AACH,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;;;;GAIG;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;;;;OAIG;IACH,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;IAOzB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,mEAAmE;IACnE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,0GAA0G;IAC1G,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;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;;;;OAQG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAShC,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,6DAA6D;IAC7D,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,sEAAsE;IACtE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAE5C;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;CAC/C;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D;;;;;OAKG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IAEtD,yDAAyD;IACzD,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAEzC,qEAAqE;IACrE,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IAEpC;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAEpD;;;;OAIG;IACH,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,CAAA;IAEtC,uEAAuE;IACvE,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAEjC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IAEnC;;;;;OAKG;IACH,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IAEvC;;;;OAIG;IACH,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IAEvC,8EAA8E;IAC9E,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAA;CACzC;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAA"}
@@ -30,7 +30,7 @@ int initialize(JavaVM* vm) {
30
30
  }
31
31
 
32
32
  struct JHybridCesiumViewSpecImpl: public jni::JavaClass<JHybridCesiumViewSpecImpl, JHybridCesiumViewSpec::JavaPart> {
33
- static constexpr auto kJavaDescriptor = "Lcom/reactnativecesium/HybridCesiumView;";
33
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/reactnativecesium/HybridCesiumView;";
34
34
  static std::shared_ptr<JHybridCesiumViewSpec> create() {
35
35
  static const auto constructorFn = javaClassStatic()->getConstructor<JHybridCesiumViewSpecImpl::javaobject()>();
36
36
  jni::local_ref<JHybridCesiumViewSpec::JavaPart> javaPart = javaClassStatic()->newObject(constructorFn);
@@ -17,7 +17,7 @@ namespace margelo::nitro::reactnativecesium {
17
17
  using namespace facebook;
18
18
 
19
19
  /**
20
- * The C++ JNI bridge between the C++ struct "CameraState" and the the Kotlin data class "CameraState".
20
+ * The C++ JNI bridge between the C++ struct "CameraState" and the Kotlin data class "CameraState".
21
21
  */
22
22
  struct JCameraState final: public jni::JavaClass<JCameraState> {
23
23
  public:
@@ -17,7 +17,7 @@ namespace margelo::nitro::reactnativecesium {
17
17
  using namespace facebook;
18
18
 
19
19
  /**
20
- * The C++ JNI bridge between the C++ struct "CesiumMetrics" and the the Kotlin data class "CesiumMetrics".
20
+ * The C++ JNI bridge between the C++ struct "CesiumMetrics" and the Kotlin data class "CesiumMetrics".
21
21
  */
22
22
  struct JCesiumMetrics final: public jni::JavaClass<JCesiumMetrics> {
23
23
  public: