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
@@ -1,8 +1,7 @@
1
1
  #import "CesiumBridge.h"
2
2
  #import <CoreFoundation/CoreFoundation.h>
3
3
 
4
- #include "engine/CameraSmoother.hpp"
5
- #include "engine/CameraTargetState.hpp"
4
+ #include "engine/CameraIntegrator.hpp"
6
5
  #include "engine/CesiumEngine.hpp"
7
6
  #include "engine/EngineTunables.hpp"
8
7
  #include "engine/GeoidConverter.hpp"
@@ -13,6 +12,7 @@
13
12
 
14
13
  #include <algorithm>
15
14
  #include <atomic>
15
+ #include <chrono>
16
16
  #include <cmath>
17
17
  #include <memory>
18
18
  #include <string>
@@ -22,15 +22,25 @@
22
22
  std::unique_ptr<reactnativecesium::CesiumEngine> _engine;
23
23
  reactnativecesium::FrameResult _frameResult;
24
24
 
25
- reactnativecesium::EngineConfig _config;
26
- reactnativecesium::CameraTargetState _target;
27
- reactnativecesium::MetricsAggregator _metrics;
25
+ reactnativecesium::EngineConfig _config;
26
+ std::unique_ptr<reactnativecesium::CameraIntegrator> _integrator;
27
+ reactnativecesium::MetricsAggregator _metrics;
28
+
29
+ // Force-render flag — set on every demand change so the render loop knows
30
+ // to dispatch at least one frame to advance the integrator. Cleared once
31
+ // the integrator reports isActive() == false.
32
+ std::atomic<bool> _forceRender;
28
33
 
29
34
  int _viewportWidth;
30
35
  int _viewportHeight;
31
36
  BOOL _initialized;
32
37
  BOOL _suspended;
33
38
 
39
+ // Last wall-clock tick used to compute the metrics-only frame dt EMA. Kept
40
+ // per-instance (not a function-local static) so a hot reload that destroys
41
+ // and recreates the bridge does not pollute the new bridge's first frame.
42
+ double _lastTickSec;
43
+
34
44
  NSString* _cacheDir;
35
45
  }
36
46
 
@@ -44,6 +54,8 @@
44
54
  _viewportHeight = height;
45
55
  _initialized = NO;
46
56
  _suspended = NO;
57
+ _lastTickSec = 0.0;
58
+ _forceRender.store(true, std::memory_order_release);
47
59
  _cacheDir = [cacheDir copy];
48
60
 
49
61
  _config.cacheDatabasePath =
@@ -79,7 +91,7 @@
79
91
  - (void)appWillResignActive:(NSNotification *)note { _suspended = YES; }
80
92
  - (void)appDidBecomeActive:(NSNotification *)note {
81
93
  _suspended = NO;
82
- _target.requestForceRender();
94
+ _forceRender.store(true, std::memory_order_release);
83
95
  }
84
96
 
