react-native-cesium 1.2.3 → 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 (39) 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 +160 -187
  7. package/cpp/engine/CameraIntegrator.cpp +374 -0
  8. package/cpp/engine/CameraIntegrator.hpp +146 -0
  9. package/cpp/engine/EngineTunables.hpp +78 -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 +213 -145
  18. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +77 -13
  19. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  20. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +90 -9
  21. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +19 -3
  22. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +20 -0
  23. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +57 -3
  24. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +9 -9
  25. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +82 -9
  26. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +25 -0
  27. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +14 -3
  28. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +210 -16
  29. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +19 -3
  30. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +20 -4
  31. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +55 -0
  32. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +5 -0
  33. package/nitrogen/generated/shared/json/CesiumViewConfig.json +5 -0
  34. package/package.json +1 -1
  35. package/src/specs/cesium-view.nitro.ts +93 -14
  36. package/cpp/engine/CameraSmoother.cpp +0 -68
  37. package/cpp/engine/CameraSmoother.hpp +0 -28
  38. package/cpp/engine/CameraTargetState.cpp +0 -205
  39. package/cpp/engine/CameraTargetState.hpp +0 -126
@@ -0,0 +1,374 @@
1
+ #include "CameraIntegrator.hpp"
2
+
3
+ #include "EngineTunables.hpp"
4
+
5
+ #include <glm/gtc/quaternion.hpp>
6
+
7
+ #include <algorithm>
8
+ #include <cmath>
9
+
10
+ namespace reactnativecesium {
11
+
12
+ namespace {
13
+
14
+ // WGS84 mean radius (m). Hard-coded so the integrator can be unit-tested in
15
+ // isolation without linking CesiumGeospatial. Matches Ellipsoid::WGS84.
16
+ inline constexpr double kEarthMeanRadiusM = 6371008.7714150595;
17
+
18
+ inline double shortAngleDeltaDeg(double from, double to) {
19
+ return std::fmod(to - from + 540.0, 360.0) - 180.0;
20
+ }
21
+
22
+ inline double angleAbsDeltaDeg(double a, double b) {
23
+ return std::abs(shortAngleDeltaDeg(a, b));
24
+ }
25
+
26
+ inline double wrap360(double deg) {
27
+ double v = std::fmod(deg, 360.0);
28
+ if (v < 0.0) v += 360.0;
29
+ return v;
30
+ }
31
+
32
+ inline double clampAbs(double v, double maxAbs) {
33
+ if (maxAbs <= 0.0) return v;
34
+ if (v > maxAbs) return maxAbs;
35
+ if (v < -maxAbs) return -maxAbs;
36
+ return v;
37
+ }
38
+
39
+ inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
40
+ return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
41
+ }
42
+
43
+ // Resolve a runtime rate cap (caps.X) against the compile-time default.
44
+ // 0.0 means "no override" so we fall back to the EngineTunables value, which
45
+ // may itself be 0.0 (i.e. uncapped). Negative values are treated as 0.
46
+ inline double resolveCap(double rt, double dflt) {
47
+ return rt > 0.0 ? rt : (dflt > 0.0 ? dflt : 0.0);
48
+ }
49
+
50
+ } // namespace
51
+
52
+ CameraIntegrator::CameraIntegrator(const CameraParams& initial) {
53
+ teleport(initial);
54
+ }
55
+
56
+ // ── α-β core ──────────────────────────────────────────────────────────────
57
+
58
+ void CameraIntegrator::updateAlphaBeta(
59
+ AlphaBeta& s, double z, TimePoint tz,
60
+ double alpha, double beta, bool wrapAngle) {
61
+ // Predict the value at the time the measurement arrived. On the very first
62
+ // call (tState default-constructed = epoch) the predicted value is the
63
+ // current state (velocity == 0), so the result is identical to a snap.
64
+ const double dt = std::chrono::duration<double>(tz - s.tState).count();
65
+ const double pred = s.value + s.velocity * std::max(dt, 0.0);
66
+
67
+ const double residual =
68
+ wrapAngle ? shortAngleDeltaDeg(pred, z) : (z - pred);
69
+
70
+ s.value = pred + alpha * residual;
71
+
72
+ // Velocity update is gated on a meaningful time gap. Two reasons:
73
+ // - Bootstrap: a teleport(...) followed immediately by a setter in the
74
+ // same microtask has dt ≈ 0. Dividing β by that produces an absurd
75
+ // velocity estimate ("you climbed 100 m in zero seconds").
76
+ // - Same-tick burst writes (e.g. setAltitude + setHeading inside a
77
+ // single useAnimatedReaction tick): treat them as coincident updates
78
+ // to two independent DoFs, not as a sample of the rate of change.
79
+ //
80
+ // 5 ms is well below any plausible sensor sample interval (max useful
81
+ // rate ~200 Hz) but comfortably above the coincident-call window. For
82
+ // longer gaps we also floor dt to 20 ms when computing β/dt so a single
83
+ // sub-frame measurement does not blow up the velocity estimate.
84
+ constexpr double kVelocityMinDtSec = 0.005;
85
+ constexpr double kVelocityFloorDtSec = 0.020;
86
+ if (dt > kVelocityMinDtSec) {
87
+ const double dtForVel = std::max(dt, kVelocityFloorDtSec);
88
+ s.velocity += (beta / dtForVel) * residual;
89
+ }
90
+ s.tState = tz;
91
+
92
+ // Inter-arrival EWMA — used by the silence-aware velocity bleed in
93
+ // extrapolate(). The first setter after a teleport seeds tLastMeas
94
+ // without contributing to the EWMA (sentinel-epoch check). Sub-millisecond
95
+ // gaps are also skipped so a same-tick burst of writes does not pull the
96
+ // mean toward zero.
97
+ if (s.tLastMeas.time_since_epoch().count() > 0) {
98
+ const double interval = std::chrono::duration<double>(tz - s.tLastMeas).count();
99
+ if (interval > 1e-3) {
100
+ s.meanIntervalSec =
101
+ (1.0 - tunables::kIntervalEwmaAlpha) * s.meanIntervalSec
102
+ + tunables::kIntervalEwmaAlpha * interval;
103
+ }
104
+ }
105
+ s.tLastMeas = tz;
106
+ }
107
+
108
+ void CameraIntegrator::extrapolate(AlphaBeta& s, TimePoint now, double maxRateAbs) {
109
+ double dt = std::chrono::duration<double>(now - s.tState).count();
110
+ if (dt <= 0.0) {
111
+ s.tState = now;
112
+ return;
113
+ }
114
+ if (dt > tunables::kMaxFrameDtSec) {
115
+ dt = tunables::kMaxFrameDtSec;
116
+ }
117
+
118
+ // Silence-aware velocity bleed. Once measurements have stopped arriving
119
+ // for this DoF (e.g. the user released a pan gesture, or the joystick
120
+ // returned to centre), bleed the learned velocity exponentially. Both the
121
+ // grace window and the decay τ scale with the EWMA of inter-arrival times
122
+ // so a 60 Hz worklet bleeds in ~50 ms while a 1 Hz GPS glide is left
123
+ // alone between fixes.
124
+ if (s.tLastMeas.time_since_epoch().count() > 0) {
125
+ const double silence = std::chrono::duration<double>(now - s.tLastMeas).count();
126
+ const double grace = tunables::kVelocitySilenceGraceFactor * s.meanIntervalSec;
127
+ if (silence > grace) {
128
+ const double tau = tunables::kVelocityBleedTauFactor * s.meanIntervalSec;
129
+ if (tau > 1.0e-6) {
130
+ s.velocity *= std::exp(-dt / tau);
131
+ } else {
132
+ s.velocity = 0.0;
133
+ }
134
+ }
135
+ }
136
+
137
+ if (maxRateAbs > 0.0) {
138
+ s.velocity = clampAbs(s.velocity, maxRateAbs);
139
+ }
140
+ s.value += s.velocity * dt;
141
+ s.tState = now;
142
+ }
143
+
144
+ // ── Demand setters ────────────────────────────────────────────────────────
145
+
146
+ void CameraIntegrator::setPosition(double latitudeDeg, double longitudeDeg) {
147
+ const auto now = Clock::now();
148
+ std::lock_guard<std::mutex> g(mutex_);
149
+
150
+ if (tunables::kOutlierLatLonDeg > 0.0) {
151
+ if (std::abs(latitudeDeg - lat_.value) > tunables::kOutlierLatLonDeg ||
152
+ std::abs(shortAngleDeltaDeg(lon_.value, longitudeDeg))
153
+ > tunables::kOutlierLatLonDeg) {
154
+ return;
155
+ }
156
+ }
157
+
158
+ updateAlphaBeta(lat_, latitudeDeg, now,
159
+ tunables::kAlphaPos, tunables::kBetaPos, false);
160
+ updateAlphaBeta(lon_, longitudeDeg, now,
161
+ tunables::kAlphaPos, tunables::kBetaPos, true);
162
+ demand_.latitude = latitudeDeg;
163
+ demand_.longitude = longitudeDeg;
164
+ }
165
+
166
+ void CameraIntegrator::setAltitude(double altitudeMeters) {
167
+ const auto now = Clock::now();
168
+ std::lock_guard<std::mutex> g(mutex_);
169
+
170
+ if (tunables::kOutlierAltMeters > 0.0 &&
171
+ std::abs(altitudeMeters - alt_.value) > tunables::kOutlierAltMeters) {
172
+ return;
173
+ }
174
+
175
+ updateAlphaBeta(alt_, altitudeMeters, now,
176
+ tunables::kAlphaAlt, tunables::kBetaAlt, false);
177
+ demand_.altitude = altitudeMeters;
178
+ }
179
+
180
+ void CameraIntegrator::setHeading(double headingDeg) {
181
+ const auto now = Clock::now();
182
+ std::lock_guard<std::mutex> g(mutex_);
183
+
184
+ if (tunables::kOutlierHeadingDeg > 0.0 &&
185
+ angleAbsDeltaDeg(headingDeg, hdg_.value) > tunables::kOutlierHeadingDeg) {
186
+ return;
187
+ }
188
+
189
+ updateAlphaBeta(hdg_, headingDeg, now,
190
+ tunables::kAlphaHdg, tunables::kBetaHdg, true);
191
+ hdg_.value = wrap360(hdg_.value);
192
+ demand_.heading = headingDeg;
193
+ }
194
+
195
+ void CameraIntegrator::setAttitude(double pitchDeg, double rollDeg) {
196
+ const auto now = Clock::now();
197
+ std::lock_guard<std::mutex> g(mutex_);
198
+
199
+ updateAlphaBeta(pitch_, pitchDeg, now,
200
+ tunables::kAlphaPitch, tunables::kBetaPitch, true);
201
+ updateAlphaBeta(roll_, rollDeg, now,
202
+ tunables::kAlphaRoll, tunables::kBetaRoll, true);
203
+ demand_.pitch = pitchDeg;
204
+ demand_.roll = rollDeg;
205
+ }
206
+
207
+ void CameraIntegrator::setViewCorrection(const glm::dquat& q) {
208
+ std::lock_guard<std::mutex> g(mutex_);
209
+
210
+ const double ql2 = q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z;
211
+ if (ql2 < 1.0e-20) {
212
+ viewCorrTarget_ = glm::dquat(1.0, 0.0, 0.0, 0.0);
213
+ } else {
214
+ const double inv = 1.0 / std::sqrt(ql2);
215
+ viewCorrTarget_ = glm::dquat(q.w * inv, q.x * inv, q.y * inv, q.z * inv);
216
+ }
217
+ demand_.viewCorrection = viewCorrTarget_;
218
+ }
219
+
220
+ void CameraIntegrator::setVerticalFov(double degrees) {
221
+ const auto now = Clock::now();
222
+ std::lock_guard<std::mutex> g(mutex_);
223
+
224
+ const double residual = degrees - vfov_.value;
225
+ vfov_.value += tunables::kAlphaVfov * residual;
226
+ vfov_.velocity = 0.0;
227
+ vfov_.tState = now;
228
+ }
229
+
230
+ void CameraIntegrator::teleport(const CameraParams& target) {
231
+ const auto now = Clock::now();
232
+ std::lock_guard<std::mutex> g(mutex_);
233
+
234
+ auto seed = [now](AlphaBeta& s, double v) {
235
+ s.value = v;
236
+ s.velocity = 0.0;
237
+ s.tState = now;
238
+ s.tLastMeas = TimePoint{}; // sentinel: bleed inactive until first setter
239
+ s.meanIntervalSec = 0.1;
240
+ };
241
+ seed(lat_, target.latitude);
242
+ seed(lon_, target.longitude);
243
+ seed(alt_, target.altitude);
244
+ seed(hdg_, wrap360(target.heading));
245
+ seed(pitch_, target.pitch);
246
+ seed(roll_, target.roll);
247
+
248
+ const glm::dquat& qIn = target.viewCorrection;
249
+ const double ql2 = qIn.w*qIn.w + qIn.x*qIn.x + qIn.y*qIn.y + qIn.z*qIn.z;
250
+ if (ql2 < 1.0e-20) {
251
+ viewCorrTarget_ = viewCorrActual_ = glm::dquat(1.0, 0.0, 0.0, 0.0);
252
+ } else {
253
+ const double inv = 1.0 / std::sqrt(ql2);
254
+ viewCorrTarget_ = viewCorrActual_ =
255
+ glm::dquat(qIn.w * inv, qIn.x * inv, qIn.y * inv, qIn.z * inv);
256
+ }
257
+
258
+ seed(vfov_, target.verticalFov > 0.0 ? target.verticalFov : 0.0);
259
+
260
+ demand_ = target;
261
+ demand_.viewCorrection = viewCorrTarget_;
262
+ if (demand_.verticalFov <= 0.0) {
263
+ demand_.verticalFov = vfov_.value;
264
+ }
265
+ }
266
+
267
+ CameraParams CameraIntegrator::step() {
268
+ const auto now = Clock::now();
269
+ std::lock_guard<std::mutex> g(mutex_);
270
+
271
+ // Resolve effective rate caps once per frame; runtime override beats default.
272
+ const double yawCap = resolveCap(caps_.maxYawRateDegSec, tunables::kMaxYawRateDegSec);
273
+ const double pitchCap = resolveCap(caps_.maxPitchRateDegSec, tunables::kMaxPitchRateDegSec);
274
+ const double rollCap = resolveCap(caps_.maxRollRateDegSec, tunables::kMaxRollRateDegSec);
275
+ const double climbCap = resolveCap(caps_.maxClimbRateMps, tunables::kMaxClimbRateMps);
276
+
277
+ // Convert ground-speed cap (m/s) to a per-DoF deg/s cap on latitude /
278
+ // longitude. We use the WGS84 mean radius and ignore the cos(lat)
279
+ // correction for the longitude term; this is a first-order approximation
280
+ // but fine for a flight-sim rate cap, and avoids tangling the rate cap
281
+ // with the current latitude (which would otherwise feed back into itself).
282
+ double latLonCapDegSec = 0.0;
283
+ const double groundCap = resolveCap(caps_.maxGroundSpeedMps, tunables::kMaxGroundSpeedMps);
284
+ if (groundCap > 0.0) {
285
+ latLonCapDegSec = glm::degrees(groundCap / kEarthMeanRadiusM);
286
+ }
287
+
288
+ extrapolate(lat_, now, latLonCapDegSec);
289
+ extrapolate(lon_, now, latLonCapDegSec);
290
+ extrapolate(alt_, now, climbCap);
291
+ extrapolate(hdg_, now, yawCap);
292
+ extrapolate(pitch_, now, pitchCap);
293
+ extrapolate(roll_, now, rollCap);
294
+ extrapolate(vfov_, now, 0.0); // no velocity term anyway
295
+
296
+ hdg_.value = wrap360(hdg_.value);
297
+ lat_.value = std::clamp(lat_.value, -90.0, 90.0);
298
+ // Longitude is allowed to drift over ±180; consumers usually want the raw
299
+ // continuous value for short-arc maths and the camera transform doesn't
300
+ // care.
301
+
302
+ // viewCorrection: SLERP toward the latest target.
303
+ glm::dquat cur = glm::normalize(viewCorrActual_);
304
+ glm::dquat tgt = glm::normalize(viewCorrTarget_);
305
+ if (glm::dot(cur, tgt) < 0.0) tgt = -tgt;
306
+ viewCorrActual_ = glm::normalize(
307
+ glm::slerp(cur, tgt, tunables::kAlphaViewCorr));
308
+
309
+ CameraParams out;
310
+ out.latitude = lat_.value;
311
+ out.longitude = lon_.value;
312
+ out.altitude = alt_.value;
313
+ out.heading = hdg_.value;
314
+ out.pitch = pitch_.value;
315
+ out.roll = roll_.value;
316
+ out.verticalFov = vfov_.value;
317
+ out.viewCorrection = viewCorrActual_;
318
+ return out;
319
+ }
320
+
321
+ void CameraIntegrator::clampActualAltitude(double newAltMsl) {
322
+ std::lock_guard<std::mutex> g(mutex_);
323
+ alt_.value = newAltMsl;
324
+ alt_.velocity = 0.0;
325
+ // Do not touch demand_.altitude — getDemand() should still report what the
326
+ // user asked for.
327
+ }
328
+
329
+ void CameraIntegrator::setRateCaps(const CameraRateCaps& caps) {
330
+ std::lock_guard<std::mutex> g(mutex_);
331
+ caps_ = caps;
332
+ }
333
+
334
+ CameraParams CameraIntegrator::getDemand() const {
335
+ std::lock_guard<std::mutex> g(mutex_);
336
+ return demand_;
337
+ }
338
+
339
+ CameraParams CameraIntegrator::getActual() const {
340
+ std::lock_guard<std::mutex> g(mutex_);
341
+ CameraParams out;
342
+ out.latitude = lat_.value;
343
+ out.longitude = lon_.value;
344
+ out.altitude = alt_.value;
345
+ out.heading = hdg_.value;
346
+ out.pitch = pitch_.value;
347
+ out.roll = roll_.value;
348
+ out.verticalFov = vfov_.value;
349
+ out.viewCorrection = viewCorrActual_;
350
+ return out;
351
+ }
352
+
353
+ bool CameraIntegrator::isActive() const {
354
+ std::lock_guard<std::mutex> g(mutex_);
355
+ if (std::abs(demand_.latitude - lat_.value) > tunables::kEpsLatLon) return true;
356
+ if (std::abs(shortAngleDeltaDeg(lon_.value, demand_.longitude))
357
+ > tunables::kEpsLatLon) return true;
358
+ if (std::abs(demand_.altitude - alt_.value) > tunables::kEpsAltitudeMeters) return true;
359
+ if (angleAbsDeltaDeg(demand_.heading, hdg_.value) > tunables::kEpsAngleDeg) return true;
360
+ if (angleAbsDeltaDeg(demand_.pitch, pitch_.value) > tunables::kEpsAngleDeg) return true;
361
+ if (angleAbsDeltaDeg(demand_.roll, roll_.value) > tunables::kEpsAngleDeg) return true;
362
+ if (std::abs(demand_.verticalFov - vfov_.value) > tunables::kEpsAngleDeg) return true;
363
+ if (std::abs(lat_.velocity) > tunables::kEpsLatLon * 60.0) return true;
364
+ if (std::abs(lon_.velocity) > tunables::kEpsLatLon * 60.0) return true;
365
+ if (std::abs(alt_.velocity) > tunables::kEpsAltitudeMeters * 60.0) return true;
366
+ if (std::abs(hdg_.velocity) > tunables::kEpsAngleDeg * 60.0) return true;
367
+ if (std::abs(pitch_.velocity) > tunables::kEpsAngleDeg * 60.0) return true;
368
+ if (std::abs(roll_.velocity) > tunables::kEpsAngleDeg * 60.0) return true;
369
+ if ((1.0 - quatDotAbs(viewCorrActual_, viewCorrTarget_))
370
+ > tunables::kEpsQuatDot) return true;
371
+ return false;
372
+ }
373
+
374
+ } // namespace reactnativecesium
@@ -0,0 +1,146 @@
1
+ #pragma once
2
+
3
+ #include "GlobeCamera.hpp"
4
+
5
+ #include <chrono>
6
+ #include <mutex>
7
+
8
+ namespace reactnativecesium {
9
+
10
+ // Runtime-tunable per-DoF rate caps and outlier thresholds. Mirrors the
11
+ // compile-time defaults in EngineTunables but exposed as a struct so a view
12
+ // prop can override them at runtime without recompiling.
13
+ struct CameraRateCaps {
14
+ double maxYawRateDegSec = 0.0;
15
+ double maxPitchRateDegSec = 0.0;
16
+ double maxRollRateDegSec = 0.0;
17
+ double maxClimbRateMps = 0.0;
18
+ double maxGroundSpeedMps = 0.0;
19
+ };
20
+
21
+ // Single owner of the camera's per-DoF α-β tracker state. Measurements are
22
+ // written from any thread (typically a Reanimated worklet on the UI thread or
23
+ // a GPS / IMU callback on the JS thread); the render thread ticks step() once
24
+ // per frame and reads the new actual camera back out.
25
+ //
26
+ // Internally every scalar DoF (latitude, longitude, altitude, heading, pitch,
27
+ // roll, verticalFov) has independent (value, velocity, t_state) state. On a
28
+ // measurement the residual is computed against the predicted value at the
29
+ // arrival time and split between value (scaled by α) and velocity
30
+ // (scaled by β / dt_arrival). On step() the value is extrapolated forward
31
+ // using the running velocity, clamped against the rate cap for that DoF.
32
+ //
33
+ // viewCorrection is a unit quaternion with no useful velocity model; it uses
34
+ // a single-coefficient SLERP toward the latest demand each frame.
35
+ //
36
+ // All public methods take `mutex_` briefly — contention is uncontested in
37
+ // practice (writers are sub-microsecond and the render thread reads once per
38
+ // frame). The integrator owns its own clock (Clock::now() inside both the
39
+ // setters and step()), so no platform time-base needs to be threaded in.
40
+ class CameraIntegrator {
41
+ public:
42
+ using Clock = std::chrono::steady_clock;
43
+ using TimePoint = Clock::time_point;
44
+
45
+ explicit CameraIntegrator(const CameraParams& initial = {});
46
+
47
+ // ── Per-DoF demand setters ──────────────────────────────────────────────
48
+ // Each call stamps the measurement with the current steady_clock now() and
49
+ // performs one α-β update against the predicted value at that time.
50
+ void setPosition(double latitudeDeg, double longitudeDeg);
51
+ void setAltitude(double altitudeMeters);
52
+ void setHeading(double headingDeg);
53
+ void setAttitude(double pitchDeg, double rollDeg);
54
+ void setViewCorrection(const glm::dquat& q);
55
+ void setVerticalFov(double degrees);
56
+
57
+ // Atomically reset every DoF to `target` — value, velocity (0), demand,
58
+ // all timestamps. Used for initial seeding and for explicit scripted
59
+ // scene jumps; bypasses the trackers entirely.
60
+ void teleport(const CameraParams& target);
61
+
62
+ // Render-thread tick. Extrapolates every DoF forward using its current
63
+ // velocity, applies rate caps, then returns the new actual camera. dt is
64
+ // clamped internally to kMaxFrameDtSec so a long sleep can't catapult the
65
+ // camera along a stale velocity vector.
66
+ //
67
+ // The wall-clock used to compute dt is Clock::now() (steady_clock) — the
68
+ // same clock the setters stamp with. This is deliberate: the integrator
69
+ // is the single owner of the time base. Earlier versions accepted an
70
+ // externally supplied `now` from the platform render loop, but on iOS
71
+ // Simulator `CACurrentMediaTime()` and `std::chrono::steady_clock` do
72
+ // not share an epoch, so mixing them produced multi-thousand-second dt
73
+ // values and an instant runaway.
74
+ CameraParams step();
75
+
76
+ // Terrain-floor write-back hook. Replaces the *actual* altitude value with
77
+ // `newAltMsl` and zeroes vertical velocity so the integrator does not
78
+ // immediately try to descend back through the floor. The *demand* altitude
79
+ // is left untouched — getDemand().altitude continues to reflect what the
80
+ // consumer asked for.
81
+ void clampActualAltitude(double newAltMsl);
82
+
83
+ // Runtime override of the rate caps (driven by view props). 0 = uncapped
84
+ // for a given axis. The defaults match tunables::kMax*.
85
+ void setRateCaps(const CameraRateCaps& caps);
86
+
87
+ // ── Readback ────────────────────────────────────────────────────────────
88
+
89
+ // What the consumer last asked for (each DoF independently). This is the
90
+ // unfiltered demand: pre-α-β. Useful for HUDs that want to display the
91
+ // requested camera rather than the rendered one.
92
+ CameraParams getDemand() const;
93
+
94
+ // The most recent value of step() (or the initial seed if step() has not
95
+ // run yet). Equivalent to what is currently being rendered.
96
+ CameraParams getActual() const;
97
+
98
+ // True when there is meaningful work to do: any residual between actual
99
+ // and demand exceeds its epsilon, or any DoF has non-zero velocity. The
100
+ // render loop can use this to drop back to idle when the camera is at
101
+ // rest.
102
+ bool isActive() const;
103
+
104
+ private:
105
+ // Per-DoF α-β state. `value` and `velocity` are the running estimates,
106
+ // `tState` the time of the last value update. `tLastMeas` is the time of
107
+ // the most recent setter call for this DoF (sentinel epoch == "no
108
+ // measurement seen yet"); `meanIntervalSec` is an EWMA of inter-arrival
109
+ // times used to drive the silence-aware velocity bleed.
110
+ struct AlphaBeta {
111
+ double value = 0.0;
112
+ double velocity = 0.0;
113
+ TimePoint tState{};
114
+ TimePoint tLastMeas{};
115
+ double meanIntervalSec = 0.1; // seed: ~10 Hz, irrelevant after a few setters
116
+ };
117
+
118
+ // Apply one α-β measurement update. `wrapAngle` is true for the angular
119
+ // DoFs (heading, pitch, roll, longitude) so the residual is computed on
120
+ // the shortest signed arc.
121
+ static void updateAlphaBeta(AlphaBeta& s, double z, TimePoint tz,
122
+ double alpha, double beta, bool wrapAngle);
123
+
124
+ // Advance the running estimate to `now`, clamp velocity to `maxRateAbs`
125
+ // (per second; 0 = uncapped). For angles, the resulting value is also
126
+ // normalised to its canonical range by the caller as needed.
127
+ static void extrapolate(AlphaBeta& s, TimePoint now, double maxRateAbs);
128
+
129
+ mutable std::mutex mutex_;
130
+
131
+ AlphaBeta lat_, lon_, alt_;
132
+ AlphaBeta hdg_, pitch_, roll_;
133
+ AlphaBeta vfov_; // velocity always zero — α-only
134
+
135
+ // Quaternion SLERP target + most recent actual (rendered) value.
136
+ glm::dquat viewCorrTarget_{1.0, 0.0, 0.0, 0.0};
137
+ glm::dquat viewCorrActual_{1.0, 0.0, 0.0, 0.0};
138
+
139
+ // Demand mirrors (raw last-set values, no α-β filter applied).
140
+ CameraParams demand_{};
141
+
142
+ // Runtime overrides; 0 = use compile-time defaults from EngineTunables.
143
+ CameraRateCaps caps_{};
144
+ };
145
+
146
+ } // namespace reactnativecesium
@@ -41,44 +41,91 @@ inline constexpr double kDefaultMaximumScreenSpaceError = 32.0;
41
41
  inline constexpr int32_t kDefaultMaximumSimultaneousTileLoads = 12;
