react-native-cesium 1.2.3 → 1.2.5

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 (44) hide show
  1. package/README.md +100 -27
  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 +199 -187
  7. package/cpp/engine/CameraIntegrator.cpp +374 -0
  8. package/cpp/engine/CameraIntegrator.hpp +146 -0
  9. package/cpp/engine/EngineTunables.hpp +90 -31
  10. package/cpp/engine/GlobeCamera.cpp +17 -6
  11. package/cpp/engine/GlobeCamera.hpp +11 -8
  12. package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
  13. package/cpp/engine/test/build_and_run.sh +27 -0
  14. package/example/README.md +1 -1
  15. package/ios/CesiumBridge.h +53 -28
  16. package/ios/CesiumBridge.mm +172 -86
  17. package/ios/HybridCesiumView.swift +256 -145
  18. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +85 -13
  19. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  20. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +2 -0
  21. package/nitrogen/generated/android/c++/JFunc_void_CameraState.hpp +77 -0
  22. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +108 -9
  23. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +21 -3
  24. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +24 -0
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CameraState.kt +80 -0
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +71 -3
  27. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.cpp +3 -3
  28. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +43 -28
  29. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +89 -9
  30. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +30 -0
  31. package/nitrogen/generated/ios/swift/Func_void_CameraState.swift +5 -5
  32. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +15 -3
  33. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +242 -16
  34. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +21 -3
  35. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +22 -4
  36. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +66 -0
  37. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +6 -0
  38. package/nitrogen/generated/shared/json/CesiumViewConfig.json +6 -0
  39. package/package.json +1 -1
  40. package/src/specs/cesium-view.nitro.ts +102 -14
  41. package/cpp/engine/CameraSmoother.cpp +0 -68
  42. package/cpp/engine/CameraSmoother.hpp +0 -28
  43. package/cpp/engine/CameraTargetState.cpp +0 -205
  44. package/cpp/engine/CameraTargetState.hpp +0 -126
@@ -25,8 +25,8 @@ namespace margelo::nitro::reactnativecesium { struct Quaternion; }
25
25
  #include <optional>
26
26
  #include "CesiumMetrics.hpp"
27
27
  #include <functional>
28
- #include <NitroModules/Promise.hpp>
29
28
  #include "Quaternion.hpp"
29
+ #include <NitroModules/Promise.hpp>
30
30
 
31
31
  namespace margelo::nitro::reactnativecesium {
32
32
 
@@ -99,14 +99,32 @@ namespace margelo::nitro::reactnativecesium {
99
99
  virtual void setTaskProcessorThreads(std::optional<double> taskProcessorThreads) = 0;
100
100
  virtual std::optional<double> getMinAltitudeAboveTerrain() = 0;
101
101
  virtual void setMinAltitudeAboveTerrain(std::optional<double> minAltitudeAboveTerrain) = 0;
102
+ virtual std::optional<double> getMaxYawRateDegSec() = 0;
103
+ virtual void setMaxYawRateDegSec(std::optional<double> maxYawRateDegSec) = 0;
104
+ virtual std::optional<double> getMaxPitchRateDegSec() = 0;
105
+ virtual void setMaxPitchRateDegSec(std::optional<double> maxPitchRateDegSec) = 0;
106
+ virtual std::optional<double> getMaxRollRateDegSec() = 0;
107
+ virtual void setMaxRollRateDegSec(std::optional<double> maxRollRateDegSec) = 0;
108
+ virtual std::optional<double> getMaxClimbRateMps() = 0;
109
+ virtual void setMaxClimbRateMps(std::optional<double> maxClimbRateMps) = 0;
110
+ virtual std::optional<double> getMaxGroundSpeedMps() = 0;
111
+ virtual void setMaxGroundSpeedMps(std::optional<double> maxGroundSpeedMps) = 0;
102
112
  virtual std::optional<std::function<void(const CesiumMetrics& /* metrics */)>> getOnMetrics() = 0;
103
113
  virtual void setOnMetrics(const std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>& onMetrics) = 0;
114
+ virtual std::optional<std::function<void(const CameraState& /* camera */)>> getOnActualCamera() = 0;
115
+ virtual void setOnActualCamera(const std::optional<std::function<void(const CameraState& /* camera */)>>& onActualCamera) = 0;
104
116
 
105
117
  public:
106
118
  // Methods
107
- virtual std::shared_ptr<Promise<CameraState>> getCameraState() = 0;
108
- virtual void setCamera(const CameraState& camera) = 0;
109
- virtual void setCameraQuaternion(const CameraState& camera, const Quaternion& viewCorrection) = 0;
119
+ virtual void setPosition(double latitude, double longitude) = 0;
120
+ virtual void setAltitude(double altitudeMeters) = 0;
121
+ virtual void setHeading(double headingDeg) = 0;
122
+ virtual void setAttitude(double pitchDeg, double rollDeg) = 0;
123
+ virtual void setViewCorrection(const Quaternion& q) = 0;
124
+ virtual void setVerticalFov(double deg) = 0;
125
+ virtual void teleport(const CameraState& camera) = 0;
126
+ virtual std::shared_ptr<Promise<CameraState>> getActualCamera() = 0;
127
+ virtual std::shared_ptr<Promise<CameraState>> getDemandCamera() = 0;
110
128
  virtual std::shared_ptr<Promise<Quaternion>> getViewCorrection() = 0;
111
129
 
112
130
  protected:
@@ -246,6 +246,56 @@ namespace margelo::nitro::reactnativecesium::views {
246
246
  throw std::runtime_error(std::string("CesiumView.minAltitudeAboveTerrain: ") + exc.what());
247
247
  }
248
248
  }()),
