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
@@ -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 {
@@ -0,0 +1,77 @@
1
+ // Host check for the EGM96 MSL <-> ellipsoid conversion.
2
+ //
3
+ // clang++ -std=c++20 -I cpp -o /tmp/geoid cpp/engine/test/GeoidConverterTest.cpp \
4
+ // cpp/engine/GeoidConverter.cpp cpp/third_party/egm96/EGM96.c
5
+ //
6
+ // Or: bash cpp/engine/test/build_and_run.sh
7
+
8
+ #include "../GeoidConverter.hpp"
9
+
10
+ #include <cmath>
11
+ #include <cstdio>
12
+
13
+ using namespace reactnativecesium;
14
+
15
+ static int g_failures = 0;
16
+
17
+ static void check(bool ok, const char* label) {
18
+ if (!ok) {
19
+ std::fprintf(stderr, "FAIL [%s]\n", label);
20
+ ++g_failures;
21
+ return;
22
+ }
23
+ std::fprintf(stderr, "PASS [%s]\n", label);
24
+ }
25
+
26
+ static void checkInverse(const char* label, double lat, double lon, double msl) {
27
+ const double hae = mslToEllipsoidMeters(lat, lon, msl);
28
+ const double back = ellipsoidToMslMeters(lat, lon, hae);
29
+ const double undulation = hae - msl;
30
+ if (!std::isfinite(hae) || !std::isfinite(back) || std::fabs(back - msl) > 1e-6) {
31
+ std::fprintf(stderr, "FAIL [%s]: msl=%.3f hae=%.3f back=%.3f\n", label, msl, hae, back);
32
+ ++g_failures;
33
+ return;
34
+ }
35
+ if (std::fabs(undulation) < 1.0) {
36
+ std::fprintf(stderr, "FAIL [%s]: undulation %.3f m is unexpectedly ~0\n", label, undulation);
37
+ ++g_failures;
38
+ return;
39
+ }
40
+ std::fprintf(stderr, "PASS [%s] undulation %.2f m\n", label, undulation);
41
+ }
42
+
43
+ static void checkDemandRoundTrip(const char* label, double lat, double lon, double msl) {
44
+ const double hae = mslToDemandAltitudeMeters(lat, lon, msl, true);
45
+ const double back = demandAltitudeToMslMeters(lat, lon, hae, true);
46
+ check(std::isfinite(hae) && std::isfinite(back) && std::fabs(back - msl) < 1e-6, label);
47
+
48
+ const double mslIdentity = demandAltitudeToMslMeters(lat, lon, msl, false);
49
+ const double haeIdentity = mslToDemandAltitudeMeters(lat, lon, msl, false);
50
+ check(mslIdentity == msl && haeIdentity == msl, "datum_msl_identity");
51
+ }
52
+
53
+ int main() {
54
+ // UK geoid is about +45 m. Alps about +50 m. Indian Ocean is negative.
55
+ checkInverse("uk", 51.5, -0.1, 1000.0);
56
+ checkInverse("alps", 46.0, 8.0, 2000.0);
57
+ checkInverse("indian_ocean", -10.0, 80.0, 50.0);
58
+
59
+ const double nan = std::nan("");
60
+ check(ellipsoidToMslMeters(nan, 0.0, 10.0) == 10.0, "ellipsoid_non_finite");
61
+ check(mslToEllipsoidMeters(nan, 0.0, 10.0) == 10.0, "msl_non_finite");
62
+
63
+ checkDemandRoundTrip("demand_round_trip", 51.5, -0.1, 1000.0);
64
+
65
+ constexpr double kUkLat = 51.5;
66
+ constexpr double kUkLon = -0.1;
67
+ constexpr double kOceanLat = -10.0;
68
+ constexpr double kOceanLon = 80.0;
69
+ constexpr double kMsl = 1000.0;
70
+ const double hae = mslToEllipsoidMeters(kUkLat, kUkLon, kMsl);
71
+ const double moved = mslAfterHorizontalMove(kUkLat, kUkLon, kMsl, kOceanLat, kOceanLon);
72
+ const double movedHae = mslToEllipsoidMeters(kOceanLat, kOceanLon, moved);
73
+ check(std::isfinite(moved) && std::fabs(movedHae - hae) < 1e-6, "move_preserves_hae");
74
+ check(std::fabs(moved - kMsl) > 1.0, "move_changes_msl");
75
+
76
+ return g_failures == 0 ? 0 : 1;
77
+ }
@@ -1,12 +1,29 @@
1
1
  #!/usr/bin/env bash
2
- # Compile and run CameraIntegratorTest against the iOS simulator-slice headers
3
- # from the vendored Cesium Native XCFramework. Lightweight smoke test —
4
- # not part of the production build.
2
+ # Compile and run the host engine tests. GeoidConverterTest needs only EGM96.
3
+ # CameraIntegratorTest needs the iOS simulator-slice headers from the vendored
4
+ # Cesium Native XCFramework. Lightweight smoke tests — not part of the
5
+ # production build.
5
6
 
6
7
  set -euo pipefail
7
8
 
8
9
  ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
9
10
  HEADERS="$ROOT/vendor/ios/CesiumNative.xcframework/ios-arm64-simulator/Headers"
11
+ OUT="$(mktemp -d)"
12
+ echo "Building into $OUT..."
13
+
14
+ xcrun -sdk macosx clang -std=c11 -O0 -g \
15
+ -I"$ROOT/cpp" \
16
+ -c "$ROOT/cpp/third_party/egm96/EGM96.c" \
17
+ -o "$OUT/EGM96.o"
18
+
19
+ xcrun -sdk macosx clang++ -std=c++20 -O0 -g \
20
+ -I"$ROOT/cpp" \
21
+ -o "$OUT/geoid" \
22
+ "$ROOT/cpp/engine/test/GeoidConverterTest.cpp" \
23
+ "$ROOT/cpp/engine/GeoidConverter.cpp" \
24
+ "$OUT/EGM96.o"
25
+
26
+ "$OUT/geoid"
10
27
 
11
28
  if [[ ! -d "$HEADERS" ]]; then
12
29
  echo "Cesium Native headers not found at $HEADERS"
@@ -14,9 +31,6 @@ if [[ ! -d "$HEADERS" ]]; then
14
31
  exit 1
15
32
  fi
16
33
 
17
- OUT="$(mktemp -d)"
18
- echo "Building into $OUT..."
19
-
20
34
  xcrun -sdk macosx clang++ -std=c++20 -O0 -g \
21
35
  -DGLM_FORCE_DEPTH_ZERO_TO_ONE=1 \
22
36
  -I"$ROOT/cpp" -I"$HEADERS" \
@@ -33,6 +33,11 @@ NS_ASSUME_NONNULL_BEGIN
33
33
  // instant. Pitch / roll are bundled because they typically come from a
34
34
  // single IMU sample.
35
35
  - (void)setPositionLatitude:(double)lat longitude:(double)lon;
36
+ /// When YES, altitudes passed to setAltitude: and teleport are WGS84
37
+ /// ellipsoidal heights. They are converted to MSL before the integrator, and
38
+ /// altitude readback is converted back. A horizontal move keeps that
39
+ /// ellipsoidal height.
40
+ - (void)setAltitudeDatumEllipsoid:(BOOL)ellipsoid;
36
41
  - (void)setAltitude:(double)alt;
37
42
  - (void)setHeadingDeg:(double)deg;
38
43
  - (void)setAttitudePitch:(double)pitch roll:(double)roll;
@@ -6,6 +6,7 @@
6
6
  #include "engine/EngineTunables.hpp"
7
7
  #include "engine/GeoidConverter.hpp"
8
8
  #include "engine/MetricsAggregator.hpp"
9
+ #include "engine/TerrainFloor.hpp"
9
10
  #include "metal/MetalBackend.h"
10
11
 
11
12
  #include <glm/gtc/quaternion.hpp>
@@ -35,6 +36,9 @@
35
36
  int _viewportHeight;
36
37
  BOOL _initialized;
37
38
  BOOL _suspended;
39
+ // Written when the prop changes; read from the caller thread inside
40
+ // setAltitude / setPosition / teleport / readback.
41
+ std::atomic<bool> _altitudeIsEllipsoid;
38
42
 
39
43
  // Last wall-clock tick used to compute the metrics-only frame dt EMA. Kept
40
44
  // per-instance (not a function-local static) so a hot reload that destroys
@@ -48,6 +52,8 @@
48
52
  // Sentinel (epoch) means "no loading seen yet".
49
53
  std::chrono::steady_clock::time_point _lastLoadingTime;
50
54
 
55
+ reactnativecesium::TerrainLeadState _terrainLead;
56
+
51
57
  NSString* _cacheDir;
52
58
  }