42
42
  inline constexpr int32_t kDefaultLoadingDescendantLimit = 20;
43
43
 
44
- // Camera smoothing constants — shared between platform bridges so jank /
45
- // motion feel is identical on iOS and Android.
44
+ // Camera smoothing constants — shared between platform bridges so motion
45
+ // feel is identical on iOS and Android.
46
46
  //
47
- // lat / lon / altitude / heading use per-DoF linear interpolation inside
48
- // CameraTargetState::snapshot(): each DoF tracks its own EWMA of inter-arrival
49
- // times and linearly lerps from its previous value to its current target over
50
- // that interval. At ~60 Hz (gestures) the EWMA collapses to ~16 ms so the
51
- // motion is indistinguishable from a snap; at 1 Hz (GPS) the DoF glides at
52
- // constant velocity between fixes — no teleports or ease-out deceleration.
53
- // pitch / roll / viewCorrection remain on their fixed exponential curves
54
- // because they are almost always driven by high-rate attitude sensors.
55
-
56
- // Exponential smoothing for pitch / roll / viewCorrection (k = 1/τ).
57
- inline constexpr double kSmoothPitch = 50.0;
58
- inline constexpr double kSmoothRoll = 50.0;
59
- inline constexpr double kSmoothViewCorr = 50.0;
60
-
61
- // Convergence epsilons (used by deltaExceedsEpsilon and snap-to-exact).
47
+ // Every scalar DoF (lat, lon, alt, heading, pitch, roll) is driven by an
48
+ // α-β (constant-velocity predictor) tracker inside CameraIntegrator:
49
+ //
50
+ // on measurement z at time t_z:
51
+ // pred = value + velocity * (t_z - t_state)
52
+ // residual = wrap(z - pred) // wrap only for angles
53
+ // value += α * residual
54
+ // velocity += (β / max(t_z - t_state, ε)) * residual
55
+ //
56
+ // on render frame at time t_now:
57
+ // value += velocity * dt // extrapolate
58
+ // // optional per-DoF rate cap applied here
59
+ //
60
+ // α controls how hard each measurement pulls the value (responsiveness).
61
+ // β controls how aggressively velocity is corrected (anticipation). With
62
+ // α ≈ 0.5–0.6 / β ≈ 0.05 every DoF stays smooth on a 1 Hz GPS feed (zero
63
+ // steady-state lag at constant motion) while remaining instant under a
64
+ // 60 Hz gesture-driven worklet.
65
+ //
66
+ // `viewCorrection` (a unit quaternion) uses a single-coefficient SLERP toward
67
+ // the latest demand each frame — there is no useful velocity concept for it.
68
+ // `verticalFovDeg` is a 1-D scalar with α only (no velocity term).
69
+ inline constexpr double kAlphaPos = 0.6; inline constexpr double kBetaPos = 0.05;
70
+ inline constexpr double kAlphaAlt = 0.6; inline constexpr double kBetaAlt = 0.05;
71
+ inline constexpr double kAlphaHdg = 0.55; inline constexpr double kBetaHdg = 0.05;
72
+ inline constexpr double kAlphaPitch = 0.5; inline constexpr double kBetaPitch = 0.05;
73
+ inline constexpr double kAlphaRoll = 0.5; inline constexpr double kBetaRoll = 0.05;
74
+ inline constexpr double kAlphaVfov = 0.4; // no velocity term
75
+ inline constexpr double kAlphaViewCorr = 0.5; // SLERP toward target
76
+
77
+ // Convergence epsilons (used to decide when the integrator has settled and
78
+ // the render loop can drop back to idle).
62
79
  inline constexpr double kEpsLatLon = 1e-7;
