react-native-cesium 1.0.6 → 1.2.0

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 (59) hide show
  1. package/README.md +64 -11
  2. package/ReactNativeCesium.podspec +1 -1
  3. package/android/CMakeLists.txt +5 -3
  4. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +197 -227
  5. package/android/src/main/cpp/CesiumBridgeAndroid.h +48 -49
  6. package/android/src/main/cpp/cpp-adapter.cpp +2 -2
  7. package/android/src/main/cpp/vulkan/VulkanBackend.cpp +1010 -569
  8. package/android/src/main/cpp/vulkan/VulkanBackend.h +121 -12
  9. package/android/src/main/cpp/vulkan/shaders/sky.frag +20 -10
  10. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +28 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +295 -58
  12. package/cpp/engine/CameraSmoother.cpp +99 -0
  13. package/cpp/engine/CameraSmoother.hpp +29 -0
  14. package/cpp/engine/CameraTargetState.cpp +49 -0
  15. package/cpp/engine/CameraTargetState.hpp +121 -0
  16. package/cpp/engine/CesiumEngine.cpp +206 -60
  17. package/cpp/engine/CesiumEngine.hpp +77 -10
  18. package/cpp/engine/EngineTunables.hpp +79 -0
  19. package/cpp/engine/GeoidConverter.cpp +36 -0
  20. package/cpp/engine/GeoidConverter.hpp +10 -0
  21. package/cpp/engine/GlobeCamera.cpp +7 -2
  22. package/cpp/engine/MetricsAggregator.cpp +71 -0
  23. package/cpp/engine/MetricsAggregator.hpp +54 -0
  24. package/cpp/engine/TileLifecycleManager.cpp +0 -12
  25. package/cpp/engine/TileLifecycleManager.hpp +0 -3
  26. package/cpp/third_party/egm96/EGM96.c +289 -0
  27. package/cpp/third_party/egm96/EGM96.h +45 -0
  28. package/cpp/third_party/egm96/EGM96_data.h +65372 -0
  29. package/ios/CesiumBridge.h +24 -5
  30. package/ios/CesiumBridge.mm +149 -271
  31. package/ios/HybridCesiumView.swift +197 -40
  32. package/ios/metal/MetalBackend.mm +72 -23
  33. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +26 -0
  34. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  35. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +5 -1
  36. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +109 -1
  37. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +24 -0
  38. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +48 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +25 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +32 -2
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +72 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Quaternion.kt +19 -0
  43. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +30 -0
  44. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +85 -1
  45. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +60 -0
  46. package/nitrogen/generated/ios/swift/CesiumMetrics.swift +7 -2
  47. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +12 -0
  48. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +288 -0
  49. package/nitrogen/generated/shared/c++/CesiumMetrics.hpp +5 -1
  50. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +24 -0
  51. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +25 -1
  52. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +132 -0
  53. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +13 -1
  54. package/nitrogen/generated/shared/json/CesiumViewConfig.json +12 -0
  55. package/package.json +4 -4
  56. package/scripts/cesium/clone.mjs +1 -1
  57. package/src/specs/cesium-view.nitro.ts +29 -0
  58. package/cpp/engine/DeletionQueue.cpp +0 -28
  59. package/cpp/engine/DeletionQueue.hpp +0 -28
@@ -3,6 +3,9 @@
3
3
 
4
4
  NS_ASSUME_NONNULL_BEGIN
5
5
 
6
+ // Thin Obj-C++ shell for the iOS side. All cross-platform logic (camera
7
+ // demand state + smoothing, FPS / metrics aggregation, credit-HTML stripping)
8
+ // lives in shared C++ next to CesiumEngine.
6
9
  @interface CesiumBridge : NSObject
7
10
 
8
11
  - (instancetype)initWithMetalLayer:(CAMetalLayer *)layer
@@ -18,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN
18
21
  heading:(double)heading
19
22
  pitch:(double)pitch
20
23
  roll:(double)roll;
21
- /// Same as `updateCameraLatitude:...` plus camera-space view correction (w,x,y,z). Does not replace HPR; applied after HPR in the engine.
24
+ /// Same as `updateCameraLatitude:...` plus a camera-space view correction (w,x,y,z) applied after HPR.
22
25
  - (void)updateCameraQuaternionLatitude:(double)lat
23
26
  longitude:(double)lon
24
27
  altitude:(double)alt
@@ -38,13 +41,27 @@ NS_ASSUME_NONNULL_BEGIN
38
41
  // Camera / globe