53
59
 
@@ -63,8 +69,9 @@
63
69
  if (self) {
64
70
  _viewportWidth = width;
65
71
  _viewportHeight = height;
66
- _initialized = NO;
67
- _suspended = NO;
72
+ _initialized = NO;
73
+ _suspended = NO;
74
+ _altitudeIsEllipsoid.store(false, std::memory_order_relaxed);
68
75
  _lastTickSec = 0.0;
69
76
  _forceRender.store(true, std::memory_order_release);
70
77
  _cacheDir = [cacheDir copy];
@@ -182,13 +189,45 @@
182
189
 
183
190
  - (void)setPositionLatitude:(double)lat longitude:(double)lon {
184
191
  if (!_initialized) return;
192
+ const bool ellipsoid = _altitudeIsEllipsoid.load(std::memory_order_acquire);
193
+ double adjustedMsl = 0.0;
194
+ bool adjustAltitude = false;
195
+ if (ellipsoid) {
196
+ const auto demand = _integrator->getDemand();
197
+ adjustedMsl = reactnativecesium::mslAfterHorizontalMove(
198
+ demand.latitude, demand.longitude, demand.altitude, lat, lon);
199
+ adjustAltitude = std::abs(adjustedMsl - demand.altitude) >
200
+ reactnativecesium::kHorizontalMoveMslEpsilonMeters;
201
+ }
185
202
  _integrator->setPosition(lat, lon);
203
+ if (adjustAltitude) {
204
+ _integrator->setAltitude(adjustedMsl);
205
+ }
186
206
  _forceRender.store(true, std::memory_order_release);
187
207
  }
188
208
 
209
+ - (void)setAltitudeDatumEllipsoid:(BOOL)ellipsoid {
210
+ _altitudeIsEllipsoid.store(ellipsoid == YES, std::memory_order_release);
211
+ }
212
+
213
+ - (double)mslAltitude:(double)alt latitude:(double)lat longitude:(double)lon {
214
+ return reactnativecesium::demandAltitudeToMslMeters(
215
+ lat, lon, alt, _altitudeIsEllipsoid.load(std::memory_order_acquire));
216
+ }
217
+
218
+ - (double)demandAltitudeFromMsl:(double)msl
219
+ latitude:(double)lat
220
+ longitude:(double)lon {
221
+ return reactnativecesium::mslToDemandAltitudeMeters(
222
+ lat, lon, msl, _altitudeIsEllipsoid.load(std::memory_order_acquire));
223
+ }
224
+
189
225
  - (void)setAltitude:(double)alt {
190
226
  if (!_initialized) return;
191
- _integrator->setAltitude(alt);
227
+ const auto demand = _integrator->getDemand();
228
+ _integrator->setAltitude([self mslAltitude:alt
229
+ latitude:demand.latitude
230
+ longitude:demand.longitude]);
192
231
  _forceRender.store(true, std::memory_order_release);
193
232
  }
194
233
 
@@ -221,7 +260,7 @@
221
260
  reactnativecesium::CameraParams p;
222
261
  p.latitude = lat;
223
262
  p.longitude = lon;
224
- p.altitude = alt;
263
+ p.altitude = [self mslAltitude:alt latitude:lat longitude:lon];
225
264
  p.heading = heading;
226
265
  p.pitch = pitch;
227
266
  p.roll = roll;
@@ -230,6 +269,7 @@
230
269
  // jump, not a boresight reset. Use setViewCorrection(identity) to clear it.
231
270
  p.viewCorrection = _integrator->getActual().viewCorrection;
232
271
  _integrator->teleport(p);
272
+ reactnativecesium::noteTerrainSlew(*_integrator, _terrainLead);
233
273
  // Apply directly to the engine so the next render uses the teleported value
234
274
  // without one frame of "almost there" interpolation.
235
275
  _engine->camera().setParams(p);
@@ -382,32 +422,9 @@
382
422
  // The integrator owns its own clock now — see CameraIntegrator::step().
383
423
  // `nowSeconds` is still used below for the FPS-EMA dt only.
384
424
  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
- }
425
+ actual = reactnativecesium::applyTerrainFloor(
426
+ *_engine, *_integrator, actual, _terrainLead,
427
+ static_cast<double>(_engine->getConfig().minAltitudeAboveTerrain));
411
428
 
