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.
- package/README.md +100 -26
- 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 +160 -187
- package/cpp/engine/CameraIntegrator.cpp +374 -0
- package/cpp/engine/CameraIntegrator.hpp +146 -0
- package/cpp/engine/CesiumEngine.cpp +3 -1
- package/cpp/engine/EngineTunables.hpp +80 -21
- 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 +213 -145
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts +77 -13
- package/lib/typescript/src/specs/cesium-view.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.cpp +90 -9
- package/nitrogen/generated/android/c++/JHybridCesiumViewSpec.hpp +19 -3
- package/nitrogen/generated/android/c++/views/JHybridCesiumViewStateUpdater.cpp +20 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecesium/HybridCesiumViewSpec.kt +57 -3
- package/nitrogen/generated/ios/ReactNativeCesium-Swift-Cxx-Bridge.hpp +9 -9
- package/nitrogen/generated/ios/c++/HybridCesiumViewSpecSwift.hpp +82 -9
- package/nitrogen/generated/ios/c++/views/HybridCesiumViewComponent.mm +25 -0
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec.swift +14 -3
- package/nitrogen/generated/ios/swift/HybridCesiumViewSpec_cxx.swift +210 -16
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.cpp +19 -3
- package/nitrogen/generated/shared/c++/HybridCesiumViewSpec.hpp +20 -4
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.cpp +55 -0
- package/nitrogen/generated/shared/c++/views/HybridCesiumViewComponent.hpp +5 -0
- package/nitrogen/generated/shared/json/CesiumViewConfig.json +5 -0
- package/package.json +2 -2
- package/src/specs/cesium-view.nitro.ts +93 -14
- package/cpp/engine/CameraSmoother.cpp +0 -71
- package/cpp/engine/CameraSmoother.hpp +0 -25
- package/cpp/engine/CameraTargetState.cpp +0 -140
- package/cpp/engine/CameraTargetState.hpp +0 -103
|
@@ -2,8 +2,18 @@ import Foundation
|
|
|
2
2
|
import Metal
|
|
3
3
|
import MetalKit
|
|
4
4
|
import NitroModules
|
|
5
|
+
import QuartzCore
|
|
5
6
|
import UIKit
|
|
6
7
|
|
|
8
|
+
/// Weak proxy for CADisplayLink so the timer does not strongly retain its
|
|
9
|
+
/// owning view. Without this, CADisplayLink → HybridCesiumView → displayLink
|
|
10
|
+
/// forms a cycle that prevents `deinit` from ever firing, which in turn
|
|
11
|
+
/// prevents the C++ engine and SqliteCache handle from being released.
|
|
12
|
+
private final class DisplayLinkProxy {
|
|
13
|
+
weak var owner: HybridCesiumView?
|
|
14
|
+
@objc func tick() { owner?.handleDisplayLinkTick() }
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
class HybridCesiumView: HybridCesiumViewSpec {
|
|
8
18
|
// MARK: - Props
|
|
9
19
|
|
|
@@ -22,9 +32,9 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
/// Construction-time seed camera. Used exactly once
|
|
26
|
-
/// is created; subsequent prop writes are ignored.
|
|
27
|
-
///
|
|
35
|
+
/// Construction-time seed camera. Used exactly once via `teleport(...)`
|
|
36
|
+
/// when the native bridge is created; subsequent prop writes are ignored.
|
|
37
|
+
/// Use the per-DoF setters (or `teleport(...)`) for runtime updates.
|
|
28
38
|
var initialCamera: CameraState = CameraState(
|
|
29
39
|
latitude: 46.15,
|
|
30
40
|
longitude: 7.35,
|
|
@@ -55,7 +65,6 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
55
65
|
var msaaSampleCount: Double = 1 {
|
|
56
66
|
didSet {
|
|
57
67
|
let s = Int32(msaaSampleCount)
|
|
58
|
-
// applyMtkViewMsaa touches MTKView/UIKit so it stays on main.
|
|
59
68
|
applyMtkViewMsaa(Int(s))
|
|
60
69
|
postToRender { [weak self] in self?.bridge?.setMsaaSampleCount(s) }
|
|
61
70
|
}
|
|
@@ -105,12 +114,31 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
105
114
|
didSet { if let v = minAltitudeAboveTerrain { postToRender { [weak self] in self?.bridge?.setMinAltitudeAboveTerrain(Float(v)) } } }
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
// ── Rate caps ────────────────────────────────────────────────────────────
|
|
118
|
+
var maxYawRateDegSec: Double? { didSet { pushRateCaps() } }
|
|
119
|
+
var maxPitchRateDegSec: Double? { didSet { pushRateCaps() } }
|
|
120
|
+
var maxRollRateDegSec: Double? { didSet { pushRateCaps() } }
|
|
121
|
+
var maxClimbRateMps: Double? { didSet { pushRateCaps() } }
|
|
122
|
+
var maxGroundSpeedMps: Double? { didSet { pushRateCaps() } }
|
|
123
|
+
|
|
124
|
+
private func pushRateCaps() {
|
|
125
|
+
let yaw = maxYawRateDegSec ?? 0
|
|
126
|
+
let pitch = maxPitchRateDegSec ?? 0
|
|
127
|
+
let roll = maxRollRateDegSec ?? 0
|
|
128
|
+
let climb = maxClimbRateMps ?? 0
|
|
129
|
+
let gnd = maxGroundSpeedMps ?? 0
|
|
130
|
+
postToRender { [weak self] in
|
|
131
|
+
self?.bridge?.setRateCapsYaw(yaw, pitch: pitch, roll: roll,
|
|
132
|
+
climb: climb, groundSpeed: gnd)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
// MARK: - Render-thread dispatch helper
|
|
109
137
|
|
|
110
138
|
/// Posts a closure to the dedicated render queue. Used by every prop setter
|
|
111
|
-
/// that mutates engine state
|
|
112
|
-
///
|
|
113
|
-
///
|
|
139
|
+
/// that mutates engine state. Camera demand setters do **not** hop here —
|
|
140
|
+
/// the CameraIntegrator's mutex makes per-DoF writes safe from any thread,
|
|
141
|
+
/// and skipping the dispatch keeps latency minimal for high-rate updates.
|
|
114
142
|
private func postToRender(_ action: @escaping () -> Void) {
|
|
115
143
|
renderQueue.async { action() }
|
|
116
144
|
}
|
|
@@ -119,7 +147,30 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
119
147
|
|
|
120
148
|
// MARK: - Methods
|
|
121
149
|
|
|
122
|
-
func
|
|
150
|
+
func getActualCamera() throws -> Promise<CameraState> {
|
|
151
|
+
guard let b = bridge else {
|
|
152
|
+
return Promise.rejected(
|
|
153
|
+
withError: NSError(
|
|
154
|
+
domain: "HybridCesiumView",
|
|
155
|
+
code: 1,
|
|
156
|
+
userInfo: [NSLocalizedDescriptionKey: "Native bridge not initialized"]
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
return Promise.resolved(
|
|
161
|
+
withResult: CameraState(
|
|
162
|
+
latitude: b.readActualLatitude(),
|
|
163
|
+
longitude: b.readActualLongitude(),
|
|
164
|
+
altitude: b.readActualAltitude(),
|
|
165
|
+
heading: b.readActualHeading(),
|
|
166
|
+
pitch: b.readActualPitch(),
|
|
167
|
+
roll: b.readActualRoll(),
|
|
168
|
+
verticalFovDeg: b.readActualVerticalFovDeg()
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func getDemandCamera() throws -> Promise<CameraState> {
|
|
123
174
|
guard let b = bridge else {
|
|
124
175
|
return Promise.rejected(
|
|
125
176
|
withError: NSError(
|
|
@@ -129,47 +180,79 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
129
180
|
)
|
|
130
181
|
)
|
|
131
182
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
return Promise.resolved(
|
|
184
|
+
withResult: CameraState(
|
|
185
|
+
latitude: b.readDemandLatitude(),
|
|
186
|
+
longitude: b.readDemandLongitude(),
|
|
187
|
+
altitude: b.readDemandAltitude(),
|
|
188
|
+
heading: b.readDemandHeading(),
|
|
189
|
+
pitch: b.readDemandPitch(),
|
|
190
|
+
roll: b.readDemandRoll(),
|
|
191
|
+
verticalFovDeg: b.readDemandVerticalFovDeg()
|
|
192
|
+
)
|
|
140
193
|
)
|
|
141
|
-
return Promise.resolved(withResult: s)
|
|
142
194
|
}
|
|
143
195
|
|
|
144
|
-
func
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
196
|
+
func setPosition(latitude: Double, longitude: Double) throws {
|
|
197
|
+
if let b = bridge {
|
|
198
|
+
b.setPositionLatitude(latitude, longitude: longitude)
|
|
199
|
+
} else {
|
|
200
|
+
pendingLat = latitude
|
|
201
|
+
pendingLon = longitude
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func setAltitude(altitudeMeters: Double) throws {
|
|
206
|
+
if let b = bridge { b.setAltitude(altitudeMeters) }
|
|
207
|
+
else { pendingAltitude = altitudeMeters }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func setHeading(headingDeg: Double) throws {
|
|
211
|
+
if let b = bridge { b.setHeadingDeg(headingDeg) }
|
|
212
|
+
else { pendingHeading = headingDeg }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func setAttitude(pitchDeg: Double, rollDeg: Double) throws {
|
|
216
|
+
if let b = bridge { b.setAttitudePitch(pitchDeg, roll: rollDeg) }
|
|
217
|
+
else { pendingPitch = pitchDeg; pendingRoll = rollDeg }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func setViewCorrection(q: Quaternion) throws {
|
|
221
|
+
if let b = bridge { b.setViewCorrectionW(q.w, x: q.x, y: q.y, z: q.z) }
|
|
222
|
+
else { pendingViewCorrection = q }
|
|
152
223
|
}
|
|
153
224
|
|
|
154
|
-
func
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
225
|
+
func setVerticalFov(deg: Double) throws {
|
|
226
|
+
if let b = bridge { b.setVerticalFovDeg(deg) }
|
|
227
|
+
else { pendingVfov = deg }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func teleport(camera: CameraState) throws {
|
|
231
|
+
if let b = bridge {
|
|
232
|
+
b.teleportLatitude(camera.latitude,
|
|
233
|
+
longitude: camera.longitude,
|
|
234
|
+
altitude: camera.altitude,
|
|
235
|
+
heading: camera.heading,
|
|
236
|
+
pitch: camera.pitch,
|
|
237
|
+
roll: camera.roll,
|
|
238
|
+
verticalFovDeg: camera.verticalFovDeg)
|
|
239
|
+
} else {
|
|
240
|
+
pendingTeleport = camera
|
|
241
|
+
}
|
|
158
242
|
}
|
|
159
243
|
|
|
160
244
|
func getViewCorrection() throws -> Promise<Quaternion> {
|
|
161
245
|
guard let b = bridge else {
|
|
162
|
-
return Promise.resolved(
|
|
163
|
-
withResult: Quaternion(w: 1, x: 0, y: 0, z: 0)
|
|
164
|
-
)
|
|
246
|
+
return Promise.resolved(withResult: Quaternion(w: 1, x: 0, y: 0, z: 0))
|
|
165
247
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
248
|
+
return Promise.resolved(
|
|
249
|
+
withResult: Quaternion(
|
|
250
|
+
w: b.readViewCorrectionW(),
|
|
251
|
+
x: b.readViewCorrectionX(),
|
|
252
|
+
y: b.readViewCorrectionY(),
|
|
253
|
+
z: b.readViewCorrectionZ()
|
|
254
|
+
)
|
|
171
255
|
)
|
|
172
|
-
return Promise.resolved(withResult: q)
|
|
173
256
|
}
|
|
174
257
|
|
|
175
258
|
// MARK: - View
|
|
@@ -177,36 +260,50 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
177
260
|
private let metalView: MTKView
|
|
178
261
|
private var bridge: CesiumBridge?
|
|
179
262
|
private var displayLink: CADisplayLink?
|
|
263
|
+
private let displayLinkProxy = DisplayLinkProxy()
|
|
180
264
|
private var layoutPollTimer: Timer?
|
|
181
265
|
private var metricsFrameCounter: Int = 0
|
|
182
266
|
private var idleProbeAccumulator: Double = 0
|
|
183
267
|
private var usingLowRefreshRate = false
|
|
184
268
|
private var hasConfiguredFrameRate = false
|
|
185
|
-
private var lastPushedCameraState: CameraState?
|
|
186
|
-
/// Last quaternion pushed via `setCameraQuaternion`; `nil` if only `setCamera` has been used.
|
|
187
|
-
private var lastPushedViewCorrection: Quaternion?
|
|
188
|
-
/// Camera state requested by `setCamera` before the bridge was ready.
|
|
189
|
-
private var pendingRuntimeCamera: CameraState?
|
|
190
|
-
private var pendingRuntimeViewCorrection: Quaternion?
|
|
191
269
|
private var lastBridgePixelSize: CGSize = .zero
|
|
192
270
|
|
|
271
|
+
/// Demand values that arrived before the bridge was created. Drained once
|
|
272
|
+
/// in `ensureInitialized()` after the seed teleport.
|
|
273
|
+
private var pendingTeleport: CameraState?
|
|
274
|
+
private var pendingLat: Double?
|
|
275
|
+
private var pendingLon: Double?
|
|
276
|
+
private var pendingAltitude: Double?
|
|
277
|
+
private var pendingHeading: Double?
|
|
278
|
+
private var pendingPitch: Double?
|
|
279
|
+
private var pendingRoll: Double?
|
|
280
|
+
private var pendingVfov: Double?
|
|
281
|
+
private var pendingViewCorrection: Quaternion?
|
|
282
|
+
|
|
193
283
|
/// Dedicated serial render queue. CADisplayLink fires on `.main`, but the
|
|
194
284
|
/// actual `bridge.shouldRenderNextFrame()` / `bridge.renderFrame(...)` work
|
|
195
285
|
/// is dispatched here so the main thread is never blocked on Cesium tile
|
|
196
286
|
/// loading or Metal command-buffer encoding. All bridge prop setters also
|
|
197
287
|
/// dispatch here so they serialize with rendering and never race with
|
|
198
|
-
/// engine state.
|
|
288
|
+
/// engine state. Per-DoF camera writes deliberately bypass this hop —
|
|
289
|
+
/// CameraIntegrator's internal mutex handles cross-thread safety.
|
|
199
290
|
private let renderQueue = DispatchQueue(
|
|
200
291
|
label: "com.margelo.cesium.render",
|
|
201
292
|
qos: .userInteractive
|
|
202
293
|
)
|
|
203
294
|
/// Drops a CADisplayLink tick if the render queue is still finishing the
|
|
204
|
-
/// previous frame
|
|
205
|
-
/// stutter (Metal's frameSemaphore would already throttle, but this saves
|
|
206
|
-
/// the dispatch hop and keeps `dt` accurate).
|
|
295
|
+
/// previous frame.
|
|
207
296
|
private var renderInFlight = false
|
|
208
297
|
private let renderInFlightLock = NSLock()
|
|
209
298
|
|
|
299
|
+
/// Set to true at the start of `deinit`. Any work still queued on the
|
|
300
|
+
/// render or main queues checks this before touching the bridge or
|
|
301
|
+
/// invoking JS callbacks. Avoids two classes of warnings during fast
|
|
302
|
+
/// refresh / unmount: Nitro "Dispatcher has already been destroyed"
|
|
303
|
+
/// when emitting `onMetrics` into a dying JS runtime, and stray frame
|
|
304
|
+
/// ticks running after the bridge has been torn down.
|
|
305
|
+
private var isShutdown = false
|
|
306
|
+
|
|
210
307
|
var view: UIView { metalView }
|
|
211
308
|
|
|
212
309
|
override init() {
|
|
@@ -270,17 +367,29 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
270
367
|
if !ionAccessToken.isEmpty {
|
|
271
368
|
bridge?.updateIonAccessToken(ionAccessToken, assetId: Int64(ionAssetId))
|
|
272
369
|
}
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
370
|
+
// Seed from initialCamera, then replay anything the consumer set before
|
|
371
|
+
// we were ready.
|
|
372
|
+
let seed = pendingTeleport ?? initialCamera
|
|
373
|
+
bridge?.teleportLatitude(seed.latitude,
|
|
374
|
+
longitude: seed.longitude,
|
|
375
|
+
altitude: seed.altitude,
|
|
376
|
+
heading: seed.heading,
|
|
377
|
+
pitch: seed.pitch,
|
|
378
|
+
roll: seed.roll,
|
|
379
|
+
verticalFovDeg: seed.verticalFovDeg)
|
|
380
|
+
pendingTeleport = nil
|
|
381
|
+
|
|
382
|
+
if let la = pendingLat, let lo = pendingLon {
|
|
383
|
+
bridge?.setPositionLatitude(la, longitude: lo); pendingLat = nil; pendingLon = nil
|
|
283
384
|
}
|
|
385
|
+
if let a = pendingAltitude { bridge?.setAltitude(a); pendingAltitude = nil }
|
|
386
|
+
if let h = pendingHeading { bridge?.setHeadingDeg(h); pendingHeading = nil }
|
|
387
|
+
if let p = pendingPitch, let r = pendingRoll {
|
|
388
|
+
bridge?.setAttitudePitch(p, roll: r); pendingPitch = nil; pendingRoll = nil
|
|
389
|
+
}
|
|
390
|
+
if let f = pendingVfov { bridge?.setVerticalFovDeg(f); pendingVfov = nil }
|
|
391
|
+
if let q = pendingViewCorrection { bridge?.setViewCorrectionW(q.w, x: q.x, y: q.y, z: q.z); pendingViewCorrection = nil }
|
|
392
|
+
|
|
284
393
|
if ionImageryAssetId != 1 {
|
|
285
394
|
bridge?.updateImageryAssetId(Int64(ionImageryAssetId))
|
|
286
395
|
}
|
|
@@ -289,6 +398,8 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
289
398
|
bridge?.setMsaaSampleCount(sc)
|
|
290
399
|
applyMtkViewMsaa(Int(sc))
|
|
291
400
|
|
|
401
|
+
pushRateCaps()
|
|
402
|
+
|
|
292
403
|
startRenderLoop()
|
|
293
404
|
}
|
|
294
405
|
|
|
@@ -325,69 +436,14 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
325
436
|
if let v = minAltitudeAboveTerrain { bridge?.setMinAltitudeAboveTerrain(Float(v)) }
|
|
326
437
|
}
|
|
327
438
|
|
|
328
|
-
/// Pushes camera state into native bridge only when values changed.
|
|
329
|
-
private func pushCameraStateIfChanged(_ camera: CameraState) {
|
|
330
|
-
guard bridge != nil else { return }
|
|
331
|
-
if let last = lastPushedCameraState,
|
|
332
|
-
last.latitude == camera.latitude,
|
|
333
|
-
last.longitude == camera.longitude,
|
|
334
|
-
last.altitude == camera.altitude,
|
|
335
|
-
last.heading == camera.heading,
|
|
336
|
-
last.pitch == camera.pitch,
|
|
337
|
-
last.roll == camera.roll,
|
|
338
|
-
last.verticalFovDeg == camera.verticalFovDeg {
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
lastPushedCameraState = camera
|
|
342
|
-
bridge?.updateCameraLatitude(
|
|
343
|
-
camera.latitude,
|
|
344
|
-
longitude: camera.longitude,
|
|
345
|
-
altitude: camera.altitude,
|
|
346
|
-
heading: camera.heading,
|
|
347
|
-
pitch: camera.pitch,
|
|
348
|
-
roll: camera.roll
|
|
349
|
-
)
|
|
350
|
-
bridge?.setVerticalFovDeg(camera.verticalFovDeg)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private func pushCameraQuaternionIfChanged(_ camera: CameraState, viewCorrection: Quaternion) {
|
|
354
|
-
guard bridge != nil else { return }
|
|
355
|
-
if let lastCam = lastPushedCameraState,
|
|
356
|
-
let lastQ = lastPushedViewCorrection,
|
|
357
|
-
lastCam.latitude == camera.latitude,
|
|
358
|
-
lastCam.longitude == camera.longitude,
|
|
359
|
-
lastCam.altitude == camera.altitude,
|
|
360
|
-
lastCam.heading == camera.heading,
|
|
361
|
-
lastCam.pitch == camera.pitch,
|
|
362
|
-
lastCam.roll == camera.roll,
|
|
363
|
-
lastCam.verticalFovDeg == camera.verticalFovDeg,
|
|
364
|
-
lastQ.w == viewCorrection.w,
|
|
365
|
-
lastQ.x == viewCorrection.x,
|
|
366
|
-
lastQ.y == viewCorrection.y,
|
|
367
|
-
lastQ.z == viewCorrection.z {
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
lastPushedCameraState = camera
|
|
371
|
-
lastPushedViewCorrection = viewCorrection
|
|
372
|
-
bridge?.updateCameraQuaternionLatitude(
|
|
373
|
-
camera.latitude,
|
|
374
|
-
longitude: camera.longitude,
|
|
375
|
-
altitude: camera.altitude,
|
|
376
|
-
heading: camera.heading,
|
|
377
|
-
pitch: camera.pitch,
|
|
378
|
-
roll: camera.roll,
|
|
379
|
-
viewCorrectionW: viewCorrection.w,
|
|
380
|
-
x: viewCorrection.x,
|
|
381
|
-
y: viewCorrection.y,
|
|
382
|
-
z: viewCorrection.z
|
|
383
|
-
)
|
|
384
|
-
bridge?.setVerticalFovDeg(camera.verticalFovDeg)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
439
|
// MARK: - Render Loop
|
|
388
440
|
|
|
389
441
|
private func startRenderLoop() {
|
|
390
|
-
|
|
442
|
+
displayLinkProxy.owner = self
|
|
443
|
+
displayLink = CADisplayLink(
|
|
444
|
+
target: displayLinkProxy,
|
|
445
|
+
selector: #selector(DisplayLinkProxy.tick)
|
|
446
|
+
)
|
|
391
447
|
setDisplayLinkFrameRate(idle: false)
|
|
392
448
|
displayLink?.add(to: .main, forMode: .common)
|
|
393
449
|
displayLink?.isPaused = pauseRendering
|
|
@@ -403,13 +459,12 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
403
459
|
: CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60)
|
|
404
460
|
}
|
|
405
461
|
|
|
406
|
-
|
|
462
|
+
/// Invoked by `DisplayLinkProxy` on every CADisplayLink tick (main thread).
|
|
463
|
+
fileprivate func handleDisplayLinkTick() {
|
|
407
464
|
guard !pauseRendering,
|
|
408
|
-
|
|
465
|
+
!isShutdown,
|
|
409
466
|
let bridge else { return }
|
|
410
467
|
|
|
411
|
-
let dt = max(dl.targetTimestamp - dl.timestamp, 1.0 / 120.0)
|
|
412
|
-
|
|
413
468
|
// metalView.bounds / drawableSize are UIKit, must stay on main.
|
|
414
469
|
let scale = metalView.contentScaleFactor
|
|
415
470
|
let w = Int(metalView.bounds.width * scale)
|
|
@@ -424,9 +479,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
424
479
|
}
|
|
425
480
|
}
|
|
426
481
|
|
|
427
|
-
//
|
|
428
|
-
// frame yet. This keeps the dispatch backlog at zero and lets the next
|
|
429
|
-
// CADisplayLink fire pick up a fresh dt.
|
|
482
|
+
// Drop the tick if the render queue hasn't finished the previous frame yet.
|
|
430
483
|
renderInFlightLock.lock()
|
|
431
484
|
if renderInFlight {
|
|
432
485
|
renderInFlightLock.unlock()
|
|
@@ -435,16 +488,24 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
435
488
|
renderInFlight = true
|
|
436
489
|
renderInFlightLock.unlock()
|
|
437
490
|
|
|
491
|
+
// Wall-clock time used for the metrics-only frame-dt EMA inside the
|
|
492
|
+
// bridge. The camera integrator owns its own clock — see
|
|
493
|
+
// CameraIntegrator::step().
|
|
494
|
+
let nowSeconds = CACurrentMediaTime()
|
|
495
|
+
|
|
438
496
|
let resizedW = w
|
|
439
497
|
let resizedH = h
|
|
440
|
-
let weakSelf = self // captured for main-thread post-frame work
|
|
441
498
|
renderQueue.async { [weak self, weak bridge] in
|
|
442
|
-
guard let self = self
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
499
|
+
guard let self = self else { return }
|
|
500
|
+
// `renderInFlight` must always be cleared so a single dropped tick
|
|
501
|
+
// doesn't permanently wedge the render loop. defer guarantees it
|
|
502
|
+
// regardless of which early-return path we take below.
|
|
503
|
+
defer {
|
|
504
|
+
self.renderInFlightLock.lock()
|
|
505
|
+
self.renderInFlight = false
|
|
506
|
+
self.renderInFlightLock.unlock()
|
|
447
507
|
}
|
|
508
|
+
guard let bridge = bridge else { return }
|
|
448
509
|
|
|
449
510
|
if didResize {
|
|
450
511
|
bridge.resize(Int32(resizedW), height: Int32(resizedH))
|
|
@@ -455,12 +516,13 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
455
516
|
let nextIsIdle: Bool
|
|
456
517
|
if shouldRenderNow {
|
|
457
518
|
self.idleProbeAccumulator = 0
|
|
458
|
-
bridge.renderFrame(
|
|
519
|
+
bridge.renderFrame(at: nowSeconds)
|
|
459
520
|
nextIsIdle = false
|
|
460
521
|
} else {
|
|
461
|
-
|
|
522
|
+
// Approximate dt for the idle probe. We don't need precision here.
|
|
523
|
+
let probeDt = 1.0 / 60.0
|
|
524
|
+
self.idleProbeAccumulator += probeDt
|
|
462
525
|
if self.idleProbeAccumulator >= 0.25 {
|
|
463
|
-
// Safety probe: occasionally tick engine state for late tile completions.
|
|
464
526
|
self.idleProbeAccumulator = 0
|
|
465
527
|
bridge.markNeedsRender()
|
|
466
528
|
}
|
|
@@ -469,7 +531,7 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
469
531
|
|
|
470
532
|
self.metricsFrameCounter += 1
|
|
471
533
|
let shouldEmitMetrics =
|
|
472
|
-
self.metricsFrameCounter >= 20 && self.onMetrics != nil
|
|
534
|
+
self.metricsFrameCounter >= 20 && self.onMetrics != nil && !self.isShutdown
|
|
473
535
|
let metricsSnapshot: CesiumMetrics? = shouldEmitMetrics ? CesiumMetrics(
|
|
474
536
|
fps: bridge.metricsFps,
|
|
475
537
|
tilesRendered: Double(bridge.metricsTilesRendered),
|
|
@@ -482,32 +544,38 @@ class HybridCesiumView: HybridCesiumViewSpec {
|
|
|
482
544
|
) : nil
|
|
483
545
|
if shouldEmitMetrics { self.metricsFrameCounter = 0 }
|
|
484
546
|
|
|
485
|
-
// Hop back to main to update CADisplayLink rate (UIKit) and emit the
|
|
486
|
-
// metrics callback (consumer may touch UI from it).
|
|
487
547
|
DispatchQueue.main.async { [weak self] in
|
|
488
|
-
guard let self = self else { return }
|
|
548
|
+
guard let self = self, !self.isShutdown else { return }
|
|
489
549
|
self.setDisplayLinkFrameRate(idle: nextIsIdle)
|
|
490
550
|
if let m = metricsSnapshot, let cb = self.onMetrics {
|
|
491
551
|
cb(m)
|
|
492
552
|
}
|
|
493
553
|
}
|
|
494
|
-
|
|
495
|
-
self.renderInFlightLock.lock()
|
|
496
|
-
self.renderInFlight = false
|
|
497
|
-
self.renderInFlightLock.unlock()
|
|
554
|
+
// renderInFlight is cleared by the defer at the top of this closure.
|
|
498
555
|
}
|
|
499
556
|
}
|
|
500
557
|
|
|
501
558
|
deinit {
|
|
559
|
+
// Set the shutdown gate first so any in-flight render-queue tick and
|
|
560
|
+
// any queued main-queue follow-on (metric emission, frame-rate update)
|
|
561
|
+
// bail out before they touch the bridge or invoke onMetrics into a
|
|
562
|
+
// JS dispatcher that may already be gone.
|
|
563
|
+
isShutdown = true
|
|
564
|
+
onMetrics = nil
|
|
565
|
+
|
|
502
566
|
layoutPollTimer?.invalidate()
|
|
567
|
+
layoutPollTimer = nil
|
|
503
568
|
displayLink?.invalidate()
|
|
569
|
+
displayLink = nil
|
|
570
|
+
displayLinkProxy.owner = nil
|
|
504
571
|
|
|
505
|
-
// Drain the render queue so we don't release `bridge` while it's
|
|
506
|
-
// mid-encode. `sync` here is bounded by Metal's frameSemaphore wait —
|
|
507
|
-
// which itself is already bounded to 1 s in MetalBackend::shutdown().
|
|
508
572
|
let bridgeRef = bridge
|
|
509
573
|
bridge = nil
|
|
510
574
|
if let b = bridgeRef {
|
|
575
|
+
// Drain anything already on the render queue, then ask the bridge
|
|
576
|
+
// to release its engine + Metal backend synchronously. We do this
|
|
577
|
+
// inside the sync hop so the SqliteCache file handle is closed
|
|
578
|
+
// before any subsequent CesiumView mount can open the same cache.
|
|
511
579
|
renderQueue.sync {
|
|
512
580
|
b.shutdown()
|
|
513
581
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { HybridView, HybridViewProps, HybridViewMethods } from 'react-native-nitro-modules';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot of the native camera for persistence / sync with React state.
|
|
4
|
+
* Returned by `getActualCamera()` and `getDemandCamera()`.
|
|
5
|
+
*/
|
|
3
6
|
export interface CameraState {
|
|
4
7
|
latitude: number;
|
|
5
8
|
longitude: number;
|
|
@@ -10,8 +13,9 @@ export interface CameraState {
|
|
|
10
13
|
verticalFovDeg: number;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
|
-
* Unit quaternion (w + xi + yj + zk). Used for camera-space view correction
|
|
14
|
-
* Non-unit values are
|
|
16
|
+
* Unit quaternion (w + xi + yj + zk). Used for camera-space view correction
|
|
17
|
+
* applied after HPR (boresight / HUD alignment). Non-unit values are
|
|
18
|
+
* normalised on the native side.
|
|
15
19
|
*/
|
|
16
20
|
export interface Quaternion {
|
|
17
21
|
w: number;
|
|
@@ -34,7 +38,11 @@ export interface CesiumMetrics {
|
|
|
34
38
|
export interface CesiumViewProps extends HybridViewProps {
|
|
35
39
|
ionAccessToken: string;
|
|
36
40
|
ionAssetId: number;
|
|
37
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Initial camera applied via `teleport(...)` once when the native bridge is
|
|
43
|
+
* created. Subsequent prop writes are ignored — use the per-DoF setters or
|
|
44
|
+
* `teleport(...)` for runtime updates.
|
|
45
|
+
*/
|
|
38
46
|
initialCamera: CameraState;
|
|
39
47
|
pauseRendering: boolean;
|
|
40
48
|
maximumScreenSpaceError: number;
|
|
@@ -74,22 +82,78 @@ export interface CesiumViewProps extends HybridViewProps {
|
|
|
74
82
|
* terrain mesh vertex to the camera nadir.
|
|
75
83
|
*/
|
|
76
84
|
minAltitudeAboveTerrain?: number;
|
|
85
|
+
/** Maximum yaw rate (degrees per second). 0 = uncapped. */
|
|
86
|
+
maxYawRateDegSec?: number;
|
|
87
|
+
/** Maximum pitch rate (degrees per second). 0 = uncapped. */
|
|
88
|
+
maxPitchRateDegSec?: number;
|
|
89
|
+
/** Maximum roll rate (degrees per second). 0 = uncapped. */
|
|
90
|
+
maxRollRateDegSec?: number;
|
|
91
|
+
/** Maximum climb / descent rate (metres per second). 0 = uncapped. */
|
|
92
|
+
maxClimbRateMps?: number;
|
|
93
|
+
/** Maximum horizontal ground speed (metres per second). 0 = uncapped. */
|
|
94
|
+
maxGroundSpeedMps?: number;
|
|
77
95
|
onMetrics?: (metrics: CesiumMetrics) => void;
|
|
78
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Per-DoF camera control. Every setter records the measurement with the
|
|
99
|
+
* native steady_clock time of arrival and feeds it into an α-β tracker
|
|
100
|
+
* (constant-velocity predictor) that owns both demand and actual state.
|
|
101
|
+
*
|
|
102
|
+
* All setters are synchronous and thread-safe. Calling them from a
|
|
103
|
+
* Reanimated worklet via a Nitro `hybridRef` is the supported path for
|
|
104
|
+
* high-frequency updates; the worklet thread takes the integrator's mutex
|
|
105
|
+
* briefly and returns immediately.
|
|
106
|
+
*
|
|
107
|
+
* Getters return Promises and should be called from the JS thread (e.g. a
|
|
108
|
+
* `useEffect` or a throttled HUD update), not from a worklet.
|
|
109
|
+
*/
|
|
79
110
|
export interface CesiumViewMethods extends HybridViewMethods {
|
|
80
|
-
/** Returns the current native camera state. */
|
|
81
|
-
getCameraState(): Promise<CameraState>;
|
|
82
111
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
112
|
+
* New geographic position demand (latitude / longitude in degrees). Use
|
|
113
|
+
* for GPS or scripted position updates. Latitude is clamped to ±90 by the
|
|
114
|
+
* integrator; longitude is continuous (no wrap normalisation) so the
|
|
115
|
+
* shortest-arc residual maths stays correct across the antimeridian.
|
|
116
|
+
*/
|
|
117
|
+
setPosition(latitude: number, longitude: number): void;
|
|
118
|
+
/** New altitude demand (metres above mean sea level). */
|
|
119
|
+
setAltitude(altitudeMeters: number): void;
|
|
120
|
+
/** New heading demand (degrees, 0 = north, increasing clockwise). */
|
|
121
|
+
setHeading(headingDeg: number): void;
|
|
122
|
+
/**
|
|
123
|
+
* New attitude demand (pitch and roll in degrees). Bundled because almost
|
|
124
|
+
* every IMU emits them together — but you can pass the previous value for
|
|
125
|
+
* either axis if you only have one of them.
|
|
126
|
+
*/
|
|
127
|
+
setAttitude(pitchDeg: number, rollDeg: number): void;
|
|
128
|
+
/**
|
|
129
|
+
* New camera-space rotation applied after HPR (unit quaternion). Use for
|
|
130
|
+
* boresight calibration or screen-fixed HUD alignment. SLERPed toward the
|
|
131
|
+
* latest demand each frame.
|
|
132
|
+
*/
|
|
133
|
+
setViewCorrection(q: Quaternion): void;
|
|
134
|
+
/** New vertical field-of-view demand (degrees). Clamped to 20..100. */
|
|
135
|
+
setVerticalFov(deg: number): void;
|
|
136
|
+
/**
|
|
137
|
+
* Atomically reset every DoF to the given camera state (value, velocity
|
|
138
|
+
* and demand all snap to the target). Bypasses the integrator entirely;
|
|
139
|
+
* use for `Fly to coordinate` actions or initial seeding from saved
|
|
140
|
+
* state.
|
|
141
|
+
*/
|
|
142
|
+
teleport(camera: CameraState): void;
|
|
143
|
+
/**
|
|
144
|
+
* Most recent camera that was rendered (post-integration). This is what
|
|
145
|
+
* the user is currently looking at and is what a HUD overlay should
|
|
146
|
+
* mirror. Differs from `getDemandCamera()` while a glide is in progress
|
|
147
|
+
* or when the terrain-floor clamp has raised the altitude.
|
|
85
148
|
*/
|
|
86
|
-
|
|
149
|
+
getActualCamera(): Promise<CameraState>;
|
|
87
150
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
151
|
+
* What the consumer last asked for (per-DoF demand). Useful for
|
|
152
|
+
* diagnostics or for showing the requested camera alongside the rendered
|
|
153
|
+
* one.
|
|
90
154
|
*/
|
|
91
|
-
|
|
92
|
-
/** Current view-correction quaternion (
|
|
155
|
+
getDemandCamera(): Promise<CameraState>;
|
|
156
|
+
/** Current view-correction quaternion (smoothed toward the latest demand). */
|
|
93
157
|
getViewCorrection(): Promise<Quaternion>;
|
|
94
158
|
}
|
|
95
159
|
export type CesiumView = HybridView<CesiumViewProps, CesiumViewMethods>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AAEnC
|
|
1
|
+
{"version":3,"file":"cesium-view.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/cesium-view.nitro.ts"],"names":[],"mappings":"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"}
|