react-native-cesium 1.0.1

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +12 -0
  3. package/README.md +308 -0
  4. package/ReactNativeCesium.podspec +54 -0
  5. package/android/CMakeLists.txt +146 -0
  6. package/android/build.gradle +178 -0
  7. package/android/fix-prefab.gradle +51 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/src/main/AndroidManifest.xml +5 -0
  10. package/android/src/main/assets/cacert.pem +3529 -0
  11. package/android/src/main/cpp/CesiumBridgeAndroid.cpp +487 -0
  12. package/android/src/main/cpp/CesiumBridgeAndroid.h +94 -0
  13. package/android/src/main/cpp/cpp-adapter.cpp +17 -0
  14. package/android/src/main/cpp/vulkan/VulkanBackend.cpp +1781 -0
  15. package/android/src/main/cpp/vulkan/VulkanBackend.h +190 -0
  16. package/android/src/main/cpp/vulkan/shaders/sky.frag +86 -0
  17. package/android/src/main/cpp/vulkan/shaders/sky.vert +9 -0
  18. package/android/src/main/cpp/vulkan/shaders/terrain.frag +144 -0
  19. package/android/src/main/cpp/vulkan/shaders/terrain.vert +25 -0
  20. package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +84 -0
  21. package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +225 -0
  22. package/android/src/main/java/com/reactnativecesium/ReactNativeCesiumPackage.kt +28 -0
  23. package/cpp/engine/CesiumEngine.cpp +413 -0
  24. package/cpp/engine/CesiumEngine.hpp +90 -0
  25. package/cpp/engine/DeletionQueue.cpp +28 -0
  26. package/cpp/engine/DeletionQueue.hpp +28 -0
  27. package/cpp/engine/GlobeCamera.cpp +181 -0
  28. package/cpp/engine/GlobeCamera.hpp +64 -0
  29. package/cpp/engine/GltfToMesh.cpp +154 -0
  30. package/cpp/engine/GltfToMesh.hpp +20 -0
  31. package/cpp/engine/ResourcePreparer.cpp +224 -0
  32. package/cpp/engine/ResourcePreparer.hpp +96 -0
  33. package/cpp/engine/TaskProcessor.cpp +61 -0
  34. package/cpp/engine/TaskProcessor.hpp +43 -0
  35. package/cpp/engine/TileLifecycleManager.cpp +30 -0
  36. package/cpp/engine/TileLifecycleManager.hpp +25 -0
  37. package/cpp/fmt/format.h +9 -0
  38. package/cpp/renderer/IGPUBackend.hpp +22 -0
  39. package/cpp/renderer/RenderTypes.hpp +112 -0
  40. package/cpp/renderer/ShaderTypes.hpp +29 -0
  41. package/example/README.md +43 -0
  42. package/example/react-native-cesium-example.jpg +0 -0
  43. package/ios/CesiumBridge.h +55 -0
  44. package/ios/CesiumBridge.mm +393 -0
  45. package/ios/HybridCesiumView.swift +299 -0
  46. package/ios/ReactNativeCesium-umbrella.h +16 -0
  47. package/ios/metal/MetalBackend.h +82 -0
  48. package/ios/metal/MetalBackend.mm +839 -0
  49. package/ios/react_native_cesium_ensure_native.rb +12 -0
  50. package/ios/react_native_cesium_post_install.rb +30 -0
  51. package/lib/commonjs/CesiumView.js +13 -0
  52. package/lib/commonjs/CesiumView.js.map +1 -0
  53. package/lib/commonjs/index.js +13 -0
  54. package/lib/commonjs/index.js.map +1 -0
  55. package/lib/commonjs/package.json +1 -0
  56. package/lib/commonjs/specs/cesium-view.nitro.js +6 -0
  57. package/lib/commonjs/specs/cesium-view.nitro.js.map +1 -0
  58. package/lib/module/CesiumView.js +9 -0
  59. package/lib/module/CesiumView.js.map +1 -0
  60. package/lib/module/index.js +4 -0
  61. package/lib/module/index.js.map +1 -0
  62. package/lib/module/specs/cesium-view.nitro.js +4 -0
  63. package/lib/module/specs/cesium-view.nitro.js.map +1 -0
  64. package/lib/typescript/src/CesiumView.d.ts +5 -0
  65. package/lib/typescript/src/CesiumView.d.ts.map +1 -0
  66. package/lib/typescript/src/index.d.ts +3 -0
  67. package/lib/typescript/src/index.d.ts.map +1 -0
  68. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +43 -0
  69. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -0
  70. package/nitro.json +30 -0
  71. package/nitrogen/generated/.gitattributes +1 -0
  72. package/nitrogen/generated/android/ReactNativeCesium+autolinking.cmake +83 -0
  73. package/nitrogen/generated/android/ReactNativeCesium+autolinking.gradle +27 -0
  74. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +58 -0
  75. package/nitrogen/generated/android/ReactNativeCesiumOnLoad.hpp +34 -0
  76. package/nitrogen/generated/android/c++/JCameraState.hpp +81 -0
  77. package/nitrogen/generated/android/c++/JCesiumMetrics.hpp +81 -0
  78. package/nitrogen/generated/android/c++/JFunc_void_CesiumMetrics.hpp +78 -0
  79. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +178 -0
  80. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +83 -0
  81. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +92 -0
  82. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.hpp +49 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CameraState.kt +56 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/CesiumMetrics.kt +56 -0
  85. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CesiumMetrics.kt +80 -0
  86. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +126 -0
  87. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/ReactNativeCesiumOnLoad.kt +35 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewManager.kt +80 -0
  89. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/views/HybridCesiumViewStateUpdater.kt +23 -0
  90. package/nitrogen/generated/ios/ReactNativeCesium+autolinking.rb +60 -0
  91. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.cpp +57 -0
  92. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +164 -0
  93. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Umbrella.hpp +53 -0
  94. package/nitrogen/generated/ios/ReactNativeCesiumAutolinking.mm +33 -0
  95. package/nitrogen/generated/ios/ReactNativeCesiumAutolinking.swift +26 -0
  96. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.cpp +11 -0
  97. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +157 -0
  98. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +167 -0
  99. package/nitrogen/generated/ios/swift/CameraState.swift +59 -0
  100. package/nitrogen/generated/ios/swift/CesiumMetrics.swift +59 -0
  101. package/nitrogen/generated/ios/swift/Func_void_CameraState.swift +46 -0
  102. package/nitrogen/generated/ios/swift/Func_void_CesiumMetrics.swift +46 -0
  103. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  104. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +65 -0
  105. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +306 -0
  106. package/nitrogen/generated/shared/c++/CameraState.hpp +107 -0
  107. package/nitrogen/generated/shared/c++/CesiumMetrics.hpp +107 -0
  108. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +42 -0
  109. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +90 -0
  110. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +182 -0
  111. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +122 -0
  112. package/nitrogen/generated/shared/json/CesiumViewConfig.json +19 -0
  113. package/package.json +140 -0
  114. package/react-native.config.js +16 -0
  115. package/scripts/cesium/build.mjs +901 -0
  116. package/scripts/cesium/clean.mjs +35 -0
  117. package/scripts/cesium/clone.mjs +32 -0
  118. package/scripts/cesium/ensure-native.mjs +116 -0
  119. package/scripts/cesium/vcpkg-ports/zstd/fix-emscripten-and-clang-cl.patch +31 -0
  120. package/scripts/cesium/vcpkg-ports/zstd/fix-windows-rc-compile.patch +13 -0
  121. package/scripts/cesium/vcpkg-ports/zstd/no-static-suffix.patch +13 -0
  122. package/scripts/cesium/vcpkg-ports/zstd/portfile.cmake +77 -0
  123. package/scripts/cesium/vcpkg-ports/zstd/usage +4 -0
  124. package/scripts/cesium/vcpkg-ports/zstd/vcpkg.json +23 -0
  125. package/scripts/cesium/vcpkg-triplets/arm64-ios-simulator.cmake +10 -0
  126. package/scripts/cesium/vcpkg-triplets/arm64-ios.cmake +14 -0
  127. package/src/CesiumView.tsx +13 -0
  128. package/src/index.ts +7 -0
  129. package/src/specs/cesium-view.nitro.ts +51 -0