412
429
  _engine->camera().setParams(actual);
413
430
 
@@ -447,7 +464,13 @@
447
464
  // ── Actual camera readback (post-integration; what was rendered) ─────────
448
465
  - (double)readActualLatitude { return _integrator ? _integrator->getActual().latitude : 0.0; }
449
466
  - (double)readActualLongitude { return _integrator ? _integrator->getActual().longitude : 0.0; }
450
- - (double)readActualAltitude { return _integrator ? _integrator->getActual().altitude : 0.0; }
467
+ - (double)readActualAltitude {
468
+ if (!_integrator) return 0.0;
469
+ const auto actual = _integrator->getActual();
470
+ return [self demandAltitudeFromMsl:actual.altitude
471
+ latitude:actual.latitude
472
+ longitude:actual.longitude];
473
+ }
451
474
  - (double)readActualHeading { return _integrator ? _integrator->getActual().heading : 0.0; }
452
475
  - (double)readActualPitch { return _integrator ? _integrator->getActual().pitch : 0.0; }
453
476
  - (double)readActualRoll { return _integrator ? _integrator->getActual().roll : 0.0; }
@@ -456,7 +479,13 @@
456
479
  // ── Demand camera readback (what the consumer last asked for) ────────────
457
480
  - (double)readDemandLatitude { return _integrator ? _integrator->getDemand().latitude : 0.0; }
