react-native-cesium 1.2.2 → 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 (40) hide show
  1. package/README.md +100 -26
  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/CesiumEngine.cpp +3 -1
  10. package/cpp/engine/EngineTunables.hpp +80 -21
  11. package/cpp/engine/GlobeCamera.cpp +17 -6
  12. package/cpp/engine/GlobeCamera.hpp +11 -8
  13. package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
  14. package/cpp/engine/test/build_and_run.sh +27 -0
  15. package/example/README.md +1 -1
  16. package/ios/CesiumBridge.h +53 -28
  17. package/ios/CesiumBridge.mm +172 -86
  18. package/ios/HybridCesiumView.swift +213 -145
  19. package/lib/typescript/src/specs/cesium-view.nitro.d.ts +77 -13
  20. package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
  21. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +90 -9
  22. package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +19 -3
  23. package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +20 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +57 -3
  25. package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +9 -9
  26. package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +82 -9
  27. package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +25 -0
  28. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +14 -3
  29. package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +210 -16
  30. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +19 -3
  31. package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +20 -4
  32. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +55 -0
  33. package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +5 -0
  34. package/nitrogen/generated/shared/json/CesiumViewConfig.json +5 -0
  35. package/package.json +2 -2
  36. package/src/specs/cesium-view.nitro.ts +93 -14
  37. package/cpp/engine/CameraSmoother.cpp +0 -71
  38. package/cpp/engine/CameraSmoother.hpp +0 -25
  39. package/cpp/engine/CameraTargetState.cpp +0 -140
  40. package/cpp/engine/CameraTargetState.hpp +0 -103
@@ -4,6 +4,7 @@ import android.content.Context
4
4
  import android.os.Handler
5
5
  import android.os.HandlerThread
6
6
  import android.os.Process
7
+ import android.os.SystemClock
7
8
  import android.util.Log
8
9
  import android.view.Choreographer
9
10
  import android.view.SurfaceHolder
@@ -16,34 +17,39 @@ import java.util.concurrent.atomic.AtomicBoolean
16
17
 