85
97
  - (void)buildEngine {
@@ -87,9 +99,11 @@
87
99
  _engine = std::make_unique<reactnativecesium::CesiumEngine>();
88
100
  _engine->initialize(_config);
89
101
 
90
- // Seed the demand target from the engine's initial camera so the first
91
- // frame is not a "snap from default" jump.
92
- _target.setAll(_engine->camera().getParams());
102
+ // Seed the integrator from the engine's initial camera (which itself was
103
+ // populated by GlobeCamera's defaults). This puts the demand and actual
104
+ // states in sync from the very first frame; no "snap from default" jump.
105
+ _integrator = std::make_unique<reactnativecesium::CameraIntegrator>(
106
+ _engine->camera().getParams());
93
107
 
94
108
  auto* backendPtr = _metalBackend.get();
95
109
  _engine->getResourcePreparer()->setGPUTextureCreator(
@@ -100,8 +114,6 @@
100
114
  [](void* tex) {
101
115
  if (tex) CFRelease(tex);
102
116
  });
103
- // Water mask textures reuse the same MTLTexture creator; the binding slot is
104
- // set at draw time on Metal so no separate factory is needed.
105
117
  _engine->getResourcePreparer()->setWaterMaskTextureCreator(
106
118
  [backendPtr](const uint8_t* pixels, int32_t w, int32_t h) -> void* {
107
119
  return backendPtr->createRasterTexture(pixels, w, h);
@@ -115,7 +127,7 @@
115
127
  - (void)applyEngineConfig {
116
128
  if (!_engine) return;
117
129
  _engine->updateConfig(_config);
118
- _target.requestForceRender();
130
+ _forceRender.store(true, std::memory_order_release);
119
131
  }
120
132
 
121
133
  - (void)updateIonAccessToken:(NSString *)token assetId:(int64_t)assetId {
@@ -129,53 +141,93 @@
129
141
  if (!_initialized) return;
130
142
  _config.ionImageryAssetId = (assetId <= 0) ? 1 : assetId;
131
143
  _engine->setImageryAssetId(_config.ionImageryAssetId);
132
- _target.requestForceRender();
144
+ _forceRender.store(true, std::memory_order_release);
145
+ }
146
+
147
+ // ── Per-DoF setters ──────────────────────────────────────────────────────
148
+
149
+ - (void)setPositionLatitude:(double)lat longitude:(double)lon {
150
+ if (!_initialized) return;
151
+ _integrator->setPosition(lat, lon);
152
+ _forceRender.store(true, std::memory_order_release);
133
153
  }
134
154
 
135
- - (void)updateCameraLatitude:(double)lat
136
- longitude:(double)lon
137
- altitude:(double)alt
138
- heading:(double)heading
139
- pitch:(double)pitch
140
- roll:(double)roll {
155
+ - (void)setAltitude:(double)alt {
141
156
  if (!_initialized) return;
142
- _target.setHpr(lat, lon, alt, heading, pitch, roll);
143
- }
144
-
145
- - (void)updateCameraQuaternionLatitude:(double)lat
146
- longitude:(double)lon
147
- altitude:(double)alt
148
- heading:(double)heading
149
- pitch:(double)pitch
150
- roll:(double)roll
151
- viewCorrectionW:(double)qw
152
- x:(double)qx
153
- y:(double)qy
154
- z:(double)qz {
157
+ _integrator->setAltitude(alt);
158
+ _forceRender.store(true, std::memory_order_release);
159
+ }
160
+
161
+ - (void)setHeadingDeg:(double)deg {
155
162
  if (!_initialized) return;
156
- _target.setHpr(lat, lon, alt, heading, pitch, roll);
157
- const double ql2 = qw * qw + qx * qx + qy * qy + qz * qz;
158
- glm::dquat q;
159
- if (ql2 < 1e-20) {
160
- q = glm::dquat(1.0, 0.0, 0.0, 0.0);
161
- } else {
162
- const double inv = 1.0 / std::sqrt(ql2);
163
- q = glm::dquat(qw * inv, qx * inv, qy * inv, qz * inv);
164
- }
165
- _target.setViewCorrection(q);
163
+ _integrator->setHeading(deg);
164
+ _forceRender.store(true, std::memory_order_release);
165
+ }
166
+
167
+ - (void)setAttitudePitch:(double)pitch roll:(double)roll {
168
+ if (!_initialized) return;
169
+ _integrator->setAttitude(pitch, roll);
170
+ _forceRender.store(true, std::memory_order_release);
171
+ }
172
+
173
+ - (void)setViewCorrectionW:(double)qw x:(double)qx y:(double)qy z:(double)qz {
174
+ if (!_initialized) return;
175
+ _integrator->setViewCorrection(glm::dquat(qw, qx, qy, qz));
176
+ _forceRender.store(true, std::memory_order_release);
177
+ }
178
+
179
+ - (void)teleportLatitude:(double)lat
180
+ longitude:(double)lon
181
+ altitude:(double)alt
182
+ heading:(double)heading
183
+ pitch:(double)pitch
184
+ roll:(double)roll
185
+ verticalFovDeg:(double)vfov {
186
+ if (!_initialized) return;
187
+ reactnativecesium::CameraParams p;
188
+ p.latitude = lat;
189
+ p.longitude = lon;
190
+ p.altitude = alt;
191
+ p.heading = heading;
192
+ p.pitch = pitch;
193
+ p.roll = roll;
194
+ p.verticalFov = vfov;
195
+ // Keep the latest viewCorrection target — teleport is a position/orientation
196
+ // jump, not a boresight reset. Use setViewCorrection(identity) to clear it.
197
+ p.viewCorrection = _integrator->getActual().viewCorrection;
198
+ _integrator->teleport(p);
199
+ // Apply directly to the engine so the next render uses the teleported value
200
+ // without one frame of "almost there" interpolation.
201
+ _engine->camera().setParams(p);
202
+ _forceRender.store(true, std::memory_order_release);
166
203
  }
167
204
 
168
205
  - (void)resize:(int)width height:(int)height {
169
206
  _viewportWidth = width;
170
207
  _viewportHeight = height;
171
208
  if (_metalBackend) _metalBackend->resize(width, height);
172
- _target.requestForceRender();
209
+ _forceRender.store(true, std::memory_order_release);
173
210
  }
174
211
 
175
212
  - (void)setVerticalFovDeg:(double)degrees {
176
213
  if (!_initialized) return;
177
- _engine->camera().setVerticalFovDegrees(degrees);
178
- _target.requestForceRender();
214
+ _integrator->setVerticalFov(degrees);
215
+ _forceRender.store(true, std::memory_order_release);
216
+ }
217
+
218
+ - (void)setRateCapsYaw:(double)yawDegSec
219
+ pitch:(double)pitchDegSec
220
+ roll:(double)rollDegSec
221
+ climb:(double)climbMps
222
+ groundSpeed:(double)groundMps {
223
+ if (!_initialized) return;
224
+ reactnativecesium::CameraRateCaps caps;
225
+ caps.maxYawRateDegSec = yawDegSec;
226
+ caps.maxPitchRateDegSec = pitchDegSec;
227
+ caps.maxRollRateDegSec = rollDegSec;
228
+ caps.maxClimbRateMps = climbMps;
229
+ caps.maxGroundSpeedMps = groundMps;
230
+ _integrator->setRateCaps(caps);
179
231
  }
180
232
 
181
233
  - (void)setMaximumScreenSpaceError:(double)v {
@@ -241,43 +293,35 @@
241
293
  - (void)setMinAltitudeAboveTerrain:(float)v {
242
294
  _config.minAltitudeAboveTerrain = std::max(0.0f, v);
243
295
  if (_engine) _engine->updateConfig(_config);
244
- _target.requestForceRender();
296
+ _forceRender.store(true, std::memory_order_release);
245
297
  }
246
298
 
247
299
  - (void)setMsaaSampleCount:(int)samples {
248
300
  if (_metalBackend) _metalBackend->setMsaaSampleCount(samples);
249
- _target.requestForceRender();
301
+ _forceRender.store(true, std::memory_order_release);
250
302
  }
251
303
 
252
- - (void)markNeedsRender { _target.requestForceRender(); }
304
+ - (void)markNeedsRender { _forceRender.store(true, std::memory_order_release); }
253
305
 
254
306
  - (BOOL)shouldRenderNextFrame {
255
307
  if (!_initialized || _suspended || !_engine) return NO;
256
- if (_target.consumeForceRender()) {
257
- _target.requestForceRender(); // sticky for one frame so we make it through
258
- return YES;
259
- }
308
+ if (_forceRender.load(std::memory_order_acquire)) return YES;
260
309
  if (_frameResult.tilesLoading > 0 || !_frameResult.tilesetActive) return YES;
261
- if (_target.isDirty()) {
262
- const auto cur = _engine->camera().getParams();
263
- const auto tgt = _target.snapshot();
264
- if (reactnativecesium::CameraTargetState::deltaExceedsEpsilon(cur, tgt)) {
265
- return YES;
266
- }
267
- _target.clearDirty();
268
- }
310
+ if (_integrator && _integrator->isActive()) return YES;
269
311
  return NO;
270
312
  }
271
313
 
272
- - (void)renderFrameWithDt:(double)dt {
314
+ - (void)renderFrameAt:(double)nowSeconds {
273
315
  if (!_initialized || _suspended) return;
316
+ if (!_engine || !_integrator || !_metalBackend) return;
274
317
 
275
318
  @autoreleasepool {
276
- (void)_target.consumeForceRender();
319
+ // Consume the force-render flag (we're rendering this frame either way).
320
+ _forceRender.store(false, std::memory_order_release);
277
321
 
278
- const auto cur = _engine->camera().getParams();
279
- const auto tgt = _target.snapshot();
280
- auto smooth = reactnativecesium::CameraSmoother::step(cur, tgt, dt);
322
+ // The integrator owns its own clock now — see CameraIntegrator::step().
323
+ // `nowSeconds` is still used below for the FPS-EMA dt only.
324
+ auto actual = _integrator->step();
281
325
 
282
326
  // ── Terrain floor clamp ───────────────────────────────────────────────
283
327
  // Uses terrain floor from the previous frame (one-frame lag is
@@ -285,23 +329,26 @@
285
329
  const float minAbove = _engine->getConfig().minAltitudeAboveTerrain;
286
330
  if (minAbove > 0.0f) {
287
331
  const double geoidOffset =
288
- reactnativecesium::mslToEllipsoidMeters(smooth.latitude, smooth.longitude, 0.0);
332
+ reactnativecesium::mslToEllipsoidMeters(actual.latitude, actual.longitude, 0.0);
289
333
  const double camEllipsoid =
290
- reactnativecesium::mslToEllipsoidMeters(smooth.latitude, smooth.longitude, smooth.altitude);
334
+ reactnativecesium::mslToEllipsoidMeters(actual.latitude, actual.longitude, actual.altitude);
291
335
  const double minEllipsoid =
292
336
  static_cast<double>(_engine->terrainFloorEllipsoidMeters()) + minAbove;
293
337
  if (camEllipsoid < minEllipsoid) {
294
- smooth.altitude = minEllipsoid - geoidOffset;
338
+ const double clampedMsl = minEllipsoid - geoidOffset;
339
+ _integrator->clampActualAltitude(clampedMsl);
340
+ actual.altitude = clampedMsl;
295
341
  }
296
342
  }
297
343
 
298
- _engine->camera().setParams(smooth);
299
-
300
- if (!reactnativecesium::CameraTargetState::deltaExceedsEpsilon(smooth, tgt)) {
301
- _target.clearDirty();
302
- }
344
+ _engine->camera().setParams(actual);
303
345
 
304
346
  _engine->updateFrame(_viewportWidth, _viewportHeight, _frameResult);
347
+ // For metrics purposes only — keeps the FPS EMA in something close to the
348
+ // wall-clock cadence. Not used by the integrator.
349
+ double dt = (_lastTickSec > 0.0) ? (nowSeconds - _lastTickSec) : (1.0 / 60.0);
350
+ _lastTickSec = nowSeconds;
351
+ if (dt > 0.5 || dt <= 0.0) dt = 1.0 / 60.0;
305
352
  _metrics.tick(dt, _frameResult, !_config.tlsCaBundlePath.empty());
306
353
 
307
354
  reactnativecesium::FrameParams params;
@@ -322,33 +369,72 @@
322
369
  return [NSString stringWithUTF8String:_metrics.latest().creditsPlainText.c_str()];
323
370
  }
324
371
 
325
- - (double)readCameraLatitude { return _engine ? _engine->camera().getParams().latitude : 0.0; }
326
- - (double)readCameraLongitude { return _engine ? _engine->camera().getParams().longitude : 0.0; }
327
- - (double)readCameraAltitude { return _engine ? _engine->camera().getParams().altitude : 0.0; }
328
- - (double)readCameraHeading { return _engine ? _engine->camera().getParams().heading : 0.0; }
329
- - (double)readCameraPitch { return _engine ? _engine->camera().getParams().pitch : 0.0; }
330
- - (double)readCameraRoll { return _engine ? _engine->camera().getParams().roll : 0.0; }
331
- - (double)readVerticalFovDeg { return _engine ? _engine->camera().getVerticalFovDegrees() : 60.0; }
372
+ // ── Actual camera readback (post-integration; what was rendered) ─────────
373
+ - (double)readActualLatitude { return _integrator ? _integrator->getActual().latitude : 0.0; }
374
+ - (double)readActualLongitude { return _integrator ? _integrator->getActual().longitude : 0.0; }
375
+ - (double)readActualAltitude { return _integrator ? _integrator->getActual().altitude : 0.0; }
376
+ - (double)readActualHeading { return _integrator ? _integrator->getActual().heading : 0.0; }
377
+ - (double)readActualPitch { return _integrator ? _integrator->getActual().pitch : 0.0; }
378
+ - (double)readActualRoll { return _integrator ? _integrator->getActual().roll : 0.0; }
379
+ - (double)readActualVerticalFovDeg { return _integrator ? _integrator->getActual().verticalFov : 60.0; }
380
+
381
+ // ── Demand camera readback (what the consumer last asked for) ────────────
382
+ - (double)readDemandLatitude { return _integrator ? _integrator->getDemand().latitude : 0.0; }
383
+ - (double)readDemandLongitude { return _integrator ? _integrator->getDemand().longitude : 0.0; }
384
+ - (double)readDemandAltitude { return _integrator ? _integrator->getDemand().altitude : 0.0; }
385
+ - (double)readDemandHeading { return _integrator ? _integrator->getDemand().heading : 0.0; }
386
+ - (double)readDemandPitch { return _integrator ? _integrator->getDemand().pitch : 0.0; }
387
+ - (double)readDemandRoll { return _integrator ? _integrator->getDemand().roll : 0.0; }
388
+ - (double)readDemandVerticalFovDeg { return _integrator ? _integrator->getDemand().verticalFov : 60.0; }
332
389
 
333
390
  - (double)readViewCorrectionW {
334
- if (!_engine) return 1.0;
335
- return _engine->camera().getParams().viewCorrection.w;
391
+ return _integrator ? _integrator->getActual().viewCorrection.w : 1.0;
336
392
  }
337
393
  - (double)readViewCorrectionX {
338
- return _engine ? _engine->camera().getParams().viewCorrection.x : 0.0;
394
+ return _integrator ? _integrator->getActual().viewCorrection.x : 0.0;
339
395
  }
340
396
  - (double)readViewCorrectionY {
341
- return _engine ? _engine->camera().getParams().viewCorrection.y : 0.0;
397
+ return _integrator ? _integrator->getActual().viewCorrection.y : 0.0;
342
398
  }
343
399
  - (double)readViewCorrectionZ {
344
- return _engine ? _engine->camera().getParams().viewCorrection.z : 0.0;
400
+ return _integrator ? _integrator->getActual().viewCorrection.z : 0.0;
345
401
  }
346
402
 
347
403
  - (void)shutdown {
348
404
  [[NSNotificationCenter defaultCenter] removeObserver:self];
405
+
406
+ // Mark uninitialised up-front so any late prop setters that race the
407
+ // teardown become no-ops instead of touching half-destroyed state.
408
+ _initialized = NO;
409
+
349
410
  if (_engine) _engine->shutdown();
350
411
  if (_metalBackend) _metalBackend->shutdown();
351
- _initialized = NO;
412
+
413
+ // Release the engine (and therefore the SqliteCache it owns) here rather
414
+ // than waiting for ARC to dealloc this CesiumBridge. During fast refresh
415
+ // the next CesiumView can mount and open the same cesium_cache.db file
416
+ // before our owning Swift view's deinit completes; freeing the unique_ptr
417
+ // now closes the SQLite handle synchronously and avoids "database is
418
+ // locked" warnings from the new engine's SqliteCache.
419
+ _integrator.reset();
420
+ _engine.reset();
421
+ _metalBackend.reset();
422
+ }
423
+
424
+ - (void)dealloc {
425
+ // Defensive: shutdown should already have run from the owning Swift view's
426
+ // deinit. If something exotic happened (e.g. a partially-built bridge that
427
+ // never had shutdown invoked, or an Objective-C exception path) we still
428
+ // need to detach from NSNotificationCenter and free the C++ owners in a
429
+ // deterministic order before ARC walks the ivar block.
430
+ if (_initialized) {
431
+ [self shutdown];
432
+ } else {
433
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
434
+ _integrator.reset();
435
+ _engine.reset();
436
+ _metalBackend.reset();
437
+ }
352
438
  }
353
439
 
354
440
  @end