458
481
  - (double)readDemandLongitude { return _integrator ? _integrator->getDemand().longitude : 0.0; }
459
- - (double)readDemandAltitude { return _integrator ? _integrator->getDemand().altitude : 0.0; }
482
+ - (double)readDemandAltitude {
483
+ if (!_integrator) return 0.0;
484
+ const auto demand = _integrator->getDemand();
485
+ return [self demandAltitudeFromMsl:demand.altitude
486
+ latitude:demand.latitude
487
+ longitude:demand.longitude];
488
+ }
460
489
  - (double)readDemandHeading { return _integrator ? _integrator->getDemand().heading : 0.0; }
461
490
  - (double)readDemandPitch { return _integrator ? _integrator->getDemand().pitch : 0.0; }
462
491
  - (double)readDemandRoll { return _integrator ? _integrator->getDemand().roll : 0.0; }
@@ -72,6 +72,11 @@ class HybridCesiumView: HybridCesiumViewSpec {
72
72
  var ionImageryAssetId: Double = 1 {
73
73
  didSet { let v = Int64(ionImageryAssetId); postToRender { [weak self] in self?.bridge?.updateImageryAssetId(v) } }
74
74
  }
75
+ /// `"msl"` (default) or `"ellipsoid"`. Ellipsoid heights are converted to MSL
76
+ /// before the camera integrator.
77
+ var altitudeDatum: String? = "msl" {
78
+ didSet { pushAltitudeDatum() }
79
+ }
75
80
 
76
81
  // ── Optional perf / quality knobs ────────────────────────────────────────
77
82
  var maximumCachedMiB: Double? {
@@ -460,6 +465,12 @@ class HybridCesiumView: HybridCesiumViewSpec {
460
465
  // sqliteCacheMaxRows and taskProcessorThreads are applied at construction
461
466
  // time via the CesiumBridge initializer; calling them here has no effect.
462
467
  if let v = minAltitudeAboveTerrain { bridge?.setMinAltitudeAboveTerrain(Float(v)) }
468
+ bridge?.setAltitudeDatumEllipsoid(altitudeDatum == "ellipsoid")
469
+ }
470
+
471
+ private func pushAltitudeDatum() {
472
+ let ellipsoid = altitudeDatum == "ellipsoid"
473
+ postToRender { [weak self] in self?.bridge?.setAltitudeDatumEllipsoid(ellipsoid) }
463
474
  }
464
475
 
465
476
  // MARK: - Render Loop