39
42
  - (void)setVerticalFovDeg:(double)degrees;
40
43
 
41
- // Tileset tuning (triggers rebuild when changed)
44
+ // Tileset tuning (runtime-mutable; no full rebuild)
42
45
  - (void)setMaximumScreenSpaceError:(double)v;
43
- - (void)setMaximumSimultaneousTileLoads:(int)v;
44
- - (void)setLoadingDescendantLimit:(int)v;
46
+ - (void)setMaximumSimultaneousTileLoads:(int32_t)v;
47
+ - (void)setLoadingDescendantLimit:(int32_t)v;
45
48
 
46
49
  - (void)setMsaaSampleCount:(int)samples;
47
50
 
51
+ // Optional perf / quality knobs (defaults match EngineConfig).
52
+ - (void)setMaximumCachedMiB:(int32_t)v;
53
+ - (void)setPreloadAncestors:(BOOL)v;
54
+ - (void)setPreloadSiblings:(BOOL)v;
55
+ - (void)setForbidHoles:(BOOL)v;
56
+ - (void)setEnableWaterMask:(BOOL)v;
57
+ - (void)setEnableFogCulling:(BOOL)v;
58
+ - (void)setEnforceCulledScreenSpaceError:(BOOL)v;
59
+ - (void)setCulledScreenSpaceError:(double)v;
60
+ - (void)setEnableLodTransitionPeriod:(BOOL)v;
61
+ - (void)setLodTransitionLength:(double)v;
62
+ - (void)setSqliteCacheMaxRows:(int32_t)v;
63
+ - (void)setTaskProcessorThreads:(int32_t)v;
64
+
48
65
  /// Throttled metrics consumed by the Swift hybrid view.
49
66
  @property (nonatomic, readonly) double metricsFps;
50
67
  @property (nonatomic, readonly) NSInteger metricsTilesRendered;
@@ -52,7 +69,9 @@ NS_ASSUME_NONNULL_BEGIN
52
69
  @property (nonatomic, readonly) NSInteger metricsTilesVisited;
53
70
  @property (nonatomic, readonly) BOOL metricsIonTokenConfigured;
54
71
  @property (nonatomic, readonly) BOOL metricsTilesetReady;
55
- @property (nonatomic, readonly) NSString *metricsCreditsPlainText;
72
+ @property (nonatomic, readonly) BOOL metricsTlsConfigured;
73
+ @property (nonatomic, readonly, copy) NSString *metricsCreditsPlainText;
74
+
56
75
  - (double)readCameraLatitude;
57
76
  - (double)readCameraLongitude;
58
77
  - (double)readCameraAltitude;
@@ -1,117 +1,36 @@
1
1
  #import "CesiumBridge.h"
2
2
  #import <CoreFoundation/CoreFoundation.h>
3
3
 
4
+ #include "engine/CameraSmoother.hpp"
5
+ #include "engine/CameraTargetState.hpp"
4
6
  #include "engine/CesiumEngine.hpp"
7
+ #include "engine/EngineTunables.hpp"
8
+ #include "engine/MetricsAggregator.hpp"
5
9
  #include "metal/MetalBackend.h"
6
10
 
7
11
  #include <glm/gtc/quaternion.hpp>
8
12
 
13
+ #include <algorithm>
9
14
  #include <atomic>
10
15
  #include <cmath>
11
16
  #include <memory>
12
17
  #include <string>
13
18
 