17
18
  class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec() {
18
19
  private val surfaceView = SurfaceView(appContext)
20
+
19
21
  /// Bridge is only ever read on the render thread. The UI thread posts
20
- /// requests via `renderHandler`. Marked volatile so init/shutdown observers
21
- /// (`bridge != null`) on the UI thread see the latest reference, but real
22
- /// work is always serialized through the render thread.
22
+ /// requests via `renderHandler`. Marked volatile so observers on the UI
23
+ /// thread (`bridge != null`) see the latest reference; real work is always
24
+ /// serialized through the render thread for config-style writes, and
25
+ /// goes directly into CameraIntegrator (with its own mutex) for camera
26
+ /// demand writes.
23
27
  @Volatile private var bridge: CesiumBridgeJNI? = null
24
28
  private var metricsFrameCounter = 0
25
29
  private var idleProbeAccumulator = 0.0
26
- private var lastFrameTimeNanos = 0L
27
- /// Last-pushed camera state — read/written on the render thread (via
28
- /// `renderHandler.post`), never directly on the UI thread.
29
- private var lastPushedCamera: CameraState? = null
30
- private var lastPushedViewCorrection: Quaternion? = null
31
- /// Camera state requested by `setCamera` before the bridge was ready.
32
- /// Updated on the UI thread, drained on the render thread.
33
- @Volatile private var pendingRuntimeCamera: CameraState? = null
34
- @Volatile private var pendingRuntimeViewCorrection: Quaternion? = null
35
- /// True while a Choreographer frame callback is pending. Mutated on the UI
36
- /// thread inside the callback / scheduleNextFrame.
37
30
  private var renderLoopActive = false
38
- /// Drops Choreographer ticks while the render thread is still finishing the
39
- /// previous frame. Set on UI thread when posting; cleared on render thread
40
- /// after the bridge call completes.
41
31
  private val renderInFlight = AtomicBoolean(false)
42
32
 
43
- /// Dedicated render thread — owns all CesiumEngine + GPU work. Created lazily
44
- /// when the first SurfaceHolder.Callback fires. The Choreographer (UI thread)
45
- /// posts render requests here; bridge config setters likewise post here so
46
- /// they serialize with rendering and never race with engine state.
33
+ /// Set in `surfaceDestroyed` before any teardown work begins. Render-thread
34
+ /// callbacks and choreographer frame ticks check this flag and bail out so
35
+ /// they don't try to emit `onMetrics` into a JS dispatcher that may already
36
+ /// have been torn down by fast refresh / view unmount. Volatile so the
37
+ /// render thread sees the write made on the UI thread.
38
+ @Volatile private var isShutdown = false
39
+
40
+ // Demand values that arrived before the bridge was created. Drained once
41
+ // in surfaceCreated after the seed teleport.
42
+ @Volatile private var pendingTeleport: CameraState? = null
43
+ @Volatile private var pendingLat: Double? = null
44
+ @Volatile private var pendingLon: Double? = null
45
+ @Volatile private var pendingAltitude: Double? = null
46
+ @Volatile private var pendingHeading: Double? = null
47
+ @Volatile private var pendingPitch: Double? = null
48
+ @Volatile private var pendingRoll: Double? = null
49
+ @Volatile private var pendingVfov: Double? = null
50
+ @Volatile private var pendingViewCorrection: Quaternion? = null
51
+
52
+ /// Dedicated render thread — owns all CesiumEngine + GPU work.
47
53
  private var renderThread: HandlerThread? = null
48
54
  private var renderHandler: Handler? = null
49
55
 
@@ -123,27 +129,38 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
123
129
  override var minAltitudeAboveTerrain: Double? = null
124
130
  set(value) { field = value; if (value != null) postToRender { bridge?.setMinAltitudeAboveTerrain(value.toFloat()) } }
125
131
 
132
+ // ── Rate caps ────────────────────────────────────────────────────────────
133
+ override var maxYawRateDegSec: Double? = null
134
+ set(value) { field = value; pushRateCaps() }
135
+ override var maxPitchRateDegSec: Double? = null
136
+ set(value) { field = value; pushRateCaps() }
137
+ override var maxRollRateDegSec: Double? = null
138
+ set(value) { field = value; pushRateCaps() }
139
+ override var maxClimbRateMps: Double? = null
140
+ set(value) { field = value; pushRateCaps() }
141
+ override var maxGroundSpeedMps: Double? = null
142
+ set(value) { field = value; pushRateCaps() }
143
+
144
+ private fun pushRateCaps() {
145
+ val yaw = maxYawRateDegSec ?: 0.0
146
+ val pitch = maxPitchRateDegSec ?: 0.0
147
+ val roll = maxRollRateDegSec ?: 0.0
148
+ val climb = maxClimbRateMps ?: 0.0
149
+ val gnd = maxGroundSpeedMps ?: 0.0
150
+ postToRender { bridge?.setRateCaps(yaw, pitch, roll, climb, gnd) }
151
+ }
152
+
126
153
  override var onMetrics: ((metrics: CesiumMetrics) -> Unit)? = null
127
154
 
128
155
  override val view: View get() = surfaceView
129
156
 
130
- private val frameCallback = Choreographer.FrameCallback { frameTimeNanos ->
157
+ private val frameCallback = Choreographer.FrameCallback {
131
158
  renderLoopActive = false
132
- if (pauseRendering || bridge == null) {
159
+ if (isShutdown || pauseRendering || bridge == null) {
133
160
  return@FrameCallback
134
161
  }
135
162
 
136
- val dt = if (lastFrameTimeNanos > 0) {
137
- (frameTimeNanos - lastFrameTimeNanos).coerceAtLeast(1_000_000L) / 1_000_000_000.0
138
- } else {
139
- 1.0 / 60.0
140
- }
141
- lastFrameTimeNanos = frameTimeNanos
142
-
143
- // Drop the tick if the previous frame's render hasn't finished — keeps the
144
- // render thread from queuing up an unbounded backlog under stutter.
145
163
  if (!renderInFlight.compareAndSet(false, true)) {
146
- // Re-arm so we try again next vsync.
147
164
  scheduleNextFrame(immediate = true)
148
165
  return@FrameCallback
149
166
  }
@@ -153,13 +170,13 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
153
170
  renderInFlight.set(false)
154
171
  return@FrameCallback
155
172
  }
156
- rh.post { runFrameOnRenderThread(dt) }
173
+ val nowSeconds = SystemClock.elapsedRealtimeNanos() / 1_000_000_000.0
174
+ rh.post { runFrameOnRenderThread(nowSeconds) }
157
175
  }
158
176
 
159
- /// Called on the render thread. Owns ALL CesiumEngine + GPU access; the UI
160
- /// thread only signals demand here. Safe to read `onMetrics` because Nitro
161
- /// callbacks dispatch internally to the JS thread.
162
- private fun runFrameOnRenderThread(dt: Double) {
177
+ /// Run on the render thread immediately after the bridge is created, before
178
+ /// the first frame.
179
+ private fun runFrameOnRenderThread(nowSeconds: Double) {
163
180
  val b = bridge
164
181
  if (b == null) {
165
182
  renderInFlight.set(false)
@@ -170,10 +187,11 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
170
187
  val nextImmediate: Boolean
171
188
  if (shouldRender) {
172
189
  idleProbeAccumulator = 0.0
173
- b.renderFrame(dt)
190
+ b.renderFrame(nowSeconds)
174
191
  nextImmediate = true
175
192
  } else {
176
- idleProbeAccumulator += dt
193
+ // Approximate dt for the idle probe; not used for motion.
194
+ idleProbeAccumulator += 1.0 / 60.0
177
195
  if (idleProbeAccumulator >= 0.25) {
178
196
  idleProbeAccumulator = 0.0
179
197
  b.markNeedsRender()
@@ -184,7 +202,7 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
184
202
  metricsFrameCounter++
185
203
  if (metricsFrameCounter >= 20) {
186
204
  metricsFrameCounter = 0
187
- val cb = onMetrics
205
+ val cb = if (isShutdown) null else onMetrics
188
206
  if (cb != null) {
189
207
  cb(CesiumMetrics(
190
208
  fps = b.getMetricsFps(),
@@ -201,9 +219,6 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
201
219
 
202
220
  renderInFlight.set(false)
203
221
 
204
- // Re-arm on the UI thread so Choreographer remains the source of truth
205
- // for vsync timing. Posting a no-op runnable to surfaceView keeps us off
206
- // the render thread for that small piece of bookkeeping.
207
222
  surfaceView.post { scheduleNextFrame(immediate = nextImmediate) }
208
223
  }
209
224
 
@@ -214,47 +229,38 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
214
229
  val h = surfaceView.height
215
230
  if (w <= 0 || h <= 0) return
216
231
 
217
- // Spin up the render thread on first surfaceCreated. If we previously
218
- // tore it down (e.g. on backgrounding), re-spawn cleanly.
219
232
  ensureRenderThread()
220
233
  val rh = renderHandler ?: return
221
234
 
222
235
  val cacheDir = appContext.cacheDir.absolutePath
223
236
  val cacertPath = extractCacert()
224
- // Init runs on the render thread because Vulkan instance/device creation
225
- // and Cesium Native init both touch state we'd rather only ever poke
226
- // from one thread.
227
237
  rh.post {
228
238
  val b = CesiumBridgeJNI()
229
239
  b.init(surface, w, h, cacheDir, cacertPath)
230
240
  bridge = b
231
241
  syncBridgePropsOnRenderThread(b)
232
242
 
233
- b.updateCamera(
234
- initialCamera.latitude, initialCamera.longitude, initialCamera.altitude,
235
- initialCamera.heading, initialCamera.pitch, initialCamera.roll,
243
+ val seed = pendingTeleport ?: initialCamera
244
+ b.teleport(
245
+ seed.latitude, seed.longitude, seed.altitude,
246
+ seed.heading, seed.pitch, seed.roll, seed.verticalFovDeg,
236
247
  )
237
- b.setVerticalFovDeg(initialCamera.verticalFovDeg)
238
- lastPushedCamera = initialCamera
239
-
240
- val pending = pendingRuntimeCamera
241
- if (pending != null) {
242
- val q = pendingRuntimeViewCorrection
243
- if (q != null) {
244
- b.updateCameraQuaternion(
245
- pending.latitude, pending.longitude, pending.altitude,
246
- pending.heading, pending.pitch, pending.roll,
247
- q.w, q.x, q.y, q.z,
248
- )
249
- lastPushedViewCorrection = q
250
- } else {
251
- b.updateCamera(
252
- pending.latitude, pending.longitude, pending.altitude,
253
- pending.heading, pending.pitch, pending.roll,
254
- )
255
- }
256
- b.setVerticalFovDeg(pending.verticalFovDeg)
257
- lastPushedCamera = pending
248
+ pendingTeleport = null
249
+
250
+ val la = pendingLat
251
+ val lo = pendingLon
252
+ if (la != null && lo != null) {
253
+ b.setPosition(la, lo); pendingLat = null; pendingLon = null
254
+ }
255
+ pendingAltitude?.let { b.setAltitude(it); pendingAltitude = null }
256
+ pendingHeading?.let { b.setHeading(it); pendingHeading = null }
257
+ val p = pendingPitch; val r = pendingRoll
258
+ if (p != null && r != null) {
259
+ b.setAttitude(p, r); pendingPitch = null; pendingRoll = null
260
+ }
261
+ pendingVfov?.let { b.setVerticalFovDeg(it); pendingVfov = null }
262
+ pendingViewCorrection?.let { q ->
263
+ b.setViewCorrection(q.w, q.x, q.y, q.z); pendingViewCorrection = null
258
264
  }
259
265
 
260
266
  if (ionAccessToken.isNotEmpty()) {
@@ -265,9 +271,8 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
265
271
  }
266
272
  b.setMsaa(msaaSampleCount.toInt())
267
273
 
268
- // Start the rendering loop here, after the bridge is fully
269
- // initialized. scheduleNextFrame must be invoked on the UI thread
270
- // because Choreographer.postFrameCallback() requires it.
274
+ pushRateCaps()
275
+
271
276
  surfaceView.post { scheduleNextFrame(immediate = true) }
272
277
  }
273
278
  }
@@ -276,30 +281,28 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
276
281
  postToRender {
277
282
  bridge?.resize(width, height)
278
283
  bridge?.markNeedsRender()
279
- // Re-arm the loop in case surfaceChanged fires before the bridge was
280
- // created (rare, but possible on cold starts). Idempotent: scheduleNextFrame
281
- // early-returns if the loop is already active.
282
284
  surfaceView.post { scheduleNextFrame(immediate = true) }
283
285
  }
284
286
  }
285
287
 
286
288
  override fun surfaceDestroyed(holder: SurfaceHolder) {
289
+ // Flip the shutdown gate and drop the JS callback before any teardown
290
+ // work — any in-flight render-thread frame or queued choreographer tick
291
+ // will then bail out without touching the bridge or emitting metrics
292
+ // into a dying JS runtime.
293
+ isShutdown = true
294
+ onMetrics = null
295
+
287
296
  val rh = renderHandler
288
297
  val ht = renderThread
289
298
 
290
299
  if (rh == null || ht == null) {
291
- // Nothing to drain; clear bridge reference if it somehow exists.
292
300
  bridge?.shutdown()
293
301
  bridge?.destroy()
294
302
  bridge = null
295
303
  return
296
304
  }
297
305
 
298
- // Block the UI thread (briefly) until the render thread has shut the
299
- // bridge down — surfaceDestroyed contract requires the surface to be
300
- // released before this returns. We cap our wait so a stuck GPU teardown
301
- // can't hang the UI thread indefinitely (matches our 1s shutdown
302
- // semaphore policy on the Vulkan / Metal side).
303
306
  val done = java.util.concurrent.CountDownLatch(1)
304
307
  rh.post {
305
308
  try {
@@ -313,8 +316,7 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
313
316
  }
314
317
  }
315
318
  if (!done.await(1500, java.util.concurrent.TimeUnit.MILLISECONDS)) {
316
- Log.w("CesiumView", "Render-thread shutdown did not complete in 1.5s; "
317
- + "tearing down handler thread anyway.")
319
+ Log.w("CesiumView", "Render-thread shutdown did not complete in 1.5s; tearing down handler thread anyway.")
318
320
  }
319
321
  stopRenderThread()
320
322
  }
@@ -341,53 +343,35 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
341
343
  ht.quitSafely()
342
344
  }
343
345
 
344
- /// Posts an action to the render thread. When the render thread isn't
345
- /// running yet (early prop assignment, or after teardown), we silently drop
346
- /// the action — bridges always take their initial config from the
347
- /// `syncBridgePropsOnRenderThread` step inside surfaceCreated.
348
346
  private fun postToRender(action: () -> Unit) {
349
347
  renderHandler?.post(action)
350
348
  }
351
349
 
352
- /// Camera reads cross the JNI from the JS thread (Nitro promise resolution).
353
- /// They read snapshot scalar values through CesiumBridgeAndroid → engine →
354
- /// GlobeCamera, none of which is mutated outside the render thread, so the
355
- /// worst that can happen is observing a value from a frame slightly behind
356
- /// what the render thread is encoding right now — acceptable for a UI-driven
357
- /// camera read.
358
- override fun getCameraState(): Promise<CameraState> {
350
+ // ── Camera readback ─────────────────────────────────────────────────────
351
+ override fun getActualCamera(): Promise<CameraState> {
359
352
  val b = bridge ?: return Promise.resolved(initialCamera)
360
353
  return Promise.resolved(CameraState(
361
- latitude = b.getCameraLat(),
362
- longitude = b.getCameraLon(),
363
- altitude = b.getCameraAlt(),
364
- heading = b.getCameraHeading(),
365
- pitch = b.getCameraPitch(),
366
- roll = b.getCameraRoll(),
367
- verticalFovDeg = b.getVerticalFovDeg(),
354
+ latitude = b.getActualLatitude(),
355
+ longitude = b.getActualLongitude(),
356
+ altitude = b.getActualAltitude(),
357
+ heading = b.getActualHeading(),
358
+ pitch = b.getActualPitch(),
359
+ roll = b.getActualRoll(),
360
+ verticalFovDeg = b.getActualVerticalFovDeg(),
368
361
  ))
369
362
  }
370
363
 
371
- override fun setCamera(camera: CameraState) {
372
- // Runtime camera control. Does NOT mutate `initialCamera` (which is
373
- // construction-time only).
374
- pendingRuntimeCamera = camera
375
- pendingRuntimeViewCorrection = null
376
- postToRender {
377
- pushCameraIfChanged(camera)
378
- bridge?.markNeedsRender()
379
- }
380
- scheduleNextFrame(immediate = true)
381
- }
382
-
383
- override fun setCameraQuaternion(camera: CameraState, viewCorrection: Quaternion) {
384
- pendingRuntimeCamera = camera
385
- pendingRuntimeViewCorrection = viewCorrection
386
- postToRender {
387
- pushCameraQuaternionIfChanged(camera, viewCorrection)
388
- bridge?.markNeedsRender()
389
- }
390
- scheduleNextFrame(immediate = true)
364
+ override fun getDemandCamera(): Promise<CameraState> {
365
+ val b = bridge ?: return Promise.resolved(initialCamera)
366
+ return Promise.resolved(CameraState(
367
+ latitude = b.getDemandLatitude(),
368
+ longitude = b.getDemandLongitude(),
369
+ altitude = b.getDemandAltitude(),
370
+ heading = b.getDemandHeading(),
371
+ pitch = b.getDemandPitch(),
372
+ roll = b.getDemandRoll(),
373
+ verticalFovDeg = b.getDemandVerticalFovDeg(),
374
+ ))
391
375
  }
392
376
 
393
377
  override fun getViewCorrection(): Promise<Quaternion> {
@@ -403,65 +387,63 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
403
387
  )
404
388
  }
405
389
 
406
- private fun pushCameraIfChanged(camera: CameraState) {
407
- val b = bridge ?: return
408
- val last = lastPushedCamera
409
- if (last != null &&
410
- last.latitude == camera.latitude &&
411
- last.longitude == camera.longitude &&
412
- last.altitude == camera.altitude &&
413
- last.heading == camera.heading &&
414
- last.pitch == camera.pitch &&
415
- last.roll == camera.roll &&
416
- last.verticalFovDeg == camera.verticalFovDeg) {
417
- return
390
+ // ── Per-DoF camera setters ──────────────────────────────────────────────
391
+ // These bypass the renderHandler hop: the underlying CameraIntegrator owns
392
+ // its own mutex and handles concurrent writers safely. Keeping the hot
393
+ // path off the render thread reduces latency for high-frequency
394
+ // (Reanimated worklet, 50 Hz IMU) drivers.
395
+ override fun setPosition(latitude: Double, longitude: Double) {
396
+ val b = bridge
397
+ if (b != null) {
398
+ b.setPosition(latitude, longitude)
399
+ scheduleNextFrame(immediate = true)
400
+ } else {
401
+ pendingLat = latitude; pendingLon = longitude
418
402
  }
419
- lastPushedCamera = camera
420
- b.updateCamera(camera.latitude, camera.longitude, camera.altitude,
421
- camera.heading, camera.pitch, camera.roll)
422
- b.setVerticalFovDeg(camera.verticalFovDeg)
423
403
  }
424
404
 
425
- private fun pushCameraQuaternionIfChanged(camera: CameraState, viewCorrection: Quaternion) {
426
- val b = bridge ?: return
427
- val last = lastPushedCamera
428
- val lastQ = lastPushedViewCorrection
429
- if (last != null && lastQ != null &&
430
- last.latitude == camera.latitude &&
431
- last.longitude == camera.longitude &&
432
- last.altitude == camera.altitude &&
433
- last.heading == camera.heading &&
434
- last.pitch == camera.pitch &&
435
- last.roll == camera.roll &&
436
- last.verticalFovDeg == camera.verticalFovDeg &&
437
- lastQ.w == viewCorrection.w &&
438
- lastQ.x == viewCorrection.x &&
439
- lastQ.y == viewCorrection.y &&
440
- lastQ.z == viewCorrection.z
441
- ) {
442
- return
405
+ override fun setAltitude(altitudeMeters: Double) {
406
+ val b = bridge
407
+ if (b != null) { b.setAltitude(altitudeMeters); scheduleNextFrame(immediate = true) }
408
+ else { pendingAltitude = altitudeMeters }
409
+ }
410
+
411
+ override fun setHeading(headingDeg: Double) {
412
+ val b = bridge
413
+ if (b != null) { b.setHeading(headingDeg); scheduleNextFrame(immediate = true) }
414
+ else { pendingHeading = headingDeg }
415
+ }
416
+
417
+ override fun setAttitude(pitchDeg: Double, rollDeg: Double) {
418
+ val b = bridge
419
+ if (b != null) { b.setAttitude(pitchDeg, rollDeg); scheduleNextFrame(immediate = true) }
420
+ else { pendingPitch = pitchDeg; pendingRoll = rollDeg }
421
+ }
422
+
423
+ override fun setViewCorrection(q: Quaternion) {
424
+ val b = bridge
425
+ if (b != null) { b.setViewCorrection(q.w, q.x, q.y, q.z); scheduleNextFrame(immediate = true) }
426
+ else { pendingViewCorrection = q }
427
+ }
428
+
429
+ override fun setVerticalFov(deg: Double) {
430
+ val b = bridge
431
+ if (b != null) { b.setVerticalFovDeg(deg); scheduleNextFrame(immediate = true) }
432
+ else { pendingVfov = deg }
433
+ }
434
+
435
+ override fun teleport(camera: CameraState) {
436
+ val b = bridge
437
+ if (b != null) {
438
+ b.teleport(camera.latitude, camera.longitude, camera.altitude,
439
+ camera.heading, camera.pitch, camera.roll, camera.verticalFovDeg)
440
+ scheduleNextFrame(immediate = true)
441
+ } else {
442
+ pendingTeleport = camera
443
443
  }
444
- lastPushedCamera = camera
445
- lastPushedViewCorrection = viewCorrection
446
- b.updateCameraQuaternion(
447
- camera.latitude,
448
- camera.longitude,
449
- camera.altitude,
450
- camera.heading,
451
- camera.pitch,
452
- camera.roll,
453
- viewCorrection.w,
454
- viewCorrection.x,
455
- viewCorrection.y,
456
- viewCorrection.z,
457
- )
458
- b.setVerticalFovDeg(camera.verticalFovDeg)
459
444
  }
460
445
 
461
- /// Run on the render thread immediately after the bridge is created, before
462
- /// the first frame. Matches Swift's syncBridgeOptionsFromProps and pushes
463
- /// every prop into the freshly-built CesiumEngine so the consumer doesn't
464
- /// have to re-set them after the surface comes up.
446
+ /// Render-thread setup, called once after the bridge is built.
465
447
  private fun syncBridgePropsOnRenderThread(b: CesiumBridgeJNI) {
466
448
  b.setMaxSSE(maximumScreenSpaceError)
467
449
  b.setMaxSimLoads(maximumSimultaneousTileLoads.toInt())
@@ -483,12 +465,6 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
483
465
 
484
466
  /**
485
467
  * Schedule the next frame callback.
486
- *
487
- * - `immediate = true`: the engine wants to render this vsync (active pan,
488
- * tile loading, prop change). Re-post on the next available vsync.
489
- * - `immediate = false`: the engine is idle. Re-post in 250ms — matches the
490
- * iOS low-refresh range and keeps the main thread mostly asleep while
491
- * still picking up late tile completions / network responses.
492
468
  */
493
469
  private fun scheduleNextFrame(immediate: Boolean) {
494
470
  if (renderLoopActive || pauseRendering || bridge == null) return
@@ -504,15 +480,12 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
504
480
  val outFile = File(appContext.cacheDir, "cacert.pem")
505
481
  try {
506
482
  appContext.assets.open("cacert.pem").use { input ->
507
- // Re-extract if the cached copy is missing or a different size to the
508
- // bundled asset (handles the file being updated across app versions).
509
483
  val assetSize = input.available().toLong()
510
484
  if (!outFile.exists() || outFile.length() != assetSize) {
511
485
  FileOutputStream(outFile).use { output -> input.copyTo(output) }
512
486
  }
513
487
  }
514
488
  } catch (_: Exception) {
515
- // cacert.pem not bundled as asset — libcurl will use system certs
516
489
  return if (outFile.exists()) outFile.absolutePath else ""
517
490
  }
518
491
  return outFile.absolutePath