@@ -0,0 +1,43 @@
1
+ # Example App
2
+
3
+ This app is the reference integration for `react-native-cesium`. For the current public API, installation notes, and platform support status, start with the root [`README.md`](../README.md).
4
+
5
+ ## What it demonstrates
6
+
7
+ - rendering `CesiumView` inside a React Native screen
8
+ - wiring a Nitro `hybridRef`
9
+ - driving the camera with `setCamera(...)`
10
+ - receiving runtime telemetry through `onMetrics`
11
+ - showing Cesium attribution text from `creditsPlainText`
12
+
13
+ ## Before you run it
14
+
15
+ 1. The example **`ios/Podfile`** includes **`react_native_cesium_ensure_native`** in **`pre_install`**, so the first **`pod install`** can clone and build Cesium Native automatically (long first run; see root README). You can still run **`yarn update`** and **`CESIUM_BUILD_ONLY=ios yarn build`** from the repo root first if you prefer. After changing `.env`, restart Metro so the Babel env transform re-reads the file.
16
+
17
+ 2. Copy `example/.env_example` to `example/.env` and set `CESIUM_ION_ACCESS_TOKEN` to your own Cesium Ion access token.
18
+
19
+ ```bash
20
+ cp .env_example .env
21
+ ```
22
+
23
+ 3. Install example dependencies and CocoaPods:
24
+
25
+ ```bash
26
+ cd example
27
+ yarn install
28
+ cd ios && pod install && cd ..
29
+ ```
30
+
31
+ ## Run the example
32
+
33
+ ```bash
34
+ yarn start
35
+ ```
36
+
37
+ In a second terminal:
38
+
39
+ ```bash
40
+ yarn ios
41
+ ```
42
+
43
+ The example links the library locally using `link:..`, so changes in the package are reflected directly while developing.
@@ -0,0 +1,55 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <MetalKit/MetalKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface CesiumBridge : NSObject
7
+
8
+ - (instancetype)initWithMetalLayer:(CAMetalLayer *)layer
9
+ width:(int)width
10
+ height:(int)height
11
+ cacheDir:(NSString *)cacheDir;
12
+
13
+ - (void)updateIonAccessToken:(NSString *)token assetId:(int64_t)assetId;
14
+ - (void)updateImageryAssetId:(int64_t)assetId;
15
+ - (void)updateCameraLatitude:(double)lat
16
+ longitude:(double)lon
17
+ altitude:(double)alt
18
+ heading:(double)heading
19
+ pitch:(double)pitch
20
+ roll:(double)roll;
21
+ - (void)resize:(int)width height:(int)height;
22
+ - (BOOL)shouldRenderNextFrame;
23
+ - (void)markNeedsRender;
24
+ - (void)renderFrameWithDt:(double)dt;
25
+ - (void)shutdown;
26
+
27
+ // Camera / globe
28
+ - (void)setVerticalFovDeg:(double)degrees;
29
+
30
+ // Tileset tuning (triggers rebuild when changed)
31
+ - (void)setMaximumScreenSpaceError:(double)v;
32
+ - (void)setMaximumSimultaneousTileLoads:(int)v;
33
+ - (void)setLoadingDescendantLimit:(int)v;
34
+
35
+ - (void)setMsaaSampleCount:(int)samples;
36
+
37
+ /// Throttled metrics consumed by the Swift hybrid view.
38
+ @property (nonatomic, readonly) double metricsFps;
39
+ @property (nonatomic, readonly) NSInteger metricsTilesRendered;
40
+ @property (nonatomic, readonly) NSInteger metricsTilesLoading;
41
+ @property (nonatomic, readonly) NSInteger metricsTilesVisited;
42
+ @property (nonatomic, readonly) BOOL metricsIonTokenConfigured;
43
+ @property (nonatomic, readonly) BOOL metricsTilesetReady;
44
+ @property (nonatomic, readonly) NSString *metricsCreditsPlainText;
45
+ - (double)readCameraLatitude;
46
+ - (double)readCameraLongitude;
47
+ - (double)readCameraAltitude;
48
+ - (double)readCameraHeading;
49
+ - (double)readCameraPitch;
50
+ - (double)readCameraRoll;
51
+ - (double)readVerticalFovDeg;
52
+
53
+ @end
54
+
55
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,393 @@
1
+ #import "CesiumBridge.h"
2
+ #import <CoreFoundation/CoreFoundation.h>
3
+
4
+ #include "engine/CesiumEngine.hpp"
5
+ #include "metal/MetalBackend.h"
6
+
7
+ #include <atomic>
8
+ #include <cmath>
9
+ #include <memory>
10
+ #include <string>
11
+
12
+ namespace {
13
+
14
+ // Strips HTML tags and collapses whitespace to produce plain-text attribution.
15
+ static NSString* stripHtmlToPlain(NSString* html) {
16
+ if (html.length == 0) return @"";
17
+ NSError* err = nil;
18
+ NSRegularExpression* tagRx =
19
+ [NSRegularExpression regularExpressionWithPattern:@"<[^>]+>" options:0 error:&err];
20
+ NSString* plain = [tagRx stringByReplacingMatchesInString:html
21
+ options:0
22
+ range:NSMakeRange(0, html.length)
23
+ withTemplate:@" "];
24
+ NSRegularExpression* wsRx =
25
+ [NSRegularExpression regularExpressionWithPattern:@"\\s+" options:0 error:&err];
26
+ plain = [wsRx stringByReplacingMatchesInString:plain
27
+ options:0
28
+ range:NSMakeRange(0, plain.length)
29
+ withTemplate:@" "];
30
+ return [plain stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
31
+ }
32
+
33
+ double lerpAngleDeg(double a, double b, double t) {
34
+ double diff = std::fmod(b - a + 540.0, 360.0) - 180.0;
35
+ return a + diff * t;
36
+ }
37
+
38
+ double angleDeltaAbsDeg(double a, double b) {
39
+ return std::abs(std::fmod(b - a + 540.0, 360.0) - 180.0);
40
+ }
41
+
42
+ } // namespace
43
+
44
+ // ── Per-DOF smoothing. ────────────────────────────────────────────────────────
45
+ static const double kSmoothAlt = 25.0;
46
+ static const double kSmoothHdg = 30.0;
47
+ static const double kSmoothRoll = 50.0;
48
+ static const double kSmoothPitch = 50.0;
49
+ static const double kEpsLatLon = 1e-7;
50
+ static const double kEpsAlt = 0.1;
51
+ static const double kEpsAngleDeg = 0.05;
52
+
53
+ @implementation CesiumBridge {
54
+ std::unique_ptr<reactnativecesium::MetalBackend> _metalBackend;
55
+ std::unique_ptr<reactnativecesium::CesiumEngine> _engine;
56
+ reactnativecesium::FrameResult _frameResult;
57
+ int _viewportWidth;
58
+ int _viewportHeight;
59
+ BOOL _initialized;
60
+
61
+ NSString* _cacheDir;
62
+ NSString* _ionAccessToken;
63
+ int64_t _ionTilesetAssetId;
64
+
65
+ double _maxSSE;
66
+ double _maxSimLoads;
67
+ double _loadDescLim;
68
+
69
+ // Demand target — incoming props/calls write here; render loop smooths toward it.
70
+ reactnativecesium::CameraParams _camTarget;
71
+ BOOL _forceRenderNextFrame;
72
+
73
+ BOOL _suspended;
74
+
75
+ double _fpsEma;
76
+ int _metricsTick;
77
+
78
+ double _metricsFps;
79
+ NSInteger _metricsTilesRendered;
80
+ NSInteger _metricsTilesLoading;
81
+ NSInteger _metricsTilesVisited;
82
+ BOOL _metricsIonTokenConfigured;
83
+ BOOL _metricsTilesetReady;
84
+ NSString* _metricsCreditsPlainText;
85
+
86
+ NSString* _lastCreditHtmlJoined;
87
+ }
88
+
89
+ - (reactnativecesium::EngineConfig)makeEngineConfig {
90
+ reactnativecesium::EngineConfig config;
91
+ if (_ionAccessToken) {
92
+ config.ionAccessToken = std::string([_ionAccessToken UTF8String]);
93
+ }
94
+ config.ionAssetId = _ionTilesetAssetId;
95
+ config.cacheDatabasePath =
96
+ _cacheDir ? std::string([_cacheDir UTF8String]) + "/cesium_cache.db" : "";
97
+ NSString* caPem = [[NSBundle mainBundle] pathForResource:@"cacert" ofType:@"pem"];
98
+ if (caPem.length > 0) {
99
+ config.tlsCaBundlePath = std::string([caPem fileSystemRepresentation]);
100
+ }
101
+ config.maximumScreenSpaceError = std::max(1.0, _maxSSE);
102
+ config.maximumSimultaneousTileLoads =
103
+ static_cast<int32_t>(std::max(0.0, std::round(_maxSimLoads)));
104
+ config.loadingDescendantLimit =
105
+ static_cast<int32_t>(std::max(1.0, std::round(_loadDescLim)));
106
+ return config;
107
+ }
108
+
109
+ - (instancetype)initWithMetalLayer:(CAMetalLayer *)layer
110
+ width:(int)width
111
+ height:(int)height
112
+ cacheDir:(NSString *)cacheDir {
113
+ self = [super init];
114
+ if (self) {
115
+ _viewportWidth = width;
116
+ _viewportHeight = height;
117
+ _initialized = NO;
118
+ _cacheDir = [cacheDir copy];
119
+ _ionAccessToken = nil;
120
+ _ionTilesetAssetId = 1;
121
+ _maxSSE = 32.0;
122
+ _maxSimLoads = 12.0;
123
+ _loadDescLim = 20.0;
124
+ _forceRenderNextFrame = YES;
125
+ _suspended = NO;
126
+ _fpsEma = 0.0;
127
+ _metricsTick = 0;
128
+ _metricsFps = 0.0;
129
+ _metricsTilesRendered = 0;
130
+ _metricsTilesLoading = 0;
131
+ _metricsTilesVisited = 0;
132
+ _metricsIonTokenConfigured = NO;
133
+ _metricsTilesetReady = NO;
134
+ _metricsCreditsPlainText = @"";
135
+ _lastCreditHtmlJoined = @"";
136
+
137
+ _metalBackend = std::make_unique<reactnativecesium::MetalBackend>();
138
+ _metalBackend->initialize((__bridge void*)layer, width, height);
139
+
140
+ [self buildEngine];
141
+ _initialized = YES;
142
+
143
+ [[NSNotificationCenter defaultCenter]
144
+ addObserver:self selector:@selector(appWillResignActive:)
145
+ name:UIApplicationWillResignActiveNotification object:nil];
146
+ [[NSNotificationCenter defaultCenter]
147
+ addObserver:self selector:@selector(appDidBecomeActive:)
148
+ name:UIApplicationDidBecomeActiveNotification object:nil];
149
+ }
150
+ return self;
151
+ }
152
+
153
+ - (void)appWillResignActive:(NSNotification *)note { _suspended = YES; }
154
+ - (void)appDidBecomeActive:(NSNotification *)note {
155
+ _suspended = NO;
156
+ _forceRenderNextFrame = YES;
157
+ }
158
+
159
+ - (void)buildEngine {
160
+ reactnativecesium::EngineConfig cfg = [self makeEngineConfig];
161
+ static std::atomic<int> caWarned{0};
162
+ if (cfg.tlsCaBundlePath.empty() && caWarned.fetch_add(1) == 0) {
163
+ NSLog(@"[ReactNativeCesium] cacert.pem not in main bundle — libcurl may fail TLS to "
164
+ @"api.cesium.com on device. Ensure the pod includes ios/cacert.pem (run pod install).");
165
+ }
166
+ _engine.reset();
167
+ _engine = std::make_unique<reactnativecesium::CesiumEngine>();
168
+ _engine->initialize(cfg);
169
+
170
+ // Sync demand target with engine's initial camera defaults.
171
+ _camTarget = _engine->camera().getParams();
172
+
173
+ auto* backendPtr = _metalBackend.get();
174
+ _engine->getResourcePreparer()->setGPUTextureCreator(
175
+ [backendPtr](const uint8_t* pixels, int32_t w, int32_t h) -> void* {
176
+ return backendPtr->createRasterTexture(pixels, w, h);
177
+ });
178
+ _engine->getResourcePreparer()->setGPUTextureDeleter(
179
+ [](void* tex) {
180
+ if (tex) CFRelease(tex);
181
+ });
182
+ // Water mask textures are identical MTLTexture objects — createRasterTexture
183
+ // is reused. On Metal, the binding slot (texture index 1) is set at draw
184
+ // time so no separate factory is needed.
185
+ NSLog(@"[WM-BRIDGE] Wiring up water mask texture creator/deleter");
186
+ _engine->getResourcePreparer()->setWaterMaskTextureCreator(
187
+ [backendPtr](const uint8_t* pixels, int32_t w, int32_t h) -> void* {
188
+ NSLog(@"[WM-BRIDGE] waterMaskCreator called w=%d h=%d pixels=%p", w, h, pixels);
189
+ void* tex = backendPtr->createRasterTexture(pixels, w, h);
190
+ NSLog(@"[WM-BRIDGE] waterMaskCreator => tex=%p", tex);
191
+ return tex;
192
+ });
193
+ _engine->getResourcePreparer()->setWaterMaskTextureDeleter(
194
+ [](void* tex) {
195
+ NSLog(@"[WM-BRIDGE] waterMaskDeleter called tex=%p", tex);
196
+ if (tex) CFRelease(tex);
197
+ });
198
+ }
199
+
200
+ - (void)updateIonAccessToken:(NSString *)token assetId:(int64_t)assetId {
201
+ if (!_initialized) return;
202
+ _ionAccessToken = [token copy];
203
+ _ionTilesetAssetId = assetId;
204
+ _engine->updateConfig([self makeEngineConfig]);
205
+ _forceRenderNextFrame = YES;
206
+ }
207
+
208
+ - (void)updateImageryAssetId:(int64_t)assetId {
209
+ if (!_initialized) return;
210
+ _engine->setImageryAssetId(assetId);
211
+ _forceRenderNextFrame = YES;
212
+ }
213
+
214
+ - (void)updateCameraLatitude:(double)lat
215
+ longitude:(double)lon
216
+ altitude:(double)alt
217
+ heading:(double)heading
218
+ pitch:(double)pitch
219
+ roll:(double)roll {
220
+ if (!_initialized) return;
221
+ _camTarget.latitude = lat;
222
+ _camTarget.longitude = lon;
223
+ _camTarget.altitude = alt;
224
+ _camTarget.heading = heading;
225
+ _camTarget.pitch = pitch;
226
+ _camTarget.roll = roll;
227
+ _forceRenderNextFrame = YES;
228
+ }
229
+
230
+ - (void)resize:(int)width height:(int)height {
231
+ _viewportWidth = width;
232
+ _viewportHeight = height;
233
+ if (_metalBackend) _metalBackend->resize(width, height);
234
+ _forceRenderNextFrame = YES;
235
+ }
236
+
237
+ - (void)setVerticalFovDeg:(double)degrees {
238
+ if (!_initialized) return;
239
+ _engine->camera().setVerticalFovDegrees(degrees);
240
+ _forceRenderNextFrame = YES;
241
+ }
242
+
243
+ - (void)setMaximumScreenSpaceError:(double)v {
244
+ _maxSSE = v;
245
+ if (!_initialized) return;
246
+ _engine->updateConfig([self makeEngineConfig]);
247
+ _forceRenderNextFrame = YES;
248
+ }
249
+ - (void)setMaximumSimultaneousTileLoads:(int)v {
250
+ _maxSimLoads = static_cast<double>(v);
251
+ if (!_initialized) return;
252
+ _engine->updateConfig([self makeEngineConfig]);
253
+ _forceRenderNextFrame = YES;
254
+ }
255
+ - (void)setLoadingDescendantLimit:(int)v {
256
+ _loadDescLim = static_cast<double>(v);
257
+ if (!_initialized) return;
258
+ _engine->updateConfig([self makeEngineConfig]);
259
+ _forceRenderNextFrame = YES;
260
+ }
261
+
262
+ - (void)setMsaaSampleCount:(int)samples {
263
+ if (_metalBackend) _metalBackend->setMsaaSampleCount(samples);
264
+ _forceRenderNextFrame = YES;
265
+ }
266
+
267
+ - (void)markNeedsRender {
268
+ _forceRenderNextFrame = YES;
269
+ }
270
+
271
+ - (BOOL)shouldRenderNextFrame {
272
+ if (!_initialized || _suspended || !_engine) {
273
+ return NO;
274
+ }
275
+
276
+ const auto cur = _engine->camera().getParams();
277
+ const bool cameraDirty =
278
+ std::abs(_camTarget.latitude - cur.latitude) > kEpsLatLon ||
279
+ std::abs(_camTarget.longitude - cur.longitude) > kEpsLatLon ||
280
+ std::abs(_camTarget.altitude - cur.altitude) > kEpsAlt ||
281
+ angleDeltaAbsDeg(cur.heading, _camTarget.heading) > kEpsAngleDeg ||
282
+ angleDeltaAbsDeg(cur.pitch, _camTarget.pitch) > kEpsAngleDeg ||
283
+ angleDeltaAbsDeg(cur.roll, _camTarget.roll) > kEpsAngleDeg;
284
+
285
+ return _forceRenderNextFrame ||
286
+ _frameResult.tilesLoading > 0 ||
287
+ !_frameResult.tilesetActive ||
288
+ cameraDirty;
289
+ }
290
+
291
+ - (void)renderFrameWithDt:(double)dt {
292
+ if (!_initialized || _suspended) return;
293
+
294
+ @autoreleasepool {
295
+ auto cur = _engine->camera().getParams();
296
+
297
+ // ── Smooth follower: track _camTarget per DOF ────────────────────────────
298
+ // lat/lon: direct copy — pan gestures must feel instant.
299
+ cur.latitude = _camTarget.latitude;
300
+ cur.longitude = _camTarget.longitude;
301
+ // alt/hdg/roll/pitch: exponential decay toward target.
302
+ const double aAlt = 1.0 - std::exp(-kSmoothAlt * dt);
303
+ const double aHdg = 1.0 - std::exp(-kSmoothHdg * dt);
304
+ const double aPitch = 1.0 - std::exp(-kSmoothPitch * dt);
305
+ const double aRoll = 1.0 - std::exp(-kSmoothRoll * dt);
306
+ cur.altitude = cur.altitude + aAlt * (_camTarget.altitude - cur.altitude);
307
+ cur.heading = lerpAngleDeg(cur.heading, _camTarget.heading, aHdg);
308
+ cur.pitch = lerpAngleDeg(cur.pitch, _camTarget.pitch, aPitch);
309
+ cur.roll = lerpAngleDeg(cur.roll, _camTarget.roll, aRoll);
310
+
311
+ if (std::abs(_camTarget.altitude - cur.altitude) <= kEpsAlt) {
312
+ cur.altitude = _camTarget.altitude;
313
+ }
314
+ if (angleDeltaAbsDeg(cur.heading, _camTarget.heading) <= kEpsAngleDeg) {
315
+ cur.heading = _camTarget.heading;
316
+ }
317
+ if (angleDeltaAbsDeg(cur.pitch, _camTarget.pitch) <= kEpsAngleDeg) {
318
+ cur.pitch = _camTarget.pitch;
319
+ }
320
+ if (angleDeltaAbsDeg(cur.roll, _camTarget.roll) <= kEpsAngleDeg) {
321
+ cur.roll = _camTarget.roll;
322
+ }
323
+ _engine->camera().setParams(cur);
324
+
325
+ _engine->updateFrame(_viewportWidth, _viewportHeight, _frameResult);
326
+
327
+ const double instFps = (dt > 1e-6) ? (1.0 / dt) : 0.0;
328
+ _fpsEma = (_fpsEma <= 1e-6) ? instFps : (_fpsEma * 0.85 + instFps * 0.15);
329
+
330
+ if (++_metricsTick >= 20) {
331
+ _metricsTick = 0;
332
+ _metricsFps = _fpsEma;
333
+ _metricsTilesRendered = _frameResult.tilesRendered;
334
+ _metricsTilesLoading = _frameResult.tilesLoading;
335
+ _metricsTilesVisited = _frameResult.tilesVisited;
336
+ _metricsIonTokenConfigured = _frameResult.ionTokenConfigured;
337
+ _metricsTilesetReady = _frameResult.tilesetActive;
338
+ if (!_frameResult.creditHtmlLines.empty()) {
339
+ NSMutableString* joined = [NSMutableString string];
340
+ for (const auto& html : _frameResult.creditHtmlLines) {
341
+ if (joined.length > 0) [joined appendString:@"|"];
342
+ [joined appendString:[NSString stringWithUTF8String:html.c_str()]];
343
+ }
344
+ if (![_lastCreditHtmlJoined isEqualToString:joined]) {
345
+ _lastCreditHtmlJoined = [joined copy];
346
+ NSMutableString* credits = [NSMutableString string];
347
+ for (const auto& html : _frameResult.creditHtmlLines) {
348
+ NSString* chunk = stripHtmlToPlain(
349
+ [NSString stringWithUTF8String:html.c_str()]);
350
+ if (chunk.length == 0) continue;
351
+ if (credits.length > 0) [credits appendString:@" · "];
352
+ [credits appendString:chunk];
353
+ }
354
+ _metricsCreditsPlainText = [credits copy];
355
+ }
356
+ } else {
357
+ _lastCreditHtmlJoined = @"";
358
+ _metricsCreditsPlainText = @"";
359
+ }
360
+ }
361
+
362
+ reactnativecesium::FrameParams params;
363
+ _metalBackend->beginFrame(params);
364
+ _metalBackend->drawScene(_frameResult);
365
+ _metalBackend->endFrame();
366
+ _forceRenderNextFrame = NO;
367
+ }
368
+ }
369
+
370
+ - (double)metricsFps { return _metricsFps; }
371
+ - (NSInteger)metricsTilesRendered { return _metricsTilesRendered; }
372
+ - (NSInteger)metricsTilesLoading { return _metricsTilesLoading; }
373
+ - (NSInteger)metricsTilesVisited { return _metricsTilesVisited; }
374
+ - (BOOL)metricsIonTokenConfigured { return _metricsIonTokenConfigured; }
375
+ - (BOOL)metricsTilesetReady { return _metricsTilesetReady; }
376
+ - (NSString*)metricsCreditsPlainText { return _metricsCreditsPlainText ?: @""; }
377
+
378
+ - (double)readCameraLatitude { return _engine ? _engine->camera().getParams().latitude : 0.0; }
379
+ - (double)readCameraLongitude { return _engine ? _engine->camera().getParams().longitude : 0.0; }
380
+ - (double)readCameraAltitude { return _engine ? _engine->camera().getParams().altitude : 0.0; }
381
+ - (double)readCameraHeading { return _engine ? _engine->camera().getParams().heading : 0.0; }
382
+ - (double)readCameraPitch { return _engine ? _engine->camera().getParams().pitch : 0.0; }
383
+ - (double)readCameraRoll { return _engine ? _engine->camera().getParams().roll : 0.0; }
384
+ - (double)readVerticalFovDeg { return _engine ? _engine->camera().getVerticalFovDegrees() : 60.0; }
385
+
386
+ - (void)shutdown {
387
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
388
+ if (_engine) _engine->shutdown();
389
+ if (_metalBackend) _metalBackend->shutdown();
390
+ _initialized = NO;
391
+ }
392
+
393
+ @end