react-native-cesium 1.2.2 → 1.2.3

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.
package/README.md CHANGED
@@ -403,10 +403,11 @@ Every value you pass to `setCamera` / `setCameraQuaternion` is treated as a **de
403
403
 
404
404
  | DoF | Smoothing |
405
405
  | --- | --- |
406
- | `latitude` / `longitude` | **Linear interpolation** at constant speed between position fixes. When a new lat/lon arrives, the native engine records the previous target as the "from" position and linearly lerps toward the new target over the EWMA inter-arrival interval (e.g. a 1 Hz GPS feed gives a 1 s travel time, so the camera moves at exactly the vehicle's speed between fixes). At 60 Hz gestures the interval collapses to ~16 ms making the interpolation indistinguishable from an instant snap. Cross-planet jumps (Δ > ~0.5°) bypass interpolation so deliberate teleports remain instant. Antimeridian crossings always rotate the short way. |
407
- | `altitude` | Exponential, fixed `τ ≈ 40 ms`. |
408
- | `heading` / `pitch` / `roll` | Exponential on the shortest-arc angular delta, fixed `τ ≈ 20–33 ms`. |
409
- | `viewCorrection` (quaternion) | SLERP-based exponential, fixed `τ ≈ 20 ms`, hemisphere-corrected so it always takes the short rotation. |
406
+ | `latitude` / `longitude` | **Linear interpolation** — own independent EWMA. Constant-velocity glide at GPS cadence; near-instant at gesture rate. Cross-planet jumps (Δ > ~0.5°) bypass interpolation. Antimeridian crossings always rotate the short way. |
407
+ | `altitude` | **Linear interpolation** — own independent EWMA. Self-tunes whether altitude comes from GPS (1 Hz → 1 s glide) or a barometer (50 Hz → ~20 ms, near-instant). Jumps > 500 m bypass interpolation. |
408
+ | `heading` | **Linear interpolation** — own independent EWMA. Constant yaw-rate between GPS course-over-ground fixes; near-instant when heading comes from a magnetometer at high rate. Jumps > 90° bypass interpolation. |
409
+ | `pitch` / `roll` | Exponential on the shortest-arc angular delta, fixed `τ ≈ 20 ms`. Almost always attitude-sensor driven; ease-out feel is appropriate. |
410
+ | `viewCorrection` (quaternion) | SLERP-based exponential, fixed `τ ≈ 20 ms`, hemisphere-corrected. |
410
411
  | `verticalFovDeg` | Applied directly (not smoothed). |
411
412
 
412
413
  You don't have to do anything special to take advantage of this — the smoother already runs, it's the same code path whether your driver is a Reanimated worklet at 60 Hz, a `setInterval` push at 10 Hz, or a 1 Hz GPS subscription. Just call `setCamera` whenever you have a new value.
@@ -29,34 +29,31 @@ CameraParams CameraSmoother::step(
29
29
  const CameraParams& current, const CameraParams& target, double dt) {
30
30
  CameraParams next = current;
31
31
 
32
- // lat/lon: snap directly to the target.
33
- // CameraTargetState::snapshot() already produces a linearly-interpolated
34
- // position that advances at constant speed between position fixes, so
35
- // snapping here gives smooth, constant-velocity motion with no ease-in/out.
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
36
  next.latitude = target.latitude;
37
37
  next.longitude = target.longitude;
38
+ next.altitude = target.altitude;
39
+ next.heading = target.heading;
38
40
 
39
- const double aAlt = 1.0 - std::exp(-tunables::kSmoothAltitude * dt);
40
- const double aHdg = 1.0 - std::exp(-tunables::kSmoothHeading * dt);
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.
41
44
  const double aPitch = 1.0 - std::exp(-tunables::kSmoothPitch * dt);
42
45
  const double aRoll = 1.0 - std::exp(-tunables::kSmoothRoll * dt);
43
46
  const double aViewQ = 1.0 - std::exp(-tunables::kSmoothViewCorr * dt);
44
47
 
45
- next.altitude = current.altitude + aAlt * (target.altitude - current.altitude);
46
- next.heading = lerpAngleDeg(current.heading, target.heading, aHdg);
47
- next.pitch = lerpAngleDeg(current.pitch, target.pitch, aPitch);
48
- next.roll = lerpAngleDeg(current.roll, target.roll, aRoll);
48
+ next.pitch = lerpAngleDeg(current.pitch, target.pitch, aPitch);
49
+ next.roll = lerpAngleDeg(current.roll, target.roll, aRoll);
49
50
 
50
51
  glm::dquat cq = glm::normalize(current.viewCorrection);
51
52
  glm::dquat tq = glm::normalize(target.viewCorrection);
52
53
  if (glm::dot(cq, tq) < 0.0) tq = -tq;
53
54
  next.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
54
55
 
55
- // Eventually-exact convergence: snap once the residual is sub-epsilon.
56
- if (std::abs(target.altitude - next.altitude) <= tunables::kEpsAltitudeMeters)
57
- next.altitude = target.altitude;
58
- if (angleDeltaAbsDeg(next.heading, target.heading) <= tunables::kEpsAngleDeg)
59
- next.heading = target.heading;
56
+ // Eventually-exact convergence for exponential DoFs: snap once sub-epsilon.
60
57
  if (angleDeltaAbsDeg(next.pitch, target.pitch) <= tunables::kEpsAngleDeg)
61
58
  next.pitch = target.pitch;
62
59
  if (angleDeltaAbsDeg(next.roll, target.roll) <= tunables::kEpsAngleDeg)
@@ -7,14 +7,17 @@ namespace reactnativecesium {
7
7
  // Stateless helper. Given a current camera state and a target camera state,
8
8
  // returns the next camera state for a frame of duration `dt` seconds.
9
9
  //
10
- // Per-DoF smoothing rules:
11
- // - lat / lon: snapped directly to target. The target itself is already a
12
- // linearly-interpolated position produced by CameraTargetState::snapshot(),
13
- // so snapping here gives constant-velocity motion between position fixes.
14
- // - altitude / heading / pitch / roll / viewCorrection: exponential decay
15
- // `1 - exp(-k * dt)` per DoF with k taken from EngineTunables.
16
- // - When the delta drops below the matching epsilon, the value is snapped
17
- // to the target so we eventually reach exact equality.
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.
18
21
  class CameraSmoother {
19
22
  public:
20
23
  static CameraParams step(const CameraParams& current,
@@ -15,11 +15,29 @@ inline double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
15
15
  return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
16
16
  }
17
17
 
18
- // Shortest signed angular delta for longitude, wrapped to [-180, 180].
19
- inline double lonDeltaDeg(double from, double to) {
18
+ // Shortest signed angular delta wrapped to [-180, 180].
19
+ inline double shortAngleDeltaDeg(double from, double to) {
20
20
  return std::fmod(to - from + 540.0, 360.0) - 180.0;
21
21
  }
22
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
+
23
41
  } // namespace
24
42
 
25
43
  // ── Public writers ────────────────────────────────────────────────────────────
@@ -28,8 +46,8 @@ void CameraTargetState::setAll(const CameraParams& target) {
28
46
  {
29
47
  std::lock_guard<std::mutex> g(mutex_);
30
48
  target_ = target;
31
- // Deliberately do NOT touch interpolation state: setAll is engine-init only
32
- // and the first frame must snap to the requested position immediately.
49
+ // Deliberately do NOT touch any interpolation state: setAll is init-only
50
+ // and the first rendered frame must snap to the requested position.
33
51
  }
34
52
  dirty_.store(true, std::memory_order_release);
35
53
  forceRender_.store(true, std::memory_order_release);
@@ -40,33 +58,58 @@ void CameraTargetState::setHpr(double lat, double lon, double alt,
40
58
  {
41
59
  std::lock_guard<std::mutex> g(mutex_);
42
60
 
61
+ const auto now = std::chrono::steady_clock::now();
62
+
63
+ // ── lat / lon ─────────────────────────────────────────────────────────
43
64
  const bool posChanged =
44
65
  std::abs(lat - target_.latitude) > tunables::kEpsLatLon ||
45
66
  std::abs(lon - target_.longitude) > tunables::kEpsLatLon;
46
67
 
47
68
  if (posChanged) {
48
- const auto now = std::chrono::steady_clock::now();
49
-
50
- if (!hasInterpolation_) {
51
- // First runtime lat/lon update: seed posFrom = destination so that
52
- // lerp(dest, dest, t) = dest for all t — clean snap, no startup glide.
53
- posFromLat_ = lat;
54
- posFromLon_ = lon;
55
- ewmaIntervalSec_ = tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha;
56
- posUpdateTime_ = now;
57
- hasInterpolation_ = true;
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);
58
76
  } else {
59
- // Subsequent update: record where we're moving FROM (the previous
60
- // target), update the EWMA interval, and reset the clock.
61
77
  posFromLat_ = target_.latitude;
62
78
  posFromLon_ = target_.longitude;
79
+ updateEwma(posEwmaIntervalSec_, hasPosInterp_, posUpdateTime_, now,
80
+ tunables::kSmoothPositionTauMax / tunables::kSmoothPositionAlpha);
81
+ }
82
+ }
63
83
 
64
- const double dt = std::chrono::duration<double>(now - posUpdateTime_).count();
65
- if (dt > 0.0) {
66
- ewmaIntervalSec_ = ewmaIntervalSec_ +
67
- tunables::kEwmaIntervalAlpha * (dt - ewmaIntervalSec_);
68
- }
69
- posUpdateTime_ = now;
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);
70
113
  }
71
114
  }
72
115
 
@@ -85,8 +128,8 @@ void CameraTargetState::setViewCorrection(const glm::dquat& q) {
85
128
  {
86
129
  std::lock_guard<std::mutex> g(mutex_);
87
130
  target_.viewCorrection = q;
88
- // Position cadence intentionally NOT updated — viewCorrection changes
89
- // carry no lat/lon information and must not affect the interpolation window.
131
+ // Interpolation state is not touched — viewCorrection carries no
132
+ // positional information and must not affect any of the three EWMAs.
90
133
  }
91
134
  dirty_.store(true, std::memory_order_release);
92
135
  forceRender_.store(true, std::memory_order_release);
@@ -100,12 +143,11 @@ CameraParams CameraTargetState::snapshot() const {
100
143
 
101
144
  CameraParams result = target_;
102
145
 
103
- if (hasInterpolation_ && ewmaIntervalSec_ > 0.0) {
146
+ // ── lat / lon ─────────────────────────────────────────────────────────────
147
+ if (hasPosInterp_ && posEwmaIntervalSec_ > 0.0) {
104
148
  const double dLat = target_.latitude - posFromLat_;
105
- const double dLon = lonDeltaDeg(posFromLon_, target_.longitude);
149
+ const double dLon = shortAngleDeltaDeg(posFromLon_, target_.longitude);
106
150
 
107
- // Snap-through guard: genuine teleports (setCamera(NewYork → Tokyo)) bypass
108
- // the interpolation so they remain instant rather than gliding across the planet.
109
151
  const bool snapThrough =
110
152
  std::abs(dLat) > tunables::kPosSnapThroughDeg ||
111
153
  std::abs(dLon) > tunables::kPosSnapThroughDeg;
@@ -113,13 +155,36 @@ CameraParams CameraTargetState::snapshot() const {
113
155
  if (!snapThrough) {
114
156
  const double elapsed =
115
157
  std::chrono::duration<double>(now - posUpdateTime_).count();
116
- const double t = std::min(elapsed / ewmaIntervalSec_, 1.0);
117
-
158
+ const double t = std::min(elapsed / posEwmaIntervalSec_, 1.0);
118
159
  result.latitude = posFromLat_ + t * dLat;
119
160
  result.longitude = posFromLon_ + t * dLon;
120
161
  }
121
162
  }
122
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
+
123
188
  return result;
124
189
  }
125
190
 
@@ -22,41 +22,49 @@ namespace reactnativecesium {
22
22
  // `dirty_` and `forceRender_` are independent atomic flags so the read side can
23
23
  // poll without taking the lock when the camera has not moved.
24
24
  //
25
- // Lat/lon interpolation
26
- // ─────────────────────
27
- // When setHpr is called with a new lat/lon, `posFrom` (old target) and
28
- // `posUpdateTime` are recorded inside the mutex. snapshot() then linearly
29
- // interpolates posFrom → target over `ewmaIntervalSec` (EWMA of inter-arrival
30
- // times). This produces constant-velocity motion between position fixes —
31
- // realistic for any vehicle/GPS source — without any changes to the public API.
25
+ // Per-DoF linear interpolation (lat/lon, altitude, heading)
26
+ // ──────────────────────────────────────────────────────────
27
+ // Each of lat/lon, altitude, and heading maintains its own independent EWMA
28
+ // of inter-arrival times and only updates that EWMA when *its own value*
29
+ // actually changes. snapshot() then linearly interpolates each DoF from its
30
+ // previous value to its current target over its own EWMA interval.
32
31
  //
33
- // Special cases handled in snapshot():
34
- // - t >= 1 : camera has reached the target, returns target_ directly.
35
- // - Snap-through guard: deltas > kPosSnapThroughDeg bypass interpolation so
36
- // deliberate teleports (setCamera(NewYork → Tokyo)) remain instant.
37
- // - First setHpr for a given lat/lon: posFrom is set to the *destination*
38
- // so lerp(dest, dest, t) = dest — no startup glide.
39
- // - setAll (initialisation only): does not touch interpolation state; always
40
- // snaps directly.
32
+ // This means lat/lon, altitude, and heading each self-tune independently:
33
+ // - 60 Hz gesture input → EWMA ~16 ms → interpolation near-instant (snap)
34
+ // - 1 Hz GPS → EWMA ~1 s → constant-velocity glide between fixes
35
+ // - 50 Hz barometer for altitude + 1 Hz GPS for lat/lon → altitude behaves
36
+ // near-instant, lat/lon glides — with no special-casing needed.
37
+ //
38
+ // Special cases in snapshot():
39
+ // - t >= 1: camera has reached the target, returns target_ directly.
40
+ // - Snap-through guard: large jumps bypass interpolation so deliberate
41
+ // teleports remain instant.
42
+ // - First change per DoF: "from" = destination, so lerp(dest, dest, t) = dest
43
+ // for all t — clean snap, no startup glide.
44
+ // - setAll (init-only): never touches any interpolation state.
45
+ //
46
+ // pitch / roll / viewCorrection are NOT interpolated here; CameraSmoother
47
+ // applies fixed-rate exponential curves to them (appropriate for high-rate
48
+ // attitude sensors).
41
49
  class CameraTargetState {
42
50
  public:
43
51
  CameraTargetState() = default;
44
52
 
45
53
  // Replace the entire target. Used only for engine initialisation; does NOT
46
- // update position-interpolation state so the first rendered frame snaps.
54
+ // update any interpolation state so the first rendered frame snaps.
47
55
  void setAll(const CameraParams& target);
48
56
 
49
57
  // Update lat/lon/alt + heading/pitch/roll, leaving viewCorrection unchanged.
50
- // When lat/lon actually changes, updates the linear-interpolation bookkeeping.
58
+ // Independently tracks cadence for lat/lon, altitude, and heading.
51
59
  void setHpr(double lat, double lon, double alt,
52
60
  double heading, double pitch, double roll);
53
61
 
54
62
  void setViewCorrection(const glm::dquat& q);
55
63
 
56
- // Snapshot the demand target for this render frame. Lat/lon are linearly
57
- // interpolated between the previous target (posFrom) and the current target
58
- // over the EWMA inter-arrival interval. All other DoFs are returned as-is
59
- // (the smoother handles their exponential curves).
64
+ // Snapshot the demand target for this render frame. lat/lon, altitude, and
65
+ // heading are linearly interpolated to their "current" position based on
66
+ // each DoF's own EWMA interval. pitch/roll/viewCorrection are returned as-is
67
+ // (CameraSmoother handles their exponential curves).
60
68
  CameraParams snapshot() const;
61
69
 
62
70
  // Lightweight, lock-free check used by the render thread to decide whether
@@ -86,18 +94,33 @@ private:
86
94
  std::atomic<bool> dirty_{false};
87
95
  std::atomic<bool> forceRender_{true};
88
96
 
89
- // ── Lat/lon linear interpolation state (all protected by mutex_) ──────────
90
- // posFromLat_/posFromLon_: the target position when the last update arrived
91
- // (i.e. where we are smoothly moving FROM).
92
- // posUpdateTime_: wall-clock time of the last lat/lon change.
93
- // ewmaIntervalSec_: EWMA of inter-arrival times of lat/lon changes.
94
- // hasInterpolation_: true once we have valid posFrom + posUpdateTime
95
- // data (false until the second distinct lat/lon value).
96
- double posFromLat_{0.0};
97
- double posFromLon_{0.0};
98
- std::chrono::steady_clock::time_point posUpdateTime_{};
99
- double ewmaIntervalSec_{0.0};
100
- bool hasInterpolation_{false};
97
+ // ── Per-DoF linear interpolation state (all protected by mutex_) ─────────
98
+ // Each DoF has: a "from" value, a wall-clock timestamp of the last change,
99
+ // an EWMA of inter-arrival times, and an "active" flag.
100
+ //
101
+ // Pattern per DoF:
102
+ // First change: from = destination (snap), seed EWMA, set active.
103
+ // Subsequent change: from = old target, update EWMA, reset clock.
104
+ // snapshot(): t = clamp(elapsed/ewma, 0, 1); result = from + t*delta.
105
+
106
+ // lat / lon
107
+ double posFromLat_{0.0};
108
+ double posFromLon_{0.0};
109
+ std::chrono::steady_clock::time_point posUpdateTime_{};
110
+ double posEwmaIntervalSec_{0.0};
111
+ bool hasPosInterp_{false};
112
+
113
+ // altitude
114
+ double altFrom_{0.0};
115
+ std::chrono::steady_clock::time_point altUpdateTime_{};
116
+ double altEwmaIntervalSec_{0.0};
117
+ bool hasAltInterp_{false};
118
+
119
+ // heading
120
+ double hdgFrom_{0.0};
121
+ std::chrono::steady_clock::time_point hdgUpdateTime_{};
122
+ double hdgEwmaIntervalSec_{0.0};
123
+ bool hasHdgInterp_{false};
101
124
  };
102
125
 
103
126
  } // namespace reactnativecesium
@@ -434,7 +434,9 @@ void CesiumEngine::updateFrame(double w, double h, FrameResult& result) {
434
434
  asyncSystem_.dispatchMainThreadTasks();
435
435
 
436
436
  const auto viewState = camera_.computeViewState(w, h);
437
- const auto& updateResult = tileset_->updateView({viewState});
437
+ const auto& updateResult =
438
+ tileset_->updateViewGroup(tileset_->getDefaultViewGroup(), {viewState});
439
+ tileset_->loadTiles();
438
440
 
439
441
  // Only emit the fallback ellipsoid when no real terrain is being rendered
440
442
  // this frame. Otherwise it would be over-drawn anyway (~8k vertices and
@@ -43,31 +43,43 @@ inline constexpr int32_t kDefaultLoadingDescendantLimit = 20;
43
43
 
44
44
  // Camera smoothing constants — shared between platform bridges so jank /
45
45
  // motion feel is identical on iOS and Android.
46
- inline constexpr double kSmoothAltitude = 25.0;
47
- inline constexpr double kSmoothHeading = 30.0;
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/τ).
48
57
  inline constexpr double kSmoothPitch = 50.0;
49
58
  inline constexpr double kSmoothRoll = 50.0;
50
59
  inline constexpr double kSmoothViewCorr = 50.0;
60
+
61
+ // Convergence epsilons (used by deltaExceedsEpsilon and snap-to-exact).
51
62
  inline constexpr double kEpsLatLon = 1e-7;
52
63
  inline constexpr double kEpsAltitudeMeters = 0.1;
53
64
  inline constexpr double kEpsAngleDeg = 0.05;
54
65
  inline constexpr double kEpsQuatDot = 1e-8;
55
66
 
56
- // Lat/lon are smoothed adaptively: τ = clamp(α · ewmaInterval, τmin, τmax),
57
- // then per frame we apply 1 - exp(-dt/τ). α<1 means the camera converges
58
- // before the next sample lands. With ~60 Hz gestures the EWMA interval is
59
- // ~16 ms ⇒ τ at the floor (5 ms) ⇒ ≤1-frame trail (indistinguishable from
60
- // the previous snap). With a 1 Hz GPS feed τ ≈ 450 ms ⇒ ~95 % converged
61
- // before the next sample arrives, removing the per-second teleport.
67
+ // Linear interpolation parameters shared by lat/lon, altitude, and heading.
68
+ //
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).
62
73
  inline constexpr double kSmoothPositionAlpha = 0.45;
63
- inline constexpr double kSmoothPositionTauMin = 0.005;
64
74
  inline constexpr double kSmoothPositionTauMax = 0.6;
65
- // Genuine teleports (e.g. setCamera(NewYork) → setCamera(Tokyo)) skip the
66
- // smoother — easing across the planet over 600 ms looks worse than a snap.
67
- inline constexpr double kPosSnapThroughDeg = 0.5;
68
- // EWMA mixing factor for the inter-arrival interval of setCamera calls.
69
75
  inline constexpr double kEwmaIntervalAlpha = 0.3;
70
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
82
+
71
83
  // Metrics throttle: emit telemetry every N rendered frames.
72
84
  inline constexpr int kMetricsEmitEveryFrames = 20;
73
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-cesium",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
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",
@@ -79,7 +79,7 @@
79
79
  "conventional-changelog-conventionalcommits": "^9.3.1",
80
80
  "nitrogen": "0.35.6",
81
81
  "react": "19.2.3",
82
- "react-native": "0.85.2",
82
+ "react-native": "0.85.3",
83
83
  "react-native-builder-bob": "^0.41.0",
84
84
  "semantic-release": "^25.0.3",
85
85
  "typescript": "^5.9.3"