63
80
  inline constexpr double kEpsAltitudeMeters = 0.1;
64
81
  inline constexpr double kEpsAngleDeg = 0.05;
65
82
  inline constexpr double kEpsQuatDot = 1e-8;
66
83
 
67
- // Linear interpolation parameters shared by lat/lon, altitude, and heading.
84
+ // Per-DoF rate caps (per second). 0 = uncapped. Hard ceilings; the integrator
85
+ // clamps the extrapolated velocity to these on every render tick. Useful to
86
+ // reject a single noisy IMU sample without making the camera spin or pitch up
87
+ // at hundreds of degrees per second. Default values are deliberately generous.
88
+ inline constexpr double kMaxYawRateDegSec = 360.0;
89
+ inline constexpr double kMaxPitchRateDegSec = 360.0;
90
+ inline constexpr double kMaxRollRateDegSec = 720.0;
91
+ inline constexpr double kMaxClimbRateMps = 0.0;
92
+ inline constexpr double kMaxGroundSpeedMps = 0.0;
93
+
94
+ // Render-step dt clamp (catch-up after a long pause / app foreground). Without
95
+ // this, the first render after a multi-second sleep would extrapolate a long
96
+ // way along the last known velocity vector and visibly jump.
97
+ inline constexpr double kMaxFrameDtSec = 0.1;
98
+
99
+ // Velocity bleed for bursty inputs. Without this, a gesture-driven feed (a
100
+ // worklet that emits at 60 Hz during the pan and stops emitting on release,
101
+ // or a joystick that pushes rates only while held) would leave the
102
+ // integrator extrapolating along the last learned velocity forever — there
103
+ // is no measurement-end signal in the per-DoF API.
104
+ //
105
+ // Each scalar DoF tracks an EWMA of inter-arrival times between its
106
+ // measurements. While measurements keep arriving on schedule, silence is
107
+ // small relative to the mean and velocity is extrapolated normally. Once
108
+ // silence exceeds the grace window the velocity bleeds exponentially with a
109
+ // time-constant proportional to the same mean interval — fast for gestures
110
+ // (mean ≈ 16 ms → τ ≈ 32 ms), slow for 1 Hz GPS (mean ≈ 1 s → τ ≈ 2 s, but
111
+ // silence rarely exceeds grace before the next fix arrives so the glide is
112
+ // untouched).
68
113
  //
