react-native-cesium 1.2.2 → 1.2.4

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 (40) hide show
  1. package/README.md +100 -26
  2. package/android/CMakeLists.txt +1 -2
  3. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +264 -130
  4. package/android/src/main/cpp/CesiumBridgeAndroid.h +70 -27
  5. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +68 -44
  6. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +160 -187
  7. package/cpp/engine/CameraIntegrator.cpp +374 -0
  8. package/cpp/engine/CameraIntegrator.hpp +146 -0
  9. package/cpp/engine/CesiumEngine.cpp +3 -1
  10. package/cpp/engine/EngineTunables.hpp +80 -21
  11. package/cpp/engine/GlobeCamera.cpp +17 -6
  12. package/cpp/engine/GlobeCamera.hpp +11 -8
  13. package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
  14. package/cpp/engine/test/build_and_run.sh +27 -0
  15. package/example/README.md +1 -1
  16. package/ios/CesiumBridge.h +53 -28
  17. package/ios/CesiumBridge.mm +172 -86
  18. package/ios/HybridCesiumView.swift +213 -145
  19. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +77 -13
  20. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  21. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +90 -9
  22. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +19 -3
  23. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +20 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +57 -3
  25. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +9 -9
  26. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +82 -9
  27. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +25 -0
  28. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +14 -3
  29. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +210 -16
  30. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +19 -3
  31. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +20 -4
  32. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +55 -0
  33. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +5 -0
  34. package/nitrogen/generated/shared/json/CesiumViewConfig.json +5 -0
  35. package/package.json +2 -2
  36. package/src/specs/cesium-view.nitro.ts +93 -14
  37. package/cpp/engine/CameraSmoother.cpp +0 -71
  38. package/cpp/engine/CameraSmoother.hpp +0 -25
  39. package/cpp/engine/CameraTargetState.cpp +0 -140
  40. package/cpp/engine/CameraTargetState.hpp +0 -103
@@ -16,11 +16,22 @@ static constexpr double kFarPlane = 50000000.0;
16
16
  static constexpr double kMinVfov = 20.0;
17
17
  static constexpr double kMaxVfov = 100.0;
18
18
 
19
- GlobeCamera::GlobeCamera() { recompute(); }
19
+ GlobeCamera::GlobeCamera() {
20
+ params_.verticalFov = std::clamp(params_.verticalFov, kMinVfov, kMaxVfov);
21
+ recompute();
22
+ }
20
23
 
21
24
  void GlobeCamera::setParams(const CameraParams& p) {
22
25
  std::lock_guard<std::mutex> lk(mutex_);
23
26
  params_ = p;
27
+ // A zero (default-init) verticalFov means "leave the previous value";
28
+ // anything else is clamped to the supported range.
29
+ if (p.verticalFov > 0.0) {
30
+ params_.verticalFov = std::clamp(p.verticalFov, kMinVfov, kMaxVfov);
31
+ } else {
32
+ // Restore the existing value rather than letting a zero overwrite it.
33
+ params_.verticalFov = std::clamp(params_.verticalFov, kMinVfov, kMaxVfov);
34
+ }
24
35
  dirty_ = true;
25
36
  }
26
37
 