249
+ maxYawRateDegSec([&]() -> CachedProp<std::optional<double>> {
250
+ try {
251
+ const react::RawValue* rawValue = rawProps.at("maxYawRateDegSec", nullptr, nullptr);
252
+ if (rawValue == nullptr) return sourceProps.maxYawRateDegSec;
253
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
254
+ return CachedProp<std::optional<double>>::fromRawValue(*runtime, value, sourceProps.maxYawRateDegSec);
255
+ } catch (const std::exception& exc) {
256
+ throw std::runtime_error(std::string("CesiumView.maxYawRateDegSec: ") + exc.what());
257
+ }
258
+ }()),
259
+ maxPitchRateDegSec([&]() -> CachedProp<std::optional<double>> {
260
+ try {
261
+ const react::RawValue* rawValue = rawProps.at("maxPitchRateDegSec", nullptr, nullptr);
262
+ if (rawValue == nullptr) return sourceProps.maxPitchRateDegSec;
263
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
264
+ return CachedProp<std::optional<double>>::fromRawValue(*runtime, value, sourceProps.maxPitchRateDegSec);
265
+ } catch (const std::exception& exc) {
266
+ throw std::runtime_error(std::string("CesiumView.maxPitchRateDegSec: ") + exc.what());
267
+ }
268
+ }()),
269
+ maxRollRateDegSec([&]() -> CachedProp<std::optional<double>> {
270
+ try {
271
+ const react::RawValue* rawValue = rawProps.at("maxRollRateDegSec", nullptr, nullptr);
272
+ if (rawValue == nullptr) return sourceProps.maxRollRateDegSec;
273
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
274
+ return CachedProp<std::optional<double>>::fromRawValue(*runtime, value, sourceProps.maxRollRateDegSec);
275
+ } catch (const std::exception& exc) {
276
+ throw std::runtime_error(std::string("CesiumView.maxRollRateDegSec: ") + exc.what());
277
+ }
278
+ }()),
279
+ maxClimbRateMps([&]() -> CachedProp<std::optional<double>> {
280
+ try {
281
+ const react::RawValue* rawValue = rawProps.at("maxClimbRateMps", nullptr, nullptr);
282
+ if (rawValue == nullptr) return sourceProps.maxClimbRateMps;
283
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
284
+ return CachedProp<std::optional<double>>::fromRawValue(*runtime, value, sourceProps.maxClimbRateMps);
285
+ } catch (const std::exception& exc) {
286
+ throw std::runtime_error(std::string("CesiumView.maxClimbRateMps: ") + exc.what());
287
+ }
288
+ }()),
289
+ maxGroundSpeedMps([&]() -> CachedProp<std::optional<double>> {
290
+ try {
291
+ const react::RawValue* rawValue = rawProps.at("maxGroundSpeedMps", nullptr, nullptr);
292
+ if (rawValue == nullptr) return sourceProps.maxGroundSpeedMps;
293
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
294
+ return CachedProp<std::optional<double>>::fromRawValue(*runtime, value, sourceProps.maxGroundSpeedMps);
295
+ } catch (const std::exception& exc) {
296
+ throw std::runtime_error(std::string("CesiumView.maxGroundSpeedMps: ") + exc.what());
297
+ }
298
+ }()),
249
299
  onMetrics([&]() -> CachedProp<std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>> {
250
300
  try {
251
301
  const react::RawValue* rawValue = rawProps.at("onMetrics", nullptr, nullptr);
@@ -256,6 +306,16 @@ namespace margelo::nitro::reactnativecesium::views {
256
306
  throw std::runtime_error(std::string("CesiumView.onMetrics: ") + exc.what());
257
307
  }
258
308
  }()),
309
+ onActualCamera([&]() -> CachedProp<std::optional<std::function<void(const CameraState& /* camera */)>>> {
310
+ try {
311
+ const react::RawValue* rawValue = rawProps.at("onActualCamera", nullptr, nullptr);
312
+ if (rawValue == nullptr) return sourceProps.onActualCamera;
313
+ const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
314
+ return CachedProp<std::optional<std::function<void(const CameraState& /* camera */)>>>::fromRawValue(*runtime, value.asObject(*runtime).getProperty(*runtime, PropNameIDCache::get(*runtime, "f")), sourceProps.onActualCamera);
315
+ } catch (const std::exception& exc) {
316
+ throw std::runtime_error(std::string("CesiumView.onActualCamera: ") + exc.what());
317
+ }
318
+ }()),
259
319
  hybridRef([&]() -> CachedProp<std::optional<std::function<void(const std::shared_ptr<HybridCesiumViewSpec>& /* ref */)>>> {
260
320
  try {
261
321
  const react::RawValue* rawValue = rawProps.at("hybridRef", nullptr, nullptr);
@@ -291,7 +351,13 @@ namespace margelo::nitro::reactnativecesium::views {
291
351
  case hashString("sqliteCacheMaxRows"): return true;
292
352
  case hashString("taskProcessorThreads"): return true;
293
353
  case hashString("minAltitudeAboveTerrain"): return true;
354
+ case hashString("maxYawRateDegSec"): return true;
355
+ case hashString("maxPitchRateDegSec"): return true;
356
+ case hashString("maxRollRateDegSec"): return true;
357
+ case hashString("maxClimbRateMps"): return true;
358
+ case hashString("maxGroundSpeedMps"): return true;
294
359
  case hashString("onMetrics"): return true;
360
+ case hashString("onActualCamera"): return true;
295
361
  case hashString("hybridRef"): return true;
296
362
  default: return false;
297
363
  }
@@ -66,7 +66,13 @@ namespace margelo::nitro::reactnativecesium::views {
66
66
  CachedProp<std::optional<double>> sqliteCacheMaxRows;
67
67
  CachedProp<std::optional<double>> taskProcessorThreads;
68
68
  CachedProp<std::optional<double>> minAltitudeAboveTerrain;
69
+ CachedProp<std::optional<double>> maxYawRateDegSec;
70
+ CachedProp<std::optional<double>> maxPitchRateDegSec;
71
+ CachedProp<std::optional<double>> maxRollRateDegSec;
72
+ CachedProp<std::optional<double>> maxClimbRateMps;
73
+ CachedProp<std::optional<double>> maxGroundSpeedMps;
69
74
  CachedProp<std::optional<std::function<void(const CesiumMetrics& /* metrics */)>>> onMetrics;
75
+ CachedProp<std::optional<std::function<void(const CameraState& /* camera */)>>> onActualCamera;
70
76
  CachedProp<std::optional<std::function<void(const std::shared_ptr<HybridCesiumViewSpec>& /* ref */)>>> hybridRef;
71
77
 
72
78
  private:
@@ -26,7 +26,13 @@
26
26
  "sqliteCacheMaxRows": true,
27
27
  "taskProcessorThreads": true,
28
28
  "minAltitudeAboveTerrain": true,
29
+ "maxYawRateDegSec": true,
30
+ "maxPitchRateDegSec": true,
31
+ "maxRollRateDegSec": true,
32
+ "maxClimbRateMps": true,
33
+ "maxGroundSpeedMps": true,
29
34
  "onMetrics": true,
35
+ "onActualCamera": true,
30
36
  "hybridRef": true
31
37
  }
32
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Experimental Cesium rendering for React Native via Nitro (iOS/Metal today; Android planned)",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -4,7 +4,10 @@ import type {
4
4
  HybridViewMethods,
5
5
  } from 'react-native-nitro-modules'
6
6
 
7
- /** Snapshot of the native camera for persistence / sync with React state. */
7
+ /**
8
+ * Snapshot of the native camera for persistence / sync with React state.
9
+ * Returned by `getActualCamera()` and `getDemandCamera()`.
10
+ */
8
11
  export interface CameraState {
9
12
  latitude: number
10
13
  longitude: number
@@ -16,8 +19,9 @@ export interface CameraState {
16
19
  }
17
20
 
18
21
  /**
19
- * Unit quaternion (w + xi + yj + zk). Used for camera-space view correction.
20
- * Non-unit values are normalized on the native side.
22
+ * Unit quaternion (w + xi + yj + zk). Used for camera-space view correction
23
+ * applied after HPR (boresight / HUD alignment). Non-unit values are
24
+ * normalised on the native side.
21
25
  */
22
26
  export interface Quaternion {
23
27
  w: number
@@ -42,7 +46,11 @@ export interface CesiumMetrics {
42
46
  export interface CesiumViewProps extends HybridViewProps {
43
47
  ionAccessToken: string
44
48
  ionAssetId: number
45
- /** Construction-time seed camera. Use `setCamera()` for runtime updates. */
49
+ /**
50
+ * Initial camera applied via `teleport(...)` once when the native bridge is
51
+ * created. Subsequent prop writes are ignored — use the per-DoF setters or
52
+ * `teleport(...)` for runtime updates.
53
+ */
46
54
  initialCamera: CameraState
47
55
  pauseRendering: boolean
48
56
  maximumScreenSpaceError: number
@@ -52,7 +60,7 @@ export interface CesiumViewProps extends HybridViewProps {
52
60
  msaaSampleCount: number
53
61
  ionImageryAssetId: number
54
62
 
55
- // ── Optional perf / quality knobs (added in 1.1) ─────────────────────
63
+ // ── Optional perf / quality knobs ────────────────────────────────────────
56
64
  /** RAM budget for live tileset, in mebibytes. Default 256. */
57
65
  maximumCachedMiB?: number
58
66
  /** Pre-load ancestor tiles around the visible set (smoother panning, more loads). Default true. */
@@ -85,23 +93,103 @@ export interface CesiumViewProps extends HybridViewProps {
85
93
  */
86
94
  minAltitudeAboveTerrain?: number
87
95
 
96
+ // ── Optional rate caps (flight-sim feel) ─────────────────────────────────
97
+ // All defaults: 0 = uncapped. Override per-screen to enforce a maximum
98
+ // angular / linear speed on the integrator's per-DoF velocity. Useful when
99
+ // your sensor feed is noisy and you would rather drop the change than
100
+ // visually whip the camera.
101
+ /** Maximum yaw rate (degrees per second). 0 = uncapped. */
102
+ maxYawRateDegSec?: number
103
+ /** Maximum pitch rate (degrees per second). 0 = uncapped. */
104
+ maxPitchRateDegSec?: number
105
+ /** Maximum roll rate (degrees per second). 0 = uncapped. */
106
+ maxRollRateDegSec?: number
107
+ /** Maximum climb / descent rate (metres per second). 0 = uncapped. */
108
+ maxClimbRateMps?: number
109
+ /** Maximum horizontal ground speed (metres per second). 0 = uncapped. */
110
+ maxGroundSpeedMps?: number
111
+
88
112
  onMetrics?: (metrics: CesiumMetrics) => void
113
+
114
+ /**
115
+ * Called at ~5 Hz with the post-integration actual camera — the same state
116
+ * returned by `getActualCamera()`. Only fires when at least one field has
117
+ * changed beyond its epsilon threshold since the last emission, so a
118
+ * stationary camera produces no bridge traffic. Use this to keep HUD overlay
119
+ * symbology aligned with the rendered view without polling from JS.
120
+ */
121
+ onActualCamera?: (camera: CameraState) => void
89
122
  }
90
123
 
124
+ /**
125
+ * Per-DoF camera control. Every setter records the measurement with the
126
+ * native steady_clock time of arrival and feeds it into an α-β tracker
127
+ * (constant-velocity predictor) that owns both demand and actual state.
128
+ *
129
+ * All setters are synchronous and thread-safe. Calling them from a
130
+ * Reanimated worklet via a Nitro `hybridRef` is the supported path for
131
+ * high-frequency updates; the worklet thread takes the integrator's mutex
132
+ * briefly and returns immediately.
133
+ *
134
+ * Getters return Promises and should be called from the JS thread (e.g. a
135
+ * `useEffect` or a throttled HUD update), not from a worklet.
136
+ */
91
137
  export interface CesiumViewMethods extends HybridViewMethods {
92
- /** Returns the current native camera state. */
93
- getCameraState(): Promise<CameraState>
94
138
  /**
95
- * Runtime camera control (heading/pitch/roll + position + VFOV).
96
- * Does not change the view-correction quaternion; use `setCameraQuaternion` to set that.
139
+ * New geographic position demand (latitude / longitude in degrees). Use
140
+ * for GPS or scripted position updates. Latitude is clamped to ±90 by the
141
+ * integrator; longitude is continuous (no wrap normalisation) so the
142
+ * shortest-arc residual maths stays correct across the antimeridian.
97
143
  */
98
- setCamera(camera: CameraState): void
144
+ setPosition(latitude: number, longitude: number): void
145
+
146
+ /** New altitude demand (metres above mean sea level). */
147
+ setAltitude(altitudeMeters: number): void
148
+
149
+ /** New heading demand (degrees, 0 = north, increasing clockwise). */
150
+ setHeading(headingDeg: number): void
151
+
99
152
  /**
100
- * Same fields as `setCamera`, plus a camera-space rotation applied after HPR
101
- * (e.g. boresight / HUD alignment). See README.
153
+ * New attitude demand (pitch and roll in degrees). Bundled because almost
154
+ * every IMU emits them together — but you can pass the previous value for
155
+ * either axis if you only have one of them.
102
156
  */
103
- setCameraQuaternion(camera: CameraState, viewCorrection: Quaternion): void
104
- /** Current view-correction quaternion (identity if never set via `setCameraQuaternion`). */
157
+ setAttitude(pitchDeg: number, rollDeg: number): void
158
+
159
+ /**
160
+ * New camera-space rotation applied after HPR (unit quaternion). Use for
161
+ * boresight calibration or screen-fixed HUD alignment. SLERPed toward the
162
+ * latest demand each frame.
163
+ */
164
+ setViewCorrection(q: Quaternion): void
165
+
166
+ /** New vertical field-of-view demand (degrees). Clamped to 20..100. */
167
+ setVerticalFov(deg: number): void
168
+
169
+ /**
170
+ * Atomically reset every DoF to the given camera state (value, velocity
171
+ * and demand all snap to the target). Bypasses the integrator entirely;
172
+ * use for `Fly to coordinate` actions or initial seeding from saved
173
+ * state.
174
+ */
175
+ teleport(camera: CameraState): void
176
+
177
+ /**
178
+ * Most recent camera that was rendered (post-integration). This is what
179
+ * the user is currently looking at and is what a HUD overlay should
180
+ * mirror. Differs from `getDemandCamera()` while a glide is in progress
181
+ * or when the terrain-floor clamp has raised the altitude.
182
+ */
183
+ getActualCamera(): Promise<CameraState>
184
+
185
+ /**
186
+ * What the consumer last asked for (per-DoF demand). Useful for
187
+ * diagnostics or for showing the requested camera alongside the rendered
188
+ * one.
189
+ */
190
+ getDemandCamera(): Promise<CameraState>
191
+
192
+ /** Current view-correction quaternion (smoothed toward the latest demand). */
105
193
  getViewCorrection(): Promise<Quaternion>
106
194
  }
107
195
 
@@ -1,68 +0,0 @@
1
- #include "CameraSmoother.hpp"
2
-
3
- #include "EngineTunables.hpp"
4
-
5
- #include <cmath>
6
-
7
- namespace reactnativecesium {
8
-
9
- namespace {
10
-
11
- inline double lerpAngleDeg(double a, double b, double t) {
12
- // Wraps difference to [-180, 180] before applying the mix factor so we
13
- // always rotate the short way around the unit circle.
14
- double diff = std::fmod(b - a + 540.0, 360.0) - 180.0;
15
- return a + diff * t;
16
- }
17
-
18
- inline double angleDeltaAbsDeg(double a, double b) {
19
- return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
20
- }
21
-
22
- inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
23
- return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
24
- }
25
-
26
- } // namespace
27
-
28
- CameraParams CameraSmoother::step(
29
- const CameraParams& current, const CameraParams& target, double dt) {
30
- CameraParams next = current;
31
-
32
- // lat / lon / altitude / heading: snap directly to the (already linearly-
33
- // interpolated) target from CameraTargetState::snapshot(). The constant-
34
- // velocity motion is fully handled there; no additional smoothing is applied
35
- // here so the movement stays linear rather than decelerating.
36
- next.latitude = target.latitude;
37
- next.longitude = target.longitude;
38
- next.altitude = target.altitude;
39
- next.heading = target.heading;
40
-
41
- // pitch / roll / viewCorrection: exponential ease-out. These are almost
42
- // always driven by high-rate attitude sensors (accelerometer, gyroscope)
43
- // where the ease-out feel is appropriate and desired.
44
- const double aPitch = 1.0 - std::exp(-tunables::kSmoothPitch * dt);
45
- const double aRoll = 1.0 - std::exp(-tunables::kSmoothRoll * dt);
46
- const double aViewQ = 1.0 - std::exp(-tunables::kSmoothViewCorr * dt);
47
-
48
- next.pitch = lerpAngleDeg(current.pitch, target.pitch, aPitch);
49
- next.roll = lerpAngleDeg(current.roll, target.roll, aRoll);
50
-
51
- glm::dquat cq = glm::normalize(current.viewCorrection);
52
- glm::dquat tq = glm::normalize(target.viewCorrection);
53
- if (glm::dot(cq, tq) < 0.0) tq = -tq;
54
- next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
55
-
56
- // Eventually-exact convergence for exponential DoFs: snap once sub-epsilon.
57
- if (angleDeltaAbsDeg(next.pitch, target.pitch) <= tunables::kEpsAngleDeg)
58
- next.pitch = target.pitch;
59
- if (angleDeltaAbsDeg(next.roll, target.roll) <= tunables::kEpsAngleDeg)
60
- next.roll = target.roll;
61
- if ((1.0 - quatDotAbs(next.viewCorrection, target.viewCorrection))
62
- <= tunables::kEpsQuatDot)
63
- next.viewCorrection = target.viewCorrection;
64
-
65
- return next;
66
- }
67
-
68
- } // namespace reactnativecesium
@@ -1,28 +0,0 @@
1
- #pragma once
2
-
3
- #include "GlobeCamera.hpp"
4
-
5
- namespace reactnativecesium {
6
-
7
- // Stateless helper. Given a current camera state and a target camera state,
8
- // returns the next camera state for a frame of duration `dt` seconds.
9
- //
10
- // lat / lon / altitude / heading are snapped directly to the target.
11
- // CameraTargetState::snapshot() has already linearly interpolated those DoFs
12
- // to their "current" position based on each DoF's own EWMA inter-arrival
13
- // interval, so snapping here gives constant-velocity motion between fixes
14
- // with no ease-in/ease-out. At gesture rates (~60 Hz) the EWMA collapses to
15
- // ~16 ms so the snap is indistinguishable from the previous behaviour.
16
- //
17
- // pitch / roll / viewCorrection use fixed-rate exponential decay
18
- // `1 - exp(-k * dt)` with k from EngineTunables — appropriate for high-rate
19
- // attitude sensors where the ease-out feel is desirable.
20
- // Once a DoF drops below its matching epsilon it is snapped to exact equality.
21
- class CameraSmoother {
22
- public:
23
- static CameraParams step(const CameraParams& current,
24
- const CameraParams& target,
25
- double dt);
26
- };
27
-
28
- } // namespace reactnativecesium
@@ -1,205 +0,0 @@
1
- #include "CameraTargetState.hpp"
2
-
3
- #include <algorithm>
4
- #include <cmath>
5
-
6
- namespace reactnativecesium {
7
-
8
- namespace {
9
-
10
- inline double angleDeltaAbsDeg(double a, double b) {
11
- return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
12
- }
13
-
14
- inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
15
- return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
16
- }
17
-
18
- // Shortest signed angular delta wrapped to [-180, 180].
19
- inline double shortAngleDeltaDeg(double from, double to) {
20
- return std::fmod(to - from + 540.0, 360.0) - 180.0;
21
- }
22
-
23
- // Advance an EWMA with a new sample, or seed it on first use.
24
- inline void updateEwma(double& ewma, bool& active,
25
- std::chrono::steady_clock::time_point& lastTime,
26
- const std::chrono::steady_clock::time_point& now,
27
- double seedSec) {
28
- if (!active) {
29
- ewma = seedSec;
30
- lastTime = now;
31
- active = true;
32
- } else {
33
- const double dt = std::chrono::duration<double>(now - lastTime).count();
34
- if (dt > 0.0) {
35
- ewma = ewma + tunables::kEwmaIntervalAlpha * (dt - ewma);
36
- }
37
- lastTime = now;
38
- }
39
- }
40
-
41
- } // namespace
42
-
43
- // ── Public writers ────────────────────────────────────────────────────────────
44
-
45
- void CameraTargetState::setAll(const CameraParams& target) {
46
- {
47
- std::lock_guard<std::mutex> g(mutex_);
48
- target_ = target;
49
- // Deliberately do NOT touch any interpolation state: setAll is init-only
50
- // and the first rendered frame must snap to the requested position.
51
- }
52
- dirty_.store(true, std::memory_order_release);
53
- forceRender_.store(true, std::memory_order_release);
54
- }
55
-
56
- void CameraTargetState::setHpr(double lat, double lon, double alt,
57
- double heading, double pitch, double roll) {
58
- {
59
- std::lock_guard<std::mutex> g(mutex_);
60
-
61
- const auto now = std::chrono::steady_clock::now();
62
-
63
- // ── lat / lon ─────────────────────────────────────────────────────────
64
- const bool posChanged =
65
- std::abs(lat - target_.latitude) > tunables::kEpsLatLon ||
66
- std::abs(lon - target_.longitude) > tunables::kEpsLatLon;
67
-
68
- if (posChanged) {
69
- if (!hasPosInterp_) {
70
- // First runtime change: seed "from" = destination for a clean snap,
71
- // then enable interpolation for subsequent updates.
72
- posFromLat_ = lat;
73
- posFromLon_ = lon;
74
- updateEwma(posEwmaIntervalSec_, hasPosInterp_, posUpdateTime_, now,
75
- tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
76
- } else {
77
- posFromLat_ = target_.latitude;
78
- posFromLon_ = target_.longitude;
79
- updateEwma(posEwmaIntervalSec_, hasPosInterp_, posUpdateTime_, now,
80
- tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
81
- }
82
- }
83
-
84
- // ── altitude ──────────────────────────────────────────────────────────
85
- const bool altChanged =
86
- std::abs(alt - target_.altitude) > tunables::kEpsAltitudeMeters;
87
-
88
- if (altChanged) {
89
- if (!hasAltInterp_) {
90
- altFrom_ = alt;
91
- updateEwma(altEwmaIntervalSec_, hasAltInterp_, altUpdateTime_, now,
92
- tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
93
- } else {
94
- altFrom_ = target_.altitude;
95
- updateEwma(altEwmaIntervalSec_, hasAltInterp_, altUpdateTime_, now,
96
- tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
97
- }
98
- }
99
-
100
- // ── heading ───────────────────────────────────────────────────────────
101
- const bool hdgChanged =
102
- angleDeltaAbsDeg(heading, target_.heading) > tunables::kEpsAngleDeg;
103
-
104
- if (hdgChanged) {
105
- if (!hasHdgInterp_) {
106
- hdgFrom_ = heading;
107
- updateEwma(hdgEwmaIntervalSec_, hasHdgInterp_, hdgUpdateTime_, now,
108
- tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
109
- } else {
110
- hdgFrom_ = target_.heading;
111
- updateEwma(hdgEwmaIntervalSec_, hasHdgInterp_, hdgUpdateTime_, now,
112
- tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
113
- }
114
- }
115
-
116
- target_.latitude = lat;
117
- target_.longitude = lon;
118
- target_.altitude = alt;
119
- target_.heading = heading;
120
- target_.pitch = pitch;
121
- target_.roll = roll;
122
- }
123
- dirty_.store(true, std::memory_order_release);
124
- forceRender_.store(true, std::memory_order_release);
125
- }
126
-
127
- void CameraTargetState::setViewCorrection(const glm::dquat& q) {
128
- {
129
- std::lock_guard<std::mutex> g(mutex_);
130
- target_.viewCorrection = q;
131
- // Interpolation state is not touched — viewCorrection carries no
132
- // positional information and must not affect any of the three EWMAs.
133
- }
134
- dirty_.store(true, std::memory_order_release);
135
- forceRender_.store(true, std::memory_order_release);
136
- }
137
-
138
- // ── Snapshot (render-thread read) ─────────────────────────────────────────────
139
-
140
- CameraParams CameraTargetState::snapshot() const {
141
- const auto now = std::chrono::steady_clock::now();
142
- std::lock_guard<std::mutex> g(mutex_);
143
-
144
- CameraParams result = target_;
145
-
146
- // ── lat / lon ─────────────────────────────────────────────────────────────
147
- if (hasPosInterp_ && posEwmaIntervalSec_ > 0.0) {
148
- const double dLat = target_.latitude - posFromLat_;
149
- const double dLon = shortAngleDeltaDeg(posFromLon_, target_.longitude);
150
-
151
- const bool snapThrough =
152
- std::abs(dLat) > tunables::kPosSnapThroughDeg ||
153
- std::abs(dLon) > tunables::kPosSnapThroughDeg;
154
-
155
- if (!snapThrough) {
156
- const double elapsed =
157
- std::chrono::duration<double>(now - posUpdateTime_).count();
158
- const double t = std::min(elapsed / posEwmaIntervalSec_, 1.0);
159
- result.latitude = posFromLat_ + t * dLat;
160
- result.longitude = posFromLon_ + t * dLon;
161
- }
162
- }
163
-
164
- // ── altitude ──────────────────────────────────────────────────────────────
165
- if (hasAltInterp_ && altEwmaIntervalSec_ > 0.0) {
166
- const double dAlt = target_.altitude - altFrom_;
167
-
168
- if (std::abs(dAlt) <= tunables::kAltSnapThroughMeters) {
169
- const double elapsed =
170
- std::chrono::duration<double>(now - altUpdateTime_).count();
171
- const double t = std::min(elapsed / altEwmaIntervalSec_, 1.0);
172
- result.altitude = altFrom_ + t * dAlt;
173
- }
174
- }
175
-
176
- // ── heading ───────────────────────────────────────────────────────────────
177
- if (hasHdgInterp_ && hdgEwmaIntervalSec_ > 0.0) {
178
- const double dHdg = shortAngleDeltaDeg(hdgFrom_, target_.heading);
179
-
180
- if (std::abs(dHdg) <= tunables::kHdgSnapThroughDeg) {
181
- const double elapsed =
182
- std::chrono::duration<double>(now - hdgUpdateTime_).count();
183
- const double t = std::min(elapsed / hdgEwmaIntervalSec_, 1.0);
184
- result.heading = hdgFrom_ + t * dHdg;
185
- }
186
- }
187
-
188
- return result;
189
- }
190
-
191
- // ── Epsilon check (shared) ────────────────────────────────────────────────────
192
-
193
- bool CameraTargetState::deltaExceedsEpsilon(
194
- const CameraParams& a, const CameraParams& b) {
195
- return std::abs(a.latitude - b.latitude) > tunables::kEpsLatLon
196
- || std::abs(a.longitude - b.longitude) > tunables::kEpsLatLon
197
- || std::abs(a.altitude - b.altitude) > tunables::kEpsAltitudeMeters
198
- || angleDeltaAbsDeg(a.heading, b.heading) > tunables::kEpsAngleDeg
199
- || angleDeltaAbsDeg(a.pitch, b.pitch) > tunables::kEpsAngleDeg
200
- || angleDeltaAbsDeg(a.roll, b.roll) > tunables::kEpsAngleDeg
201
- || (1.0 - quatDotAbs(a.viewCorrection, b.viewCorrection))
202
- > tunables::kEpsQuatDot;
203
- }
204
-
205
- } // namespace reactnativecesium