react-native-cesium 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -27
- package/android/CMakeLists.txt +1 -2
- package/android/src/main/cpp/CesiumBridgeAndroid.cpp +264 -130
- package/android/src/main/cpp/CesiumBridgeAndroid.h +70 -27
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/CesiumBridgeJNI.kt +68 -44
- package/android/src/main/java/com/margelo/nitro/reactnativecesium/HybridCesiumView.kt +199 -187
- package/cpp/engine/CameraIntegrator.cpp +374 -0
- package/cpp/engine/CameraIntegrator.hpp +146 -0
- package/cpp/engine/EngineTunables.hpp +90 -31
- package/cpp/engine/GlobeCamera.cpp +17 -6
- package/cpp/engine/GlobeCamera.hpp +11 -8
- package/cpp/engine/test/CameraIntegratorTest.cpp +205 -0
- package/cpp/engine/test/build_and_run.sh +27 -0
- package/example/README.md +1 -1
- package/ios/CesiumBridge.h +53 -28
- package/ios/CesiumBridge.mm +172 -86
- package/ios/HybridCesiumView.swift +256 -145
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts +85 -13
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/ReactNativeCesiumOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_CameraState.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +108 -9
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +21 -3
- package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/Func_void_CameraState.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +71 -3
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.cpp +3 -3
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +43 -28
- package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +89 -9
- package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +30 -0
- package/nitrogen/generated/ios/swift/Func_void_CameraState.swift +5 -5
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +15 -3
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +242 -16
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +21 -3
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +22 -4
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +66 -0
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/CesiumViewConfig.json +6 -0
- package/package.json +1 -1
- package/src/specs/cesium-view.nitro.ts +102 -14
- package/cpp/engine/CameraSmoother.cpp +0 -68
- package/cpp/engine/CameraSmoother.hpp +0 -28
- package/cpp/engine/CameraTargetState.cpp +0 -205
- package/cpp/engine/CameraTargetState.hpp +0 -126
|
@@ -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
|
|
21
|
-
/// (`bridge != null`)
|
|
22
|
-
///
|
|
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
|
-
///
|
|
44
|
-
///
|
|
45
|
-
///
|
|
46
|
-
///
|
|
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,54 @@ 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
|
|
|
155
|
+
override var onActualCamera: ((camera: CameraState) -> Unit)? = null
|
|
156
|
+
// Both accessed only on the render thread; no extra locking needed.
|
|
157
|
+
private var actualCameraLastEmitSec: Double = 0.0
|
|
158
|
+
private var actualCameraLastSent: CameraState? = null
|
|
159
|
+
|
|
160
|
+
// Returns true when the two snapshots differ enough to warrant a JS dispatch.
|
|
161
|
+
// Thresholds mirror EngineTunables.hpp kActualCameraCallbackInterval* constants.
|
|
162
|
+
private fun actualCameraChanged(a: CameraState, b: CameraState): Boolean =
|
|
163
|
+
Math.abs(a.latitude - b.latitude) > 1e-7 ||
|
|
164
|
+
Math.abs(a.longitude - b.longitude) > 1e-7 ||
|
|
165
|
+
Math.abs(a.altitude - b.altitude) > 0.05 ||
|
|
166
|
+
Math.abs(a.heading - b.heading) > 0.01 ||
|
|
167
|
+
Math.abs(a.pitch - b.pitch) > 0.01 ||
|
|
168
|
+
Math.abs(a.roll - b.roll) > 0.01 ||
|
|
169
|
+
Math.abs(a.verticalFovDeg - b.verticalFovDeg) > 0.01
|
|
170
|
+
|
|
128
171
|
override val view: View get() = surfaceView
|
|
129
172
|
|
|
130
|
-
private val frameCallback = Choreographer.FrameCallback {
|
|
173
|
+
private val frameCallback = Choreographer.FrameCallback {
|
|
131
174
|
renderLoopActive = false
|
|
132
|
-
if (pauseRendering || bridge == null) {
|
|
175
|
+
if (isShutdown || pauseRendering || bridge == null) {
|
|
133
176
|
return@FrameCallback
|
|
134
177
|
}
|
|
135
178
|
|
|
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
179
|
if (!renderInFlight.compareAndSet(false, true)) {
|
|
146
|
-
// Re-arm so we try again next vsync.
|
|
147
180
|
scheduleNextFrame(immediate = true)
|
|
148
181
|
return@FrameCallback
|
|
149
182
|
}
|
|
@@ -153,13 +186,13 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
153
186
|
renderInFlight.set(false)
|
|
154
187
|
return@FrameCallback
|
|
155
188
|
}
|
|
156
|
-
|
|
189
|
+
val nowSeconds = SystemClock.elapsedRealtimeNanos() / 1_000_000_000.0
|
|
190
|
+
rh.post { runFrameOnRenderThread(nowSeconds) }
|
|
157
191
|
}
|
|
158
192
|
|
|
159
|
-
///
|
|
160
|
-
///
|
|
161
|
-
|
|
162
|
-
private fun runFrameOnRenderThread(dt: Double) {
|
|
193
|
+
/// Run on the render thread immediately after the bridge is created, before
|
|
194
|
+
/// the first frame.
|
|
195
|
+
private fun runFrameOnRenderThread(nowSeconds: Double) {
|
|
163
196
|
val b = bridge
|
|
164
197
|
if (b == null) {
|
|
165
198
|
renderInFlight.set(false)
|
|
@@ -170,10 +203,11 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
170
203
|
val nextImmediate: Boolean
|
|
171
204
|
if (shouldRender) {
|
|
172
205
|
idleProbeAccumulator = 0.0
|
|
173
|
-
b.renderFrame(
|
|
206
|
+
b.renderFrame(nowSeconds)
|
|
174
207
|
nextImmediate = true
|
|
175
208
|
} else {
|
|
176
|
-
|
|
209
|
+
// Approximate dt for the idle probe; not used for motion.
|
|
210
|
+
idleProbeAccumulator += 1.0 / 60.0
|
|
177
211
|
if (idleProbeAccumulator >= 0.25) {
|
|
178
212
|
idleProbeAccumulator = 0.0
|
|
179
213
|
b.markNeedsRender()
|
|
@@ -184,7 +218,7 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
184
218
|
metricsFrameCounter++
|
|
185
219
|
if (metricsFrameCounter >= 20) {
|
|
186
220
|
metricsFrameCounter = 0
|
|
187
|
-
val cb = onMetrics
|
|
221
|
+
val cb = if (isShutdown) null else onMetrics
|
|
188
222
|
if (cb != null) {
|
|
189
223
|
cb(CesiumMetrics(
|
|
190
224
|
fps = b.getMetricsFps(),
|
|
@@ -199,11 +233,29 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
199
233
|
}
|
|
200
234
|
}
|
|
201
235
|
|
|
236
|
+
// onActualCamera: poll at 5 Hz (kActualCameraCallbackIntervalSec = 0.2 s),
|
|
237
|
+
// but only dispatch when at least one field has moved beyond its epsilon.
|
|
238
|
+
val acCb = if (isShutdown) null else onActualCamera
|
|
239
|
+
if (acCb != null && (nowSeconds - actualCameraLastEmitSec) >= 0.2) {
|
|
240
|
+
actualCameraLastEmitSec = nowSeconds
|
|
241
|
+
val candidate = CameraState(
|
|
242
|
+
latitude = b.getActualLatitude(),
|
|
243
|
+
longitude = b.getActualLongitude(),
|
|
244
|
+
altitude = b.getActualAltitude(),
|
|
245
|
+
heading = b.getActualHeading(),
|
|
246
|
+
pitch = b.getActualPitch(),
|
|
247
|
+
roll = b.getActualRoll(),
|
|
248
|
+
verticalFovDeg = b.getActualVerticalFovDeg(),
|
|
249
|
+
)
|
|
250
|
+
val prev = actualCameraLastSent
|
|
251
|
+
if (prev == null || actualCameraChanged(candidate, prev)) {
|
|
252
|
+
actualCameraLastSent = candidate
|
|
253
|
+
acCb(candidate)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
202
257
|
renderInFlight.set(false)
|
|
203
258
|
|
|
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
259
|
surfaceView.post { scheduleNextFrame(immediate = nextImmediate) }
|
|
208
260
|
}
|
|
209
261
|
|
|
@@ -214,47 +266,38 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
214
266
|
val h = surfaceView.height
|
|
215
267
|
if (w <= 0 || h <= 0) return
|
|
216
268
|
|
|
217
|
-
// Spin up the render thread on first surfaceCreated. If we previously
|
|
218
|
-
// tore it down (e.g. on backgrounding), re-spawn cleanly.
|
|
219
269
|
ensureRenderThread()
|
|
220
270
|
val rh = renderHandler ?: return
|
|
221
271
|
|
|
222
272
|
val cacheDir = appContext.cacheDir.absolutePath
|
|
223
273
|
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
274
|
rh.post {
|
|
228
275
|
val b = CesiumBridgeJNI()
|
|
229
276
|
b.init(surface, w, h, cacheDir, cacertPath)
|
|
230
277
|
bridge = b
|
|
231
278
|
syncBridgePropsOnRenderThread(b)
|
|
232
279
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
280
|
+
val seed = pendingTeleport ?: initialCamera
|
|
281
|
+
b.teleport(
|
|
282
|
+
seed.latitude, seed.longitude, seed.altitude,
|
|
283
|
+
seed.heading, seed.pitch, seed.roll, seed.verticalFovDeg,
|
|
236
284
|
)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
val
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
pending.heading, pending.pitch, pending.roll,
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
b.setVerticalFovDeg(pending.verticalFovDeg)
|
|
257
|
-
lastPushedCamera = pending
|
|
285
|
+
pendingTeleport = null
|
|
286
|
+
|
|
287
|
+
val la = pendingLat
|
|
288
|
+
val lo = pendingLon
|
|
289
|
+
if (la != null && lo != null) {
|
|
290
|
+
b.setPosition(la, lo); pendingLat = null; pendingLon = null
|
|
291
|
+
}
|
|
292
|
+
pendingAltitude?.let { b.setAltitude(it); pendingAltitude = null }
|
|
293
|
+
pendingHeading?.let { b.setHeading(it); pendingHeading = null }
|
|
294
|
+
val p = pendingPitch; val r = pendingRoll
|
|
295
|
+
if (p != null && r != null) {
|
|
296
|
+
b.setAttitude(p, r); pendingPitch = null; pendingRoll = null
|
|
297
|
+
}
|
|
298
|
+
pendingVfov?.let { b.setVerticalFovDeg(it); pendingVfov = null }
|
|
299
|
+
pendingViewCorrection?.let { q ->
|
|
300
|
+
b.setViewCorrection(q.w, q.x, q.y, q.z); pendingViewCorrection = null
|
|
258
301
|
}
|
|
259
302
|
|
|
260
303
|
if (ionAccessToken.isNotEmpty()) {
|
|
@@ -265,9 +308,8 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
265
308
|
}
|
|
266
309
|
b.setMsaa(msaaSampleCount.toInt())
|
|
267
310
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// because Choreographer.postFrameCallback() requires it.
|
|
311
|
+
pushRateCaps()
|
|
312
|
+
|
|
271
313
|
surfaceView.post { scheduleNextFrame(immediate = true) }
|
|
272
314
|
}
|
|
273
315
|
}
|
|
@@ -276,30 +318,30 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
276
318
|
postToRender {
|
|
277
319
|
bridge?.resize(width, height)
|
|
278
320
|
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
321
|
surfaceView.post { scheduleNextFrame(immediate = true) }
|
|
283
322
|
}
|
|
284
323
|
}
|
|
285
324
|
|
|
286
325
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
326
|
+
// Flip the shutdown gate and drop the JS callback before any teardown
|
|
327
|
+
// work — any in-flight render-thread frame or queued choreographer tick
|
|
328
|
+
// will then bail out without touching the bridge or emitting metrics
|
|
329
|
+
// into a dying JS runtime.
|
|
330
|
+
isShutdown = true
|
|
331
|
+
onMetrics = null
|
|
332
|
+
onActualCamera = null
|
|
333
|
+
actualCameraLastSent = null
|
|
334
|
+
|
|
287
335
|
val rh = renderHandler
|
|
288
336
|
val ht = renderThread
|
|
289
337
|
|
|
290
338
|
if (rh == null || ht == null) {
|
|
291
|
-
// Nothing to drain; clear bridge reference if it somehow exists.
|
|
292
339
|
bridge?.shutdown()
|
|
293
340
|
bridge?.destroy()
|
|
294
341
|
bridge = null
|
|
295
342
|
return
|
|
296
343
|
}
|
|
297
344
|
|
|
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
345
|
val done = java.util.concurrent.CountDownLatch(1)
|
|
304
346
|
rh.post {
|
|
305
347
|
try {
|
|
@@ -313,8 +355,7 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
313
355
|
}
|
|
314
356
|
}
|
|
315
357
|
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.")
|
|
358
|
+
Log.w("CesiumView", "Render-thread shutdown did not complete in 1.5s; tearing down handler thread anyway.")
|
|
318
359
|
}
|
|
319
360
|
stopRenderThread()
|
|
320
361
|
}
|
|
@@ -341,53 +382,35 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
341
382
|
ht.quitSafely()
|
|
342
383
|
}
|
|
343
384
|
|
|
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
385
|
private fun postToRender(action: () -> Unit) {
|
|
349
386
|
renderHandler?.post(action)
|
|
350
387
|
}
|
|
351
388
|
|
|
352
|
-
|
|
353
|
-
|
|
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> {
|
|
389
|
+
// ── Camera readback ─────────────────────────────────────────────────────
|
|
390
|
+
override fun getActualCamera(): Promise<CameraState> {
|
|
359
391
|
val b = bridge ?: return Promise.resolved(initialCamera)
|
|
360
392
|
return Promise.resolved(CameraState(
|
|
361
|
-
latitude = b.
|
|
362
|
-
longitude = b.
|
|
363
|
-
altitude = b.
|
|
364
|
-
heading = b.
|
|
365
|
-
pitch = b.
|
|
366
|
-
roll = b.
|
|
367
|
-
verticalFovDeg = b.
|
|
393
|
+
latitude = b.getActualLatitude(),
|
|
394
|
+
longitude = b.getActualLongitude(),
|
|
395
|
+
altitude = b.getActualAltitude(),
|
|
396
|
+
heading = b.getActualHeading(),
|
|
397
|
+
pitch = b.getActualPitch(),
|
|
398
|
+
roll = b.getActualRoll(),
|
|
399
|
+
verticalFovDeg = b.getActualVerticalFovDeg(),
|
|
368
400
|
))
|
|
369
401
|
}
|
|
370
402
|
|
|
371
|
-
override fun
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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)
|
|
403
|
+
override fun getDemandCamera(): Promise<CameraState> {
|
|
404
|
+
val b = bridge ?: return Promise.resolved(initialCamera)
|
|
405
|
+
return Promise.resolved(CameraState(
|
|
406
|
+
latitude = b.getDemandLatitude(),
|
|
407
|
+
longitude = b.getDemandLongitude(),
|
|
408
|
+
altitude = b.getDemandAltitude(),
|
|
409
|
+
heading = b.getDemandHeading(),
|
|
410
|
+
pitch = b.getDemandPitch(),
|
|
411
|
+
roll = b.getDemandRoll(),
|
|
412
|
+
verticalFovDeg = b.getDemandVerticalFovDeg(),
|
|
413
|
+
))
|
|
391
414
|
}
|
|
392
415
|
|
|
393
416
|
override fun getViewCorrection(): Promise<Quaternion> {
|
|
@@ -403,65 +426,63 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
403
426
|
)
|
|
404
427
|
}
|
|
405
428
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
429
|
+
// ── Per-DoF camera setters ──────────────────────────────────────────────
|
|
430
|
+
// These bypass the renderHandler hop: the underlying CameraIntegrator owns
|
|
431
|
+
// its own mutex and handles concurrent writers safely. Keeping the hot
|
|
432
|
+
// path off the render thread reduces latency for high-frequency
|
|
433
|
+
// (Reanimated worklet, 50 Hz IMU) drivers.
|
|
434
|
+
override fun setPosition(latitude: Double, longitude: Double) {
|
|
435
|
+
val b = bridge
|
|
436
|
+
if (b != null) {
|
|
437
|
+
b.setPosition(latitude, longitude)
|
|
438
|
+
scheduleNextFrame(immediate = true)
|
|
439
|
+
} else {
|
|
440
|
+
pendingLat = latitude; pendingLon = longitude
|
|
418
441
|
}
|
|
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
442
|
}
|
|
424
443
|
|
|
425
|
-
|
|
426
|
-
val b = bridge
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
444
|
+
override fun setAltitude(altitudeMeters: Double) {
|
|
445
|
+
val b = bridge
|
|
446
|
+
if (b != null) { b.setAltitude(altitudeMeters); scheduleNextFrame(immediate = true) }
|
|
447
|
+
else { pendingAltitude = altitudeMeters }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
override fun setHeading(headingDeg: Double) {
|
|
451
|
+
val b = bridge
|
|
452
|
+
if (b != null) { b.setHeading(headingDeg); scheduleNextFrame(immediate = true) }
|
|
453
|
+
else { pendingHeading = headingDeg }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
override fun setAttitude(pitchDeg: Double, rollDeg: Double) {
|
|
457
|
+
val b = bridge
|
|
458
|
+
if (b != null) { b.setAttitude(pitchDeg, rollDeg); scheduleNextFrame(immediate = true) }
|
|
459
|
+
else { pendingPitch = pitchDeg; pendingRoll = rollDeg }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
override fun setViewCorrection(q: Quaternion) {
|
|
463
|
+
val b = bridge
|
|
464
|
+
if (b != null) { b.setViewCorrection(q.w, q.x, q.y, q.z); scheduleNextFrame(immediate = true) }
|
|
465
|
+
else { pendingViewCorrection = q }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
override fun setVerticalFov(deg: Double) {
|
|
469
|
+
val b = bridge
|
|
470
|
+
if (b != null) { b.setVerticalFovDeg(deg); scheduleNextFrame(immediate = true) }
|
|
471
|
+
else { pendingVfov = deg }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
override fun teleport(camera: CameraState) {
|
|
475
|
+
val b = bridge
|
|
476
|
+
if (b != null) {
|
|
477
|
+
b.teleport(camera.latitude, camera.longitude, camera.altitude,
|
|
478
|
+
camera.heading, camera.pitch, camera.roll, camera.verticalFovDeg)
|
|
479
|
+
scheduleNextFrame(immediate = true)
|
|
480
|
+
} else {
|
|
481
|
+
pendingTeleport = camera
|
|
443
482
|
}
|
|
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
483
|
}
|
|
460
484
|
|
|
461
|
-
///
|
|
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.
|
|
485
|
+
/// Render-thread setup, called once after the bridge is built.
|
|
465
486
|
private fun syncBridgePropsOnRenderThread(b: CesiumBridgeJNI) {
|
|
466
487
|
b.setMaxSSE(maximumScreenSpaceError)
|
|
467
488
|
b.setMaxSimLoads(maximumSimultaneousTileLoads.toInt())
|
|
@@ -483,12 +504,6 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
483
504
|
|
|
484
505
|
/**
|
|
485
506
|
* 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
507
|
*/
|
|
493
508
|
private fun scheduleNextFrame(immediate: Boolean) {
|
|
494
509
|
if (renderLoopActive || pauseRendering || bridge == null) return
|
|
@@ -504,15 +519,12 @@ class HybridCesiumView(private val appContext: Context) : HybridCesiumViewSpec()
|
|
|
504
519
|
val outFile = File(appContext.cacheDir, "cacert.pem")
|
|
505
520
|
try {
|
|
506
521
|
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
522
|
val assetSize = input.available().toLong()
|
|
510
523
|
if (!outFile.exists() || outFile.length() != assetSize) {
|
|
511
524
|
FileOutputStream(outFile).use { output -> input.copyTo(output) }
|
|
512
525
|
}
|
|
513
526
|
}
|
|
514
527
|
} catch (_: Exception) {
|
|
515
|
-
// cacert.pem not bundled as asset — libcurl will use system certs
|
|
516
528
|
return if (outFile.exists()) outFile.absolutePath else ""
|
|
517
529
|
}
|
|
518
530
|
return outFile.absolutePath
|