14
- namespace {
15
-
16
- // Strips HTML tags and collapses whitespace to produce plain-text attribution.
17
- static NSString* stripHtmlToPlain(NSString* html) {
18
- if (html.length == 0) return @"";
19
- NSError* err = nil;
20
- NSRegularExpression* tagRx =
21
- [NSRegularExpression regularExpressionWithPattern:@"<[^>]+>" options:0 error:&err];
22
- NSString* plain = [tagRx stringByReplacingMatchesInString:html
23
- options:0
24
- range:NSMakeRange(0, html.length)
25
- withTemplate:@" "];
26
- NSRegularExpression* wsRx =
27
- [NSRegularExpression regularExpressionWithPattern:@"\\s+" options:0 error:&err];
28
- plain = [wsRx stringByReplacingMatchesInString:plain
29
- options:0
30
- range:NSMakeRange(0, plain.length)
31
- withTemplate:@" "];
32
- return [plain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
33
- }
34
-
35
- double lerpAngleDeg(double a, double b, double t) {
36
- double diff = std::fmod(b - a + 540.0, 360.0) - 180.0;
37
- return a + diff * t;
38
- }
39
-
40
- double angleDeltaAbsDeg(double a, double b) {
41
- return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
42
- }
43
-
44
- } // namespace
45
-
46
- // ── Per-DOF smoothing. ────────────────────────────────────────────────────────
47
- static const double kSmoothAlt = 25.0;
48
- static const double kSmoothHdg = 30.0;
49
- static const double kSmoothRoll = 50.0;
50
- static const double kSmoothPitch = 50.0;
51
- static const double kSmoothViewCorr = 50.0;
52
- static const double kEpsLatLon = 1e-7;
53
- static const double kEpsAlt = 0.1;
54
- static const double kEpsAngleDeg = 0.05;
55
- static const double kEpsQuatDot = 1e-8;
19
+ @implementation CesiumBridge {
20
+ std::unique_ptr<reactnativecesium::MetalBackend> _metalBackend;
21
+ std::unique_ptr<reactnativecesium::CesiumEngine> _engine;
22
+ reactnativecesium::FrameResult _frameResult;
56
23
 
57
- static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
58
- return std::abs(glm::dot(glm::normalize(a), glm::normalize(b)));
59
- }
24
+ reactnativecesium::EngineConfig _config;
25
+ reactnativecesium::CameraTargetState _target;
26
+ reactnativecesium::MetricsAggregator _metrics;
60
27
 
61
- @implementation CesiumBridge {
62
- std::unique_ptr<reactnativecesium::MetalBackend> _metalBackend;
63
- std::unique_ptr<reactnativecesium::CesiumEngine> _engine;
64
- reactnativecesium::FrameResult _frameResult;
65
28
  int _viewportWidth;
66
29
  int _viewportHeight;
67
30
  BOOL _initialized;
31
+ BOOL _suspended;
68
32
 
69
33
  NSString* _cacheDir;
70
- NSString* _ionAccessToken;
71
- int64_t _ionTilesetAssetId;
72
-
73
- double _maxSSE;
74
- double _maxSimLoads;
75
- double _loadDescLim;
76
-
77
- // Demand target — incoming props/calls write here; render loop smooths toward it.
78
- reactnativecesium::CameraParams _camTarget;
79
- BOOL _forceRenderNextFrame;
80
-
81
- BOOL _suspended;
82
-
83
- double _fpsEma;
84
- int _metricsTick;
85
-
86
- double _metricsFps;
87
- NSInteger _metricsTilesRendered;
88
- NSInteger _metricsTilesLoading;
89
- NSInteger _metricsTilesVisited;
90
- BOOL _metricsIonTokenConfigured;
91
- BOOL _metricsTilesetReady;
92
- NSString* _metricsCreditsPlainText;
93
-
94
- NSString* _lastCreditHtmlJoined;
95
- }
96
-
97
- - (reactnativecesium::EngineConfig)makeEngineConfig {
98
- reactnativecesium::EngineConfig config;
99
- if (_ionAccessToken) {
100
- config.ionAccessToken = std::string([_ionAccessToken UTF8String]);
101
- }
102
- config.ionAssetId = _ionTilesetAssetId;
103
- config.cacheDatabasePath =
104
- _cacheDir ? std::string([_cacheDir UTF8String]) + "/cesium_cache.db" : "";
105
- NSString* caPem = [[NSBundle mainBundle] pathForResource:@"cacert" ofType:@"pem"];
106
- if (caPem.length > 0) {
107
- config.tlsCaBundlePath = std::string([caPem fileSystemRepresentation]);
108
- }
109
- config.maximumScreenSpaceError = std::max(1.0, _maxSSE);
110
- config.maximumSimultaneousTileLoads =
111
- static_cast<int32_t>(std::max(0.0, std::round(_maxSimLoads)));
112
- config.loadingDescendantLimit =
113
- static_cast<int32_t>(std::max(1.0, std::round(_loadDescLim)));
114
- return config;
115
34
  }
116
35
 
117
36
  - (instancetype)initWithMetalLayer:(CAMetalLayer *)layer
@@ -123,24 +42,22 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
123
42
  _viewportWidth = width;
124
43
  _viewportHeight = height;
125
44
  _initialized = NO;
126
- _cacheDir = [cacheDir copy];
127
- _ionAccessToken = nil;
128
- _ionTilesetAssetId = 1;
129
- _maxSSE = 32.0;
130
- _maxSimLoads = 12.0;
131
- _loadDescLim = 20.0;
132
- _forceRenderNextFrame = YES;
133
45
  _suspended = NO;
134
- _fpsEma = 0.0;
135
- _metricsTick = 0;
136
- _metricsFps = 0.0;
137
- _metricsTilesRendered = 0;
138
- _metricsTilesLoading = 0;
139
- _metricsTilesVisited = 0;
140
- _metricsIonTokenConfigured = NO;
141
- _metricsTilesetReady = NO;
142
- _metricsCreditsPlainText = @"";
143
- _lastCreditHtmlJoined = @"";
46
+ _cacheDir = [cacheDir copy];
47
+
48
+ _config.cacheDatabasePath =
49
+ cacheDir ? std::string([cacheDir UTF8String]) + "/cesium_cache.db" : "";
50
+
51
+ NSString* caPem = [[NSBundle mainBundle] pathForResource:@"cacert" ofType:@"pem"];
52
+ if (caPem.length > 0) {
53
+ _config.tlsCaBundlePath = std::string([caPem fileSystemRepresentation]);
54
+ }
55
+ static std::atomic<int> caWarned{0};
56
+ if (_config.tlsCaBundlePath.empty() && caWarned.fetch_add(1) == 0) {
57
+ NSLog(@"[ReactNativeCesium] cacert.pem not in main bundle — libcurl may "
58
+ @"fail TLS to api.cesium.com on device. Ensure the pod includes "
59
+ @"ios/cacert.pem (run pod install).");
60
+ }
144
61
 
145
62
  _metalBackend = std::make_unique<reactnativecesium::MetalBackend>();
146
63
  _metalBackend->initialize((__bridge void*)layer, width, height);
@@ -161,22 +78,17 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
161
78
  - (void)appWillResignActive:(NSNotification *)note { _suspended = YES; }
162
79
  - (void)appDidBecomeActive:(NSNotification *)note {
163
80
  _suspended = NO;
164
- _forceRenderNextFrame = YES;
81
+ _target.requestForceRender();
165
82
  }
166
83
 
167
84
  - (void)buildEngine {
168
- reactnativecesium::EngineConfig cfg = [self makeEngineConfig];
169
- static std::atomic<int> caWarned{0};
170
- if (cfg.tlsCaBundlePath.empty() && caWarned.fetch_add(1) == 0) {
171
- NSLog(@"[ReactNativeCesium] cacert.pem not in main bundle — libcurl may fail TLS to "
172
- @"api.cesium.com on device. Ensure the pod includes ios/cacert.pem (run pod install).");
173
- }
174
85
  _engine.reset();
175
86
  _engine = std::make_unique<reactnativecesium::CesiumEngine>();
176
- _engine->initialize(cfg);
87
+ _engine->initialize(_config);
177
88
 
178
- // Sync demand target with engine's initial camera defaults.
179
- _camTarget = _engine->camera().getParams();
89
+ // Seed the demand target from the engine's initial camera so the first
90
+ // frame is not a "snap from default" jump.
91
+ _target.setAll(_engine->camera().getParams());
180
92
 
181
93
  auto* backendPtr = _metalBackend.get();
182
94
  _engine->getResourcePreparer()->setGPUTextureCreator(
@@ -187,9 +99,8 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
187
99
  [](void* tex) {
188
100
  if (tex) CFRelease(tex);
189
101
  });
190
- // Water mask textures are identical MTLTexture objects — createRasterTexture
191
- // is reused. On Metal, the binding slot (texture index 1) is set at draw
192
- // time so no separate factory is needed.
102
+ // Water mask textures reuse the same MTLTexture creator; the binding slot is
103
+ // set at draw time on Metal so no separate factory is needed.
193
104
  _engine->getResourcePreparer()->setWaterMaskTextureCreator(
194
105
  [backendPtr](const uint8_t* pixels, int32_t w, int32_t h) -> void* {
195
106
  return backendPtr->createRasterTexture(pixels, w, h);
@@ -200,18 +111,24 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
200
111
  });
201
112
  }
202
113
 
114
+ - (void)applyEngineConfig {
115
+ if (!_engine) return;
116
+ _engine->updateConfig(_config);
117
+ _target.requestForceRender();
118
+ }
119
+
203
120
  - (void)updateIonAccessToken:(NSString *)token assetId:(int64_t)assetId {
204
121
  if (!_initialized) return;
205
- _ionAccessToken = [token copy];
206
- _ionTilesetAssetId = assetId;
207
- _engine->updateConfig([self makeEngineConfig]);
208
- _forceRenderNextFrame = YES;
122
+ _config.ionAccessToken = token ? std::string([token UTF8String]) : std::string();
123
+ _config.ionAssetId = assetId;
124
+ [self applyEngineConfig];
209
125
  }
210
126
 
211
127
  - (void)updateImageryAssetId:(int64_t)assetId {
212
128
  if (!_initialized) return;
213
- _engine->setImageryAssetId(assetId);
214
- _forceRenderNextFrame = YES;
129
+ _config.ionImageryAssetId = (assetId <= 0) ? 1 : assetId;
130
+ _engine->setImageryAssetId(_config.ionImageryAssetId);
131
+ _target.requestForceRender();
215
132
  }
216
133
 
217
134
  - (void)updateCameraLatitude:(double)lat
@@ -221,14 +138,7 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
221
138
  pitch:(double)pitch
222
139
  roll:(double)roll {
223
140
  if (!_initialized) return;
224
- _camTarget.latitude = lat;
225
- _camTarget.longitude = lon;
226
- _camTarget.altitude = alt;
227
- _camTarget.heading = heading;
228
- _camTarget.pitch = pitch;
229
- _camTarget.roll = roll;
230
- // Intentionally does not modify `_camTarget.viewCorrection`.
231
- _forceRenderNextFrame = YES;
141
+ _target.setHpr(lat, lon, alt, heading, pitch, roll);
232
142
  }
233
143
 
234
144
  - (void)updateCameraQuaternionLatitude:(double)lat
@@ -242,182 +152,153 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
242
152
  y:(double)qy
243
153
  z:(double)qz {
244
154
  if (!_initialized) return;
245
- _camTarget.latitude = lat;
246
- _camTarget.longitude = lon;
247
- _camTarget.altitude = alt;
248
- _camTarget.heading = heading;
249
- _camTarget.pitch = pitch;
250
- _camTarget.roll = roll;
155
+ _target.setHpr(lat, lon, alt, heading, pitch, roll);
251
156
  const double ql2 = qw * qw + qx * qx + qy * qy + qz * qz;
252
- glm::dquat q;
157
+ glm::dquat q;
253
158
  if (ql2 < 1e-20) {
254
159
  q = glm::dquat(1.0, 0.0, 0.0, 0.0);
255
160
  } else {
256
161
  const double inv = 1.0 / std::sqrt(ql2);
257
162
  q = glm::dquat(qw * inv, qx * inv, qy * inv, qz * inv);
258
163
  }
259
- _camTarget.viewCorrection = q;
260
- _forceRenderNextFrame = YES;
164
+ _target.setViewCorrection(q);
261
165
  }
262
166
 
263
167
  - (void)resize:(int)width height:(int)height {
264
168
  _viewportWidth = width;
265
169
  _viewportHeight = height;
266
170
  if (_metalBackend) _metalBackend->resize(width, height);
267
- _forceRenderNextFrame = YES;
171
+ _target.requestForceRender();
268
172
  }
269
173
 
270
174
  - (void)setVerticalFovDeg:(double)degrees {
271
175
  if (!_initialized) return;
272
176
  _engine->camera().setVerticalFovDegrees(degrees);
273
- _forceRenderNextFrame = YES;
177
+ _target.requestForceRender();
274
178
  }
275
179
 
276
180
  - (void)setMaximumScreenSpaceError:(double)v {
277
- _maxSSE = v;
278
- if (!_initialized) return;
279
- _engine->updateConfig([self makeEngineConfig]);
280
- _forceRenderNextFrame = YES;
181
+ _config.maximumScreenSpaceError = v;
182
+ if (_initialized) [self applyEngineConfig];
281
183
  }
282
- - (void)setMaximumSimultaneousTileLoads:(int)v {
283
- _maxSimLoads = static_cast<double>(v);
284
- if (!_initialized) return;
285
- _engine->updateConfig([self makeEngineConfig]);
286
- _forceRenderNextFrame = YES;
184
+ - (void)setMaximumSimultaneousTileLoads:(int32_t)v {
185
+ _config.maximumSimultaneousTileLoads = v;
186
+ if (_initialized) [self applyEngineConfig];
287
187
  }
288
- - (void)setLoadingDescendantLimit:(int)v {
289
- _loadDescLim = static_cast<double>(v);
290
- if (!_initialized) return;
291
- _engine->updateConfig([self makeEngineConfig]);
292
- _forceRenderNextFrame = YES;
188
+ - (void)setLoadingDescendantLimit:(int32_t)v {
189
+ _config.loadingDescendantLimit = v;
190
+ if (_initialized) [self applyEngineConfig];
191
+ }
192
+ - (void)setMaximumCachedMiB:(int32_t)v {
193
+ _config.maximumCachedBytes =
194
+ static_cast<int64_t>(std::max<int32_t>(16, v)) * 1024LL * 1024LL;
195
+ if (_initialized) [self applyEngineConfig];
196
+ }
197
+ - (void)setPreloadAncestors:(BOOL)v {
198
+ _config.preloadAncestors = (v == YES);
199
+ if (_initialized) [self applyEngineConfig];
200
+ }
201
+ - (void)setPreloadSiblings:(BOOL)v {
202
+ _config.preloadSiblings = (v == YES);
203
+ if (_initialized) [self applyEngineConfig];
204
+ }
205
+ - (void)setForbidHoles:(BOOL)v {
206
+ _config.forbidHoles = (v == YES);
207
+ if (_initialized) [self applyEngineConfig];
208
+ }
209
+ - (void)setEnableWaterMask:(BOOL)v {
210
+ _config.enableWaterMask = (v == YES);
211
+ if (_initialized) [self applyEngineConfig];
212
+ }
213
+ - (void)setEnableFogCulling:(BOOL)v {
214
+ _config.enableFogCulling = (v == YES);
215
+ if (_initialized) [self applyEngineConfig];
216
+ }
217
+ - (void)setEnforceCulledScreenSpaceError:(BOOL)v {
218
+ _config.enforceCulledScreenSpaceError = (v == YES);
219
+ if (_initialized) [self applyEngineConfig];
220
+ }
221
+ - (void)setCulledScreenSpaceError:(double)v {
222
+ _config.culledScreenSpaceError = v;
223
+ if (_initialized) [self applyEngineConfig];
224
+ }
225
+ - (void)setEnableLodTransitionPeriod:(BOOL)v {
226
+ _config.enableLodTransitionPeriod = (v == YES);
227
+ if (_initialized) [self applyEngineConfig];
228
+ }
229
+ - (void)setLodTransitionLength:(double)v {
230
+ _config.lodTransitionLength = v;
231
+ if (_initialized) [self applyEngineConfig];
232
+ }
233
+ - (void)setSqliteCacheMaxRows:(int32_t)v {
234
+ // Cache rows are taken into account on the next initialize().
235
+ _config.sqliteCacheMaxRows = std::max<int32_t>(64, v);
236
+ }
237
+ - (void)setTaskProcessorThreads:(int32_t)v {
238
+ _config.taskProcessorThreads = std::max<int32_t>(0, v);
293
239
  }
294
240
 
295
241
  - (void)setMsaaSampleCount:(int)samples {
296
242
  if (_metalBackend) _metalBackend->setMsaaSampleCount(samples);
297
- _forceRenderNextFrame = YES;
243
+ _target.requestForceRender();
298
244
  }
299
245
 
300
- - (void)markNeedsRender {
301
- _forceRenderNextFrame = YES;
302
- }
246
+ - (void)markNeedsRender { _target.requestForceRender(); }
303
247
 
304
248
  - (BOOL)shouldRenderNextFrame {
305
- if (!_initialized || _suspended || !_engine) {
306
- return NO;
249
+ if (!_initialized || _suspended || !_engine) return NO;
250
+ if (_target.consumeForceRender()) {
251
+ _target.requestForceRender(); // sticky for one frame so we make it through
252
+ return YES;
307
253
  }
308
-
309
- const auto cur = _engine->camera().getParams();
310
- const bool cameraDirty =
311
- std::abs(_camTarget.latitude - cur.latitude) > kEpsLatLon ||
312
- std::abs(_camTarget.longitude - cur.longitude) > kEpsLatLon ||
313
- std::abs(_camTarget.altitude - cur.altitude) > kEpsAlt ||
314
- angleDeltaAbsDeg(cur.heading, _camTarget.heading) > kEpsAngleDeg ||
315
- angleDeltaAbsDeg(cur.pitch, _camTarget.pitch) > kEpsAngleDeg ||
316
- angleDeltaAbsDeg(cur.roll, _camTarget.roll) > kEpsAngleDeg ||
317
- (1.0 - quatDotAbs(cur.viewCorrection, _camTarget.viewCorrection)) > kEpsQuatDot;
318
-
319
- return _forceRenderNextFrame ||
320
- _frameResult.tilesLoading > 0 ||
321
- !_frameResult.tilesetActive ||
322
- cameraDirty;
254
+ if (_frameResult.tilesLoading > 0 || !_frameResult.tilesetActive) return YES;
255
+ if (_target.isDirty()) {
256
+ const auto cur = _engine->camera().getParams();
257
+ const auto tgt = _target.snapshot();
258
+ if (reactnativecesium::CameraTargetState::deltaExceedsEpsilon(cur, tgt)) {
259
+ return YES;
260
+ }
261
+ _target.clearDirty();
262
+ }
263
+ return NO;
323
264
  }
324
265
 
325
266
  - (void)renderFrameWithDt:(double)dt {
326
267
  if (!_initialized || _suspended) return;
327
268
 
328
269
  @autoreleasepool {
329
- auto cur = _engine->camera().getParams();
330
-
331
- // ── Smooth follower: track _camTarget per DOF ────────────────────────────
332
- // lat/lon: direct copy — pan gestures must feel instant.
333
- cur.latitude = _camTarget.latitude;
334
- cur.longitude = _camTarget.longitude;
335
- // alt/hdg/roll/pitch: exponential decay toward target.
336
- const double aAlt = 1.0 - std::exp(-kSmoothAlt * dt);
337
- const double aHdg = 1.0 - std::exp(-kSmoothHdg * dt);
338
- const double aPitch = 1.0 - std::exp(-kSmoothPitch * dt);
339
- const double aRoll = 1.0 - std::exp(-kSmoothRoll * dt);
340
- const double aViewQ = 1.0 - std::exp(-kSmoothViewCorr * dt);
341
- cur.altitude = cur.altitude + aAlt * (_camTarget.altitude - cur.altitude);
342
- cur.heading = lerpAngleDeg(cur.heading, _camTarget.heading, aHdg);
343
- cur.pitch = lerpAngleDeg(cur.pitch, _camTarget.pitch, aPitch);
344
- cur.roll = lerpAngleDeg(cur.roll, _camTarget.roll, aRoll);
345
-
346
- glm::dquat cq = glm::normalize(cur.viewCorrection);
347
- glm::dquat tq = glm::normalize(_camTarget.viewCorrection);
348
- if (glm::dot(cq, tq) < 0.0)
349
- tq = -tq;
350
- cur.viewCorrection = glm::normalize(glm::slerp(cq, tq, aViewQ));
351
-
352
- if (std::abs(_camTarget.altitude - cur.altitude) <= kEpsAlt) {
353
- cur.altitude = _camTarget.altitude;
354
- }
355
- if (angleDeltaAbsDeg(cur.heading, _camTarget.heading) <= kEpsAngleDeg) {
356
- cur.heading = _camTarget.heading;
357
- }
358
- if (angleDeltaAbsDeg(cur.pitch, _camTarget.pitch) <= kEpsAngleDeg) {
359
- cur.pitch = _camTarget.pitch;
360
- }
361
- if (angleDeltaAbsDeg(cur.roll, _camTarget.roll) <= kEpsAngleDeg) {
362
- cur.roll = _camTarget.roll;
363
- }
364
- if ((1.0 - quatDotAbs(cur.viewCorrection, _camTarget.viewCorrection)) <= kEpsQuatDot) {
365
- cur.viewCorrection = _camTarget.viewCorrection;
366
- }
367
- _engine->camera().setParams(cur);
270
+ (void)_target.consumeForceRender();
368
271
 
369
- _engine->updateFrame(_viewportWidth, _viewportHeight, _frameResult);
272
+ const auto cur = _engine->camera().getParams();
273
+ const auto tgt = _target.snapshot();
274
+ const auto smooth = reactnativecesium::CameraSmoother::step(
275
+ cur, tgt, dt, _target.recentIntervalSec());
276
+ _engine->camera().setParams(smooth);
370
277
 
371
- const double instFps = (dt > 1e-6) ? (1.0 / dt) : 0.0;
372
- _fpsEma = (_fpsEma <= 1e-6) ? instFps : (_fpsEma * 0.85 + instFps * 0.15);
373
-
374
- if (++_metricsTick >= 20) {
375
- _metricsTick = 0;
376
- _metricsFps = _fpsEma;
377
- _metricsTilesRendered = _frameResult.tilesRendered;
378
- _metricsTilesLoading = _frameResult.tilesLoading;
379
- _metricsTilesVisited = _frameResult.tilesVisited;
380
- _metricsIonTokenConfigured = _frameResult.ionTokenConfigured;
381
- _metricsTilesetReady = _frameResult.tilesetActive;
382
- if (!_frameResult.creditHtmlLines.empty()) {
383
- NSMutableString* joined = [NSMutableString string];
384
- for (const auto& html : _frameResult.creditHtmlLines) {
385
- if (joined.length > 0) [joined appendString:@"|"];
386
- [joined appendString:[NSString stringWithUTF8String:html.c_str()]];
387
- }
388
- if (![_lastCreditHtmlJoined isEqualToString:joined]) {
389
- _lastCreditHtmlJoined = [joined copy];
390
- NSMutableString* credits = [NSMutableString string];
391
- for (const auto& html : _frameResult.creditHtmlLines) {
392
- NSString* chunk = stripHtmlToPlain(
393
- [NSString stringWithUTF8String:html.c_str()]);
394
- if (chunk.length == 0) continue;
395
- if (credits.length > 0) [credits appendString:@" · "];
396
- [credits appendString:chunk];
397
- }
398
- _metricsCreditsPlainText = [credits copy];
399
- }
400
- } else {
401
- _lastCreditHtmlJoined = @"";
402
- _metricsCreditsPlainText = @"";
403
- }
278
+ if (!reactnativecesium::CameraTargetState::deltaExceedsEpsilon(smooth, tgt)) {
279
+ _target.clearDirty();
404
280
  }
405
281
 
282
+ _engine->updateFrame(_viewportWidth, _viewportHeight, _frameResult);
283
+ _metrics.tick(dt, _frameResult, !_config.tlsCaBundlePath.empty());
284
+
406
285
  reactnativecesium::FrameParams params;
407
286
  _metalBackend->beginFrame(params);
408
287
  _metalBackend->drawScene(_frameResult);
409
288
  _metalBackend->endFrame();
410
- _forceRenderNextFrame = NO;
411
289
  }
412
290
  }
413
291
 
414
- - (double)metricsFps { return _metricsFps; }
415
- - (NSInteger)metricsTilesRendered { return _metricsTilesRendered; }
416
- - (NSInteger)metricsTilesLoading { return _metricsTilesLoading; }
417
- - (NSInteger)metricsTilesVisited { return _metricsTilesVisited; }
418
- - (BOOL)metricsIonTokenConfigured { return _metricsIonTokenConfigured; }
419
- - (BOOL)metricsTilesetReady { return _metricsTilesetReady; }
420
- - (NSString*)metricsCreditsPlainText { return _metricsCreditsPlainText ?: @""; }
292
+ - (double)metricsFps { return _metrics.latest().fps; }
293
+ - (NSInteger)metricsTilesRendered { return _metrics.latest().tilesRendered; }
294
+ - (NSInteger)metricsTilesLoading { return _metrics.latest().tilesLoading; }
295
+ - (NSInteger)metricsTilesVisited { return _metrics.latest().tilesVisited; }
296
+ - (BOOL)metricsIonTokenConfigured { return _metrics.latest().ionTokenConfigured; }
297
+ - (BOOL)metricsTilesetReady { return _metrics.latest().tilesetReady; }
298
+ - (BOOL)metricsTlsConfigured { return _metrics.latest().tlsConfigured; }
299
+ - (NSString*)metricsCreditsPlainText {
300
+ return [NSString stringWithUTF8String:_metrics.latest().creditsPlainText.c_str()];
301
+ }
421
302
 
422
303
  - (double)readCameraLatitude { return _engine ? _engine->camera().getParams().latitude : 0.0; }
423
304
  - (double)readCameraLongitude { return _engine ? _engine->camera().getParams().longitude : 0.0; }
@@ -432,16 +313,13 @@ static double quatDotAbs(const glm::dquat& a, const glm::dquat& b) {
432
313
  return _engine->camera().getParams().viewCorrection.w;
433
314
  }
434
315
  - (double)readViewCorrectionX {
435
- if (!_engine) return 0.0;
436
- return _engine->camera().getParams().viewCorrection.x;
316
+ return _engine ? _engine->camera().getParams().viewCorrection.x : 0.0;
437
317
  }
438
318
  - (double)readViewCorrectionY {
439
- if (!_engine) return 0.0;
440
- return _engine->camera().getParams().viewCorrection.y;
319
+ return _engine ? _engine->camera().getParams().viewCorrection.y : 0.0;
441
320
  }
442
321
  - (double)readViewCorrectionZ {
443
- if (!_engine) return 0.0;
444
- return _engine->camera().getParams().viewCorrection.z;
322
+ return _engine ? _engine->camera().getParams().viewCorrection.z : 0.0;
445
323
  }
446
324
 
447
325
  - (void)shutdown {