69
- // kSmoothPositionTauMax / kSmoothPositionAlpha is the seed value for the EWMA
70
- // on the first update (gives a generous interpolation window before the cadence
71
- // is known). kEwmaIntervalAlpha controls how quickly the EWMA adapts to a rate
72
- // change (0.3 = ~3-5 samples to converge to a new cadence).
73
- inline constexpr double kSmoothPositionAlpha = 0.45;
74
- inline constexpr double kSmoothPositionTauMax = 0.6;
75
- inline constexpr double kEwmaIntervalAlpha = 0.3;
76
-
77
- // Snap-through guards: deltas larger than these skip interpolation so a
78
- // deliberate teleport (e.g. setCamera(NewYork → Tokyo)) remains instant.
79
- inline constexpr double kPosSnapThroughDeg = 0.5; // lat or lon, ~55 km
80
- inline constexpr double kAltSnapThroughMeters = 500.0; // altitude
81
- inline constexpr double kHdgSnapThroughDeg = 90.0; // heading
114
+ // silence > kVelocitySilenceGraceFactor * meanIntervalSec → bleed active.
115
+ // velocity *= exp(-frameDt / (kVelocityBleedTauFactor * meanIntervalSec))
116
+ inline constexpr double kVelocitySilenceGraceFactor = 1.5;
117
+ inline constexpr double kVelocityBleedTauFactor = 2.0;
118
+ // EWMA blend coefficient for the inter-arrival mean. Higher = faster adapt
119
+ // to cadence changes (e.g. user switches from gesture to sparse GPS feed).
120
+ inline constexpr double kIntervalEwmaAlpha = 0.3;
121
+
122
+ // Optional outlier rejection (off if 0). When > 0, a single measurement whose
123
+ // residual against the predicted state exceeds the threshold is dropped (and
124
+ // logged in debug builds). For deliberate jumps the consumer should call
125
+ // teleport() instead. Disabled by default — easy to enable per-deployment.
126
+ inline constexpr double kOutlierLatLonDeg = 0.0;
127
+ inline constexpr double kOutlierAltMeters = 0.0;
128
+ inline constexpr double kOutlierHeadingDeg = 0.0;
82
129
 
83
130
  // Metrics throttle: emit telemetry every N rendered frames.
84
131
  inline constexpr int kMetricsEmitEveryFrames = 20;