@@ -31,12 +42,12 @@ CameraParams GlobeCamera::getParams() const {
31
42
 
32
43
  void GlobeCamera::setVerticalFovDegrees(double degrees) {
33
44
  std::lock_guard<std::mutex> lk(mutex_);
34
- verticalFovDeg_ = std::clamp(degrees, kMinVfov, kMaxVfov);
45
+ params_.verticalFov = std::clamp(degrees, kMinVfov, kMaxVfov);
35
46
  }
36
47
 
37
48
  double GlobeCamera::getVerticalFovDegrees() const {
38
49
  std::lock_guard<std::mutex> lk(mutex_);
39
- return verticalFovDeg_;
50
+ return params_.verticalFov;
40
51
  }
41
52
 
42
53
  void GlobeCamera::recompute() const {
@@ -151,7 +162,7 @@ GlobeCamera::computeViewState(double w, double h) const {
151
162
  recompute();
152
163
 
153
164
  double aspect = w / std::max(h, 1.0);
154
- double vfov = glm::radians(verticalFovDeg_);
165
+ double vfov = glm::radians(params_.verticalFov);
155
166
  double hfov = 2.0 * std::atan(std::tan(vfov * 0.5) * aspect);
156
167
 
157
168
  return Cesium3DTilesSelection::ViewState(
@@ -168,7 +179,7 @@ glm::mat4 GlobeCamera::computeVPMatrix(double w, double h) const {
168
179
  fullView[0], fullView[1], fullView[2], glm::dvec4(0.0, 0.0, 0.0, 1.0)));
169
180
 
170
181
  double aspect = w / std::max(h, 1.0);
171
- double vfov = glm::radians(verticalFovDeg_);
182
+ double vfov = glm::radians(params_.verticalFov);
172
183
  double tanHalfV = std::tan(vfov * 0.5);
173
184
  double tanHalfH = tanHalfV * aspect;
174
185
 
@@ -193,7 +204,7 @@ glm::dmat4 GlobeCamera::computeVPMatrixDouble(double w, double h) const {
193
204
  fullView[0], fullView[1], fullView[2], glm::dvec4(0.0, 0.0, 0.0, 1.0));
194
205
 
195
206
  double aspect = w / std::max(h, 1.0);
196
- double vfov = glm::radians(verticalFovDeg_);
207
+ double vfov = glm::radians(params_.verticalFov);
197
208
  double tanHalfV = std::tan(vfov * 0.5);
198
209
  double tanHalfH = tanHalfV * aspect;
199
210
 
@@ -11,12 +11,16 @@
11
11
  namespace reactnativecesium {
12
12
 
13
13
  struct CameraParams {
14
- double latitude = 46.10;
15
- double longitude = 7.80;
16
- double altitude = 5500.0;
17
- double heading = 220.0;
18
- double pitch = -10.0;
19
- double roll = 0.0;
14
+ double latitude = 46.10;
15
+ double longitude = 7.80;
16
+ double altitude = 5500.0;
17
+ double heading = 220.0;
18
+ double pitch = -10.0;
19
+ double roll = 0.0;
20
+ /// Vertical field of view in degrees. Clamped to [kMinVfov, kMaxVfov] by
21
+ /// GlobeCamera when written. Zero means "leave the previous value" — used
22
+ /// by partial constructors that don't want to opine on FOV.
23
+ double verticalFov = 60.0;
20
24
  /// Camera-space rotation applied after HPR (unit quaternion; w,x,y,z).
21
25
  glm::dquat viewCorrection{1.0, 0.0, 0.0, 0.0};
22
26
  };
@@ -28,6 +32,7 @@ public:
28
32
  void setParams(const CameraParams& params);
29
33
  CameraParams getParams() const;
30
34
 
35
+ /// Convenience helper — equivalent to mutating only verticalFov via setParams.
31
36
  void setVerticalFovDegrees(double degrees);
32
37
  double getVerticalFovDegrees() const;
33
38
 
@@ -56,8 +61,6 @@ private:
56
61
  mutable std::mutex mutex_;
57
62
  CameraParams params_;
58
63
 
59
- double verticalFovDeg_ = 60.0;
60
-
61
64
  mutable bool dirty_ = true;
62
65
  mutable glm::dvec3 ecefPosition_{0.0};
63
66
  mutable glm::dvec3 direction_{0.0, 1.0, 0.0};
@@ -0,0 +1,205 @@
1
+ // Self-contained host harness for CameraIntegrator. NOT compiled into the
2
+ // library — invoked manually by maintainers:
3
+ //
4
+ // $ bash cpp/engine/test/build_and_run.sh
5
+ //
6
+ // The integrator uses steady_clock internally, so the harness mixes real
7
+ // sleeps with calls — total wall-clock runtime is ~3 seconds.
8
+ //
9
+ // Each test prints PASS / FAIL with a short diagnostic. Exit code is the
10
+ // number of failures.
11
+
12
+ #include "../CameraIntegrator.hpp"
13
+ #include "../EngineTunables.hpp"
14
+
15
+ #include <chrono>
16
+ #include <cmath>
17
+ #include <cstdio>
18
+ #include <cstdlib>
19
+ #include <thread>
20
+
21
+ using namespace reactnativecesium;
22
+
23
+ static int g_failures = 0;
24
+
25
+ #define CHECK(cond, label) \
26
+ do { \
27
+ if (!(cond)) { \
28
+ std::fprintf(stderr, "FAIL [%s]: %s (line %d)\n", \
29
+ label, #cond, __LINE__); \
30
+ ++g_failures; \
31
+ } else { \
32
+ std::fprintf(stderr, "PASS [%s]\n", label); \
33
+ } \
34
+ } while (0)
35
+
36
+ static bool approxEq(double a, double b, double tol) {
37
+ return std::fabs(a - b) <= tol;
38
+ }
39
+
40
+ // Sleep a few ms then call step() so the integrator advances along the
41
+ // learned velocity. Returns the latest actual.
42
+ static CameraParams stepFor(CameraIntegrator& integ, int totalMs, int tickMs = 16) {
43
+ CameraParams out;
44
+ for (int t = 0; t < totalMs; t += tickMs) {
45
+ std::this_thread::sleep_for(std::chrono::milliseconds(tickMs));
46
+ out = integ.step();
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // 1. Step input: a single setter call should move the value toward the
52
+ // target by α × residual (no further movement until another measurement).
53
+ // Demand is preserved through getDemand() regardless.
54
+ static void testStepInput() {
55
+ CameraParams seed;
56
+ seed.altitude = 100.0;
57
+ CameraIntegrator integ(seed);
58
+
59
+ integ.setAltitude(200.0);
60
+ auto out = stepFor(integ, 80);
61
+
62
+ // Single shot: should be between 100 and 200, much closer to value+α*residual
63
+ CHECK(out.altitude > 100.0 && out.altitude < 200.0, "step_input.between");
64
+ CHECK(approxEq(integ.getDemand().altitude, 200.0, 1e-9),
65
+ "step_input.demand_preserved");
66
+ }
67
+
68
+ // 2. Repeated setter at constant velocity. Simulates a 10 Hz altitude feed
69
+ // climbing at 5 m/s. After enough samples, actual altitude should track
70
+ // demand to within a fraction of one inter-arrival delta.
71
+ static void testConstantVelocity() {
72
+ CameraIntegrator integ;
73
+ integ.teleport({}); // default seed: alt=5500
74
+
75
+ const double climbMps = 5.0;
76
+ const double dtSampleSec = 0.1;
77
+ const int numSamples = 25; // 2.5s of feed
78
+
79
+ double demandAlt = 5500.0;
80
+ for (int i = 0; i < numSamples; ++i) {
81
+ std::this_thread::sleep_for(std::chrono::milliseconds(
82
+ static_cast<int>(dtSampleSec * 1000)));
83
+ demandAlt += climbMps * dtSampleSec;
84
+ integ.setAltitude(demandAlt);
85
+ // A few render ticks between samples.
86
+ for (int k = 0; k < 6; ++k) {
87
+ std::this_thread::sleep_for(std::chrono::milliseconds(16));
88
+ integ.step();
89
+ }
90
+ }
91
+ auto actual = integ.getActual();
92
+ // Allow a tracking lag of one full sample delta.
93
+ CHECK(approxEq(actual.altitude, demandAlt, climbMps * dtSampleSec * 2.0),
94
+ "const_vel.tracks");
95
+ }
96
+
97
+ // 3. Rate cap bounds a single noisy heading spike.
98
+ static void testRateCap() {
99
+ CameraIntegrator integ;
100
+ CameraRateCaps caps;
101
+ caps.maxYawRateDegSec = 90.0;
102
+ integ.setRateCaps(caps);
103
+
104
+ integ.teleport({}); // default heading=220
105
+
106
+ // 10 calm samples at 220° to settle.
107
+ for (int i = 0; i < 10; ++i) {
108
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
109
+ integ.setHeading(220.0);
110
+ integ.step();
111
+ }
112
+ // Single 110° spike.
113
+ std::this_thread::sleep_for(std::chrono::milliseconds(20));
114
+ integ.setHeading(330.0);
115
+ auto out = integ.step();
116
+ // Without a cap a 110° step would produce v ≈ 110 / 0.02 = 5500 deg/s
117
+ // and the next step() would extrapolate by v*dt = 5500*0.02 = 110°.
118
+ // With the 90 deg/s cap, the change in this single frame is at most
119
+ // 90 * 0.02 ≈ 1.8° beyond the α*residual jump (~66°).
120
+ CHECK(std::fabs(out.heading - 220.0) < 90.0, "rate_cap.bounded");
121
+ }
122
+
123
+ // 4. dt clamp prevents a stale velocity vector from jumping after a long
124
+ // pause. We feed a 30 deg/s heading rate, then wait 5 real seconds with no
125
+ // further measurements, and check that the next step() only advances by
126
+ // kMaxFrameDtSec * velocity.
127
+ static void testLongPause() {
128
+ CameraIntegrator integ;
129
+ integ.teleport({});
130
+ // Build a yaw rate.
131
+ for (int i = 1; i <= 10; ++i) {
132
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
133
+ integ.setHeading(220.0 + 3.0 * i);
134
+ integ.step();
135
+ }
136
+ auto beforePause = integ.getActual();
137
+ // Long real-time pause with no setters and no steps.
138
+ std::this_thread::sleep_for(std::chrono::milliseconds(1500));
139
+ auto out = integ.step();
140
+ // dt was clamped to 0.1 s, so heading advanced by at most
141
+ // velocity * 0.1 s. With v ≈ 30 deg/s, that's at most 3°.
142
+ CHECK(std::fabs(out.heading - beforePause.heading) < 10.0,
143
+ "long_pause.dt_clamped");
144
+ }
145
+
146
+ // 5. Gesture-end bleed. A 60 Hz worklet streams ramping heading values for
147
+ // ~250 ms (simulating an active pan), then stops emitting entirely.
148
+ // Without the silence-aware velocity bleed the integrator would extrapolate
149
+ // the last learned velocity forever; with the bleed, motion should die in
150
+ // well under a second.
151
+ static void testGestureEndBleed() {
152
+ CameraIntegrator integ;
153
+ integ.teleport({});
154
+
155
+ // Active "gesture": 16 ms cadence, +0.5° heading per frame (= 31 deg/s).
156
+ double demand = 220.0;
157
+ for (int i = 0; i < 16; ++i) {
158
+ std::this_thread::sleep_for(std::chrono::milliseconds(16));
159
+ demand += 0.5;
160
+ integ.setHeading(demand);
161
+ integ.step();
162
+ }
163
+ const double duringHeading = integ.getActual().heading;
164
+
165
+ // "Release": no more setters, just render ticks at 60 Hz.
166
+ CameraParams out;
167
+ for (int i = 0; i < 60; ++i) {
168
+ std::this_thread::sleep_for(std::chrono::milliseconds(16));
169
+ out = integ.step();
170
+ }
171
+ const double endHeading = out.heading;
172
+ // Drift during the 1-second silence should be small — the integrator
173
+ // should have given up on the learned 31 deg/s rate well before the end.
174
+ CHECK(std::fabs(endHeading - duringHeading) < 5.0,
175
+ "gesture_end_bleed.stops");
176
+ }
177
+
178
+ // 6. Terrain clamp write-back — clampActualAltitude raises actual without
179
+ // touching demand.
180
+ static void testTerrainClamp() {
181
+ CameraIntegrator integ;
182
+ CameraParams seed;
183
+ seed.altitude = 100.0;
184
+ integ.teleport(seed);
185
+ integ.clampActualAltitude(150.0);
186
+ auto demand = integ.getDemand();
187
+ auto actual = integ.getActual();
188
+ CHECK(approxEq(actual.altitude, 150.0, 1e-6), "terrain_clamp.actual_raised");
189
+ CHECK(approxEq(demand.altitude, 100.0, 1e-6), "terrain_clamp.demand_preserved");
190
+ }
191
+
192
+ int main() {
193
+ testStepInput();
194
+ testConstantVelocity();
195
+ testRateCap();
196
+ testLongPause();
197
+ testGestureEndBleed();
198
+ testTerrainClamp();
199
+ if (g_failures == 0) {
200
+ std::fprintf(stderr, "\nALL TESTS PASSED.\n");
201
+ } else {
202
+ std::fprintf(stderr, "\n%d TEST(S) FAILED.\n", g_failures);
203
+ }
204
+ return g_failures;
205
+ }
@@ -0,0 +1,27 @@
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.
5
+
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
9
+ HEADERS="$ROOT/vendor/ios/CesiumNative.xcframework/ios-arm64-simulator/Headers"
10
+
11
+ if [[ ! -d "$HEADERS" ]]; then
12
+ echo "Cesium Native headers not found at $HEADERS"
13
+ echo "Run 'npm run update && CESIUM_BUILD_ONLY=ios npm run build' first."
14
+ exit 1
15
+ fi
16
+
17
+ OUT="$(mktemp -d)"
18
+ echo "Building into $OUT..."
19
+
20
+ xcrun -sdk macosx clang++ -std=c++20 -O0 -g \
21
+ -DGLM_FORCE_DEPTH_ZERO_TO_ONE=1 \
22
+ -I"$ROOT/cpp" -I"$HEADERS" \
23
+ -o "$OUT/cit" \
24
+ "$ROOT/cpp/engine/CameraIntegrator.cpp" \
25
+ "$ROOT/cpp/engine/test/CameraIntegratorTest.cpp"
26
+
27
+ "$OUT/cit"
package/example/README.md CHANGED
@@ -6,7 +6,7 @@ This app is the reference integration for `react-native-cesium`. For the current
6
6
 
7
7
  - rendering `CesiumView` inside a React Native screen
8
8
  - wiring a Nitro `hybridRef`
9
- - driving the camera with `setCamera(...)`
9
+ - driving the camera through the per-DoF setters (`setPosition`, `setAltitude`, `setHeading`, `setAttitude`) from a Reanimated worklet — see [`hooks/useCameraController.ts`](./hooks/useCameraController.ts)
10
10
  - receiving runtime telemetry through `onMetrics`
11
11
  - showing Cesium attribution text from `creditsPlainText`
12
12
 
@@ -3,9 +3,9 @@
3
3
 
4
4
  NS_ASSUME_NONNULL_BEGIN
5
5
 
6
- // Thin Obj-C++ shell for the iOS side. All cross-platform logic (camera
7
- // demand state + smoothing, FPS / metrics aggregation, credit-HTML stripping)
8
- // lives in shared C++ next to CesiumEngine.
6
+ // Thin Obj-C++ shell for the iOS side. All cross-platform logic (per-DoF
7
+ // camera integrator, FPS / metrics aggregation, credit-HTML stripping) lives
8
+ // in shared C++ next to CesiumEngine.
9
9
  @interface CesiumBridge : NSObject
10
10
 
11
11
  - (instancetype)initWithMetalLayer:(CAMetalLayer *)layer
@@ -15,32 +15,46 @@ NS_ASSUME_NONNULL_BEGIN
15
15
 
16
16
  - (void)updateIonAccessToken:(NSString *)token assetId:(int64_t)assetId;
17
17
  - (void)updateImageryAssetId:(int64_t)assetId;
18
- - (void)updateCameraLatitude:(double)lat
19
- longitude:(double)lon
20
- altitude:(double)alt
21
- heading:(double)heading
22
- pitch:(double)pitch
23
- roll:(double)roll;
24
- /// Same as `updateCameraLatitude:...` plus a camera-space view correction (w,x,y,z) applied after HPR.
25
- - (void)updateCameraQuaternionLatitude:(double)lat
26
- longitude:(double)lon
27
- altitude:(double)alt
28
- heading:(double)heading
29
- pitch:(double)pitch
30
- roll:(double)roll
31
- viewCorrectionW:(double)qw
32
- x:(double)qx
33
- y:(double)qy
34
- z:(double)qz;
18
+
19
+ // ── Per-DoF camera demand setters ────────────────────────────────────────
20
+ // Each call records the measurement with the current native steady_clock
21
+ // time and performs one α-β update against the predicted state at that
22
+ // instant. Pitch / roll are bundled because they typically come from a
23
+ // single IMU sample.
24
+ - (void)setPositionLatitude:(double)lat longitude:(double)lon;
25
+ - (void)setAltitude:(double)alt;
26
+ - (void)setHeadingDeg:(double)deg;
27
+ - (void)setAttitudePitch:(double)pitch roll:(double)roll;
28
+ - (void)setViewCorrectionW:(double)qw x:(double)qx y:(double)qy z:(double)qz;
29
+
30
+ /// Hard scene jump — resets every DoF (value + velocity + demand) atomically.
31
+ - (void)teleportLatitude:(double)lat
32
+ longitude:(double)lon
33
+ altitude:(double)alt
34
+ heading:(double)heading
35
+ pitch:(double)pitch
36
+ roll:(double)roll
37
+ verticalFovDeg:(double)vfov;
38
+
35
39
  - (void)resize:(int)width height:(int)height;
36
40
  - (BOOL)shouldRenderNextFrame;
37
41
  - (void)markNeedsRender;
38
- - (void)renderFrameWithDt:(double)dt;
42
+ /// Render one frame. `nowSeconds` is wall-clock seconds (steady_clock) used
43
+ /// by the integrator to extrapolate every DoF forward.
44
+ - (void)renderFrameAt:(double)nowSeconds;
39
45
  - (void)shutdown;
40
46
 
41
47
  // Camera / globe
42
48
  - (void)setVerticalFovDeg:(double)degrees;
43
49
 
50
+ /// Runtime rate caps (0 = uncapped). See EngineTunables for compile-time
51
+ /// defaults.
52
+ - (void)setRateCapsYaw:(double)yawDegSec
53
+ pitch:(double)pitchDegSec
54
+ roll:(double)rollDegSec
55
+ climb:(double)climbMps
56
+ groundSpeed:(double)groundMps;
57
+
44
58
  // Tileset tuning (runtime-mutable; no full rebuild)
45
59
  - (void)setMaximumScreenSpaceError:(double)v;
46
60
  - (void)setMaximumSimultaneousTileLoads:(int32_t)v;
@@ -73,13 +87,24 @@ NS_ASSUME_NONNULL_BEGIN
73
87
  @property (nonatomic, readonly) BOOL metricsTlsConfigured;
74
88
  @property (nonatomic, readonly, copy) NSString *metricsCreditsPlainText;
75
89
 
76
- - (double)readCameraLatitude;
77
- - (double)readCameraLongitude;
78
- - (double)readCameraAltitude;
79
- - (double)readCameraHeading;
80
- - (double)readCameraPitch;
81
- - (double)readCameraRoll;
82
- - (double)readVerticalFovDeg;
90
+ // ── Actual camera readback (what was rendered most recently) ────────────
91
+ - (double)readActualLatitude;
92
+ - (double)readActualLongitude;
93
+ - (double)readActualAltitude;
94
+ - (double)readActualHeading;
95
+ - (double)readActualPitch;
96
+ - (double)readActualRoll;
97
+ - (double)readActualVerticalFovDeg;
98
+
99
+ // ── Demand camera readback (what the consumer last asked for) ───────────
100
+ - (double)readDemandLatitude;
101
+ - (double)readDemandLongitude;
102
+ - (double)readDemandAltitude;
103
+ - (double)readDemandHeading;
104
+ - (double)readDemandPitch;
105
+ - (double)readDemandRoll;
106
+ - (double)readDemandVerticalFovDeg;
107
+
83
108
  - (double)readViewCorrectionW;
84
109
  - (double)readViewCorrectionX;
85
110
  - (double)readViewCorrectionY;