react-native-image-stitcher 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/android/src/main/java/io/imagestitcher/rn/ARFrameContext.kt +89 -0
- package/android/src/main/java/io/imagestitcher/rn/ARFramePlugin.kt +57 -0
- package/android/src/main/java/io/imagestitcher/rn/AROverlayRenderer.kt +406 -0
- package/android/src/main/java/io/imagestitcher/rn/AROverlayStore.kt +441 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +472 -13
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +30 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +177 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +127 -0
- package/dist/camera/ARCameraView.d.ts +55 -2
- package/dist/camera/ARCameraView.js +68 -2
- package/dist/camera/Camera.d.ts +65 -2
- package/dist/camera/Camera.js +24 -6
- package/dist/camera/arOverlayController.d.ts +52 -0
- package/dist/camera/arOverlayController.js +132 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -2
- package/dist/stitching/ARFrameMeta.d.ts +49 -0
- package/dist/stitching/AROverlay.d.ts +97 -0
- package/dist/stitching/AROverlay.js +4 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +15 -8
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +22 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +14 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +117 -1
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.h +25 -0
- package/ios/Sources/RNImageStitcher/CameraFrameHostObject.mm +66 -54
- package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +284 -0
- package/ios/Sources/RNImageStitcher/RNISAROverlay.swift +409 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +281 -3
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +127 -1
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +139 -3
- package/src/camera/Camera.tsx +94 -3
- package/src/camera/arOverlayController.ts +184 -0
- package/src/index.ts +21 -1
- package/src/stitching/ARFrameMeta.ts +50 -0
- package/src/stitching/AROverlay.ts +105 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// RNISAROverlay — v0.20.0 AR overlay / annotation data model + store.
|
|
4
|
+
//
|
|
5
|
+
// An "overlay" is a 2D annotation (outline, box, or marker + label)
|
|
6
|
+
// anchored to a WORLD position (or explicit world corners) and
|
|
7
|
+
// reprojected to screen EVERY AR frame from the current camera
|
|
8
|
+
// pose+intrinsics. This file owns ONLY the data model + a process-wide
|
|
9
|
+
// store; the actual drawing + per-frame reprojection lives in
|
|
10
|
+
// `RNSARCameraView` (`AROverlayDrawView`).
|
|
11
|
+
//
|
|
12
|
+
// Mirrors the shared TS contract (`src/stitching/AROverlay.ts`):
|
|
13
|
+
//
|
|
14
|
+
// interface AROverlay {
|
|
15
|
+
// id: string;
|
|
16
|
+
// worldPosition?: [number, number, number]; // world metres
|
|
17
|
+
// sizeMeters?: [number, number]; // box extent at worldPosition
|
|
18
|
+
// worldQuad?: Array<[number, number, number]>; // 3-4 explicit world corners
|
|
19
|
+
// shape?: 'box' | 'outline'; // default 'outline'
|
|
20
|
+
// label?: string;
|
|
21
|
+
// color?: string; // hex; default theme color
|
|
22
|
+
// mode?: '2d' | '3d'; // default '2d'; '3d' SCAFFOLD only
|
|
23
|
+
// }
|
|
24
|
+
//
|
|
25
|
+
// TWO overlay namespaces, rendered as a UNION (see `AROverlayStore`):
|
|
26
|
+
// 1. JS-set overlays — declarative `overlays` prop / imperative ref
|
|
27
|
+
// methods, forwarded through the `RNSARCameraViewManager` view
|
|
28
|
+
// commands.
|
|
29
|
+
// 2. Plugin-set overlays — native plugins place overlays directly via
|
|
30
|
+
// `RNISARPluginRegistry.shared.setOverlays(...)` (zero JS latency).
|
|
31
|
+
// JS `setOverlays` never clobbers plugin overlays and vice-versa; the
|
|
32
|
+
// draw view renders both sets every frame.
|
|
33
|
+
|
|
34
|
+
import Foundation
|
|
35
|
+
import UIKit
|
|
36
|
+
import simd
|
|
37
|
+
|
|
38
|
+
// MARK: - Overlay model
|
|
39
|
+
|
|
40
|
+
/// One AR overlay annotation. Value type (struct) so snapshots handed
|
|
41
|
+
/// to the draw view are cheap, immutable copies — no shared mutable
|
|
42
|
+
/// state across the AR thread / main thread boundary.
|
|
43
|
+
public struct RNISAROverlay: Equatable {
|
|
44
|
+
|
|
45
|
+
/// Shape rendering style.
|
|
46
|
+
public enum Shape: String {
|
|
47
|
+
/// Stroked outline only (default) — a polygon connecting the
|
|
48
|
+
/// projected corners.
|
|
49
|
+
case outline
|
|
50
|
+
/// Stroked box (same as outline for a 4-corner quad; for a
|
|
51
|
+
/// `worldPosition` marker, a small square billboard).
|
|
52
|
+
case box
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Render mode. `.twoD` (default) is the only mode implemented in
|
|
56
|
+
/// v1. `.threeD` is a SCAFFOLD this release — treated as `.twoD`
|
|
57
|
+
/// with a one-time log warning; the native 3D hook in
|
|
58
|
+
/// `AROverlayDrawView` is where a SceneKit renderer will plug in.
|
|
59
|
+
public enum Mode: String {
|
|
60
|
+
case twoD = "2d"
|
|
61
|
+
case threeD = "3d"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Stable identifier. Diffed by id against the current set so a
|
|
65
|
+
/// declarative `overlays` prop / imperative update can replace one
|
|
66
|
+
/// overlay without disturbing the rest.
|
|
67
|
+
public let id: String
|
|
68
|
+
|
|
69
|
+
/// A single world point (metres, ARKit world frame). When set (and
|
|
70
|
+
/// `worldQuad` is nil), the overlay is a billboard marker/box facing
|
|
71
|
+
/// the camera at this point, sized by `sizeMeters`.
|
|
72
|
+
public let worldPosition: simd_float3?
|
|
73
|
+
|
|
74
|
+
/// Box extent (width, height) in metres at `worldPosition`. Only
|
|
75
|
+
/// meaningful with `worldPosition`. Defaults to a small marker.
|
|
76
|
+
public let sizeMeters: CGSize?
|
|
77
|
+
|
|
78
|
+
/// Explicit world corners (3-4 points, metres). When set, the
|
|
79
|
+
/// overlay is the polygon through these corners (e.g. a detected
|
|
80
|
+
/// quad). Takes precedence over `worldPosition`.
|
|
81
|
+
public let worldQuad: [simd_float3]?
|
|
82
|
+
|
|
83
|
+
/// Shape style — `.outline` (default) or `.box`.
|
|
84
|
+
public let shape: Shape
|
|
85
|
+
|
|
86
|
+
/// Optional text label drawn near the overlay's centroid.
|
|
87
|
+
public let label: String?
|
|
88
|
+
|
|
89
|
+
/// Stroke / label color. Defaults to a theme color when the JS hex
|
|
90
|
+
/// is absent / unparseable.
|
|
91
|
+
public let color: UIColor
|
|
92
|
+
|
|
93
|
+
/// Render mode (`.twoD` default; `.threeD` is scaffold-only — see
|
|
94
|
+
/// `Mode`).
|
|
95
|
+
public let mode: Mode
|
|
96
|
+
|
|
97
|
+
public init(
|
|
98
|
+
id: String,
|
|
99
|
+
worldPosition: simd_float3?,
|
|
100
|
+
sizeMeters: CGSize?,
|
|
101
|
+
worldQuad: [simd_float3]?,
|
|
102
|
+
shape: Shape,
|
|
103
|
+
label: String?,
|
|
104
|
+
color: UIColor,
|
|
105
|
+
mode: Mode
|
|
106
|
+
) {
|
|
107
|
+
self.id = id
|
|
108
|
+
self.worldPosition = worldPosition
|
|
109
|
+
self.sizeMeters = sizeMeters
|
|
110
|
+
self.worldQuad = worldQuad
|
|
111
|
+
self.shape = shape
|
|
112
|
+
self.label = label
|
|
113
|
+
self.color = color
|
|
114
|
+
self.mode = mode
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Default theme color when none / an unparseable hex is supplied.
|
|
118
|
+
/// Cyan matches the example overlay (`#00E5FF`).
|
|
119
|
+
public static let defaultColor = UIColor(
|
|
120
|
+
red: 0.0, green: 0.898, blue: 1.0, alpha: 1.0
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
/// Default marker extent (metres) for a bare `worldPosition` with no
|
|
124
|
+
/// `sizeMeters` — a ~6 cm square so a single anchor point is visible.
|
|
125
|
+
public static let defaultMarkerExtent: CGFloat = 0.06
|
|
126
|
+
|
|
127
|
+
/// Build from the JS bridge dictionary shape (the same keys the TS
|
|
128
|
+
/// `AROverlay` interface serialises to). Returns `nil` when there's
|
|
129
|
+
/// no `id` (the only required field) or no geometry at all
|
|
130
|
+
/// (`worldPosition`/`worldQuad` both missing) — a geometryless
|
|
131
|
+
/// overlay can never be drawn.
|
|
132
|
+
public static func from(dictionary dict: [String: Any]) -> RNISAROverlay? {
|
|
133
|
+
guard let id = dict["id"] as? String, !id.isEmpty else { return nil }
|
|
134
|
+
|
|
135
|
+
let worldPosition = parseVec3(dict["worldPosition"])
|
|
136
|
+
let worldQuad = parseQuad(dict["worldQuad"])
|
|
137
|
+
// Need at least one geometry source.
|
|
138
|
+
guard worldPosition != nil || (worldQuad?.isEmpty == false) else {
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let sizeMeters: CGSize? = {
|
|
143
|
+
guard let arr = dict["sizeMeters"] as? [Any], arr.count >= 2,
|
|
144
|
+
let w = numeric(arr[0]), let h = numeric(arr[1]) else {
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
return CGSize(width: w, height: h)
|
|
148
|
+
}()
|
|
149
|
+
|
|
150
|
+
let shape: Shape = {
|
|
151
|
+
if let s = dict["shape"] as? String, let parsed = Shape(rawValue: s) {
|
|
152
|
+
return parsed
|
|
153
|
+
}
|
|
154
|
+
return .outline
|
|
155
|
+
}()
|
|
156
|
+
|
|
157
|
+
let mode: Mode = {
|
|
158
|
+
if let m = dict["mode"] as? String, let parsed = Mode(rawValue: m) {
|
|
159
|
+
return parsed
|
|
160
|
+
}
|
|
161
|
+
return .twoD
|
|
162
|
+
}()
|
|
163
|
+
|
|
164
|
+
let color: UIColor = {
|
|
165
|
+
if let hex = dict["color"] as? String,
|
|
166
|
+
let parsed = UIColor(hexString: hex) {
|
|
167
|
+
return parsed
|
|
168
|
+
}
|
|
169
|
+
return defaultColor
|
|
170
|
+
}()
|
|
171
|
+
|
|
172
|
+
let label = dict["label"] as? String
|
|
173
|
+
|
|
174
|
+
return RNISAROverlay(
|
|
175
|
+
id: id,
|
|
176
|
+
worldPosition: worldPosition,
|
|
177
|
+
sizeMeters: sizeMeters,
|
|
178
|
+
worldQuad: (worldQuad?.isEmpty == false) ? worldQuad : nil,
|
|
179
|
+
shape: shape,
|
|
180
|
+
label: label,
|
|
181
|
+
color: color,
|
|
182
|
+
mode: mode
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// MARK: Dictionary parsing helpers
|
|
187
|
+
|
|
188
|
+
private static func numeric(_ v: Any?) -> CGFloat? {
|
|
189
|
+
if let n = v as? NSNumber { return CGFloat(n.doubleValue) }
|
|
190
|
+
if let d = v as? Double { return CGFloat(d) }
|
|
191
|
+
if let i = v as? Int { return CGFloat(i) }
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private static func parseVec3(_ v: Any?) -> simd_float3? {
|
|
196
|
+
guard let arr = v as? [Any], arr.count >= 3,
|
|
197
|
+
let x = numeric(arr[0]), let y = numeric(arr[1]),
|
|
198
|
+
let z = numeric(arr[2]) else {
|
|
199
|
+
return nil
|
|
200
|
+
}
|
|
201
|
+
return simd_float3(Float(x), Float(y), Float(z))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private static func parseQuad(_ v: Any?) -> [simd_float3]? {
|
|
205
|
+
guard let arr = v as? [Any] else { return nil }
|
|
206
|
+
var pts: [simd_float3] = []
|
|
207
|
+
pts.reserveCapacity(arr.count)
|
|
208
|
+
for item in arr {
|
|
209
|
+
guard let p = parseVec3(item) else { continue }
|
|
210
|
+
pts.append(p)
|
|
211
|
+
}
|
|
212
|
+
// A polygon needs at least 3 corners; cap at 4 (the contract's
|
|
213
|
+
// "3-4 points").
|
|
214
|
+
guard pts.count >= 3 else { return nil }
|
|
215
|
+
return Array(pts.prefix(4))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
// MARK: - UIColor hex parsing
|
|
221
|
+
|
|
222
|
+
extension UIColor {
|
|
223
|
+
/// Parse a CSS-style hex string: `#RGB`, `#RGBA`, `#RRGGBB`, or
|
|
224
|
+
/// `#RRGGBBAA` (the leading `#` is optional). Returns `nil` for
|
|
225
|
+
/// anything unparseable so the caller can fall back to a theme color.
|
|
226
|
+
public convenience init?(hexString raw: String) {
|
|
227
|
+
var s = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
228
|
+
if s.hasPrefix("#") { s.removeFirst() }
|
|
229
|
+
// Expand shorthand #RGB / #RGBA to #RRGGBB / #RRGGBBAA.
|
|
230
|
+
if s.count == 3 || s.count == 4 {
|
|
231
|
+
s = s.map { "\($0)\($0)" }.joined()
|
|
232
|
+
}
|
|
233
|
+
guard s.count == 6 || s.count == 8,
|
|
234
|
+
let value = UInt64(s, radix: 16) else {
|
|
235
|
+
return nil
|
|
236
|
+
}
|
|
237
|
+
let r, g, b, a: CGFloat
|
|
238
|
+
if s.count == 8 {
|
|
239
|
+
r = CGFloat((value & 0xFF00_0000) >> 24) / 255.0
|
|
240
|
+
g = CGFloat((value & 0x00FF_0000) >> 16) / 255.0
|
|
241
|
+
b = CGFloat((value & 0x0000_FF00) >> 8) / 255.0
|
|
242
|
+
a = CGFloat(value & 0x0000_00FF) / 255.0
|
|
243
|
+
} else {
|
|
244
|
+
r = CGFloat((value & 0xFF0000) >> 16) / 255.0
|
|
245
|
+
g = CGFloat((value & 0x00FF00) >> 8) / 255.0
|
|
246
|
+
b = CGFloat(value & 0x0000FF) / 255.0
|
|
247
|
+
a = 1.0
|
|
248
|
+
}
|
|
249
|
+
self.init(red: r, green: g, blue: b, alpha: a)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
// MARK: - Overlay store
|
|
255
|
+
|
|
256
|
+
/// Process-wide store of active overlays, split into two namespaces:
|
|
257
|
+
/// JS-set (declarative prop / imperative ref) and plugin-set (native
|
|
258
|
+
/// plugins via `RNISARPluginRegistry`). Mounted `RNSARCameraView`s
|
|
259
|
+
/// subscribe (via `addObserver`) and re-read `snapshot()` to redraw.
|
|
260
|
+
///
|
|
261
|
+
/// THREAD SAFETY: mutations come from the bridge / main thread (JS
|
|
262
|
+
/// commands) and arbitrary plugin queues (`setOverlays`); reads come from
|
|
263
|
+
/// the main thread (the draw view). All access is serialised by an
|
|
264
|
+
/// internal lock; `snapshot()` returns a value-type array copy so the
|
|
265
|
+
/// reader never holds the lock while drawing.
|
|
266
|
+
@objc(RNISAROverlayStore)
|
|
267
|
+
public final class RNISAROverlayStore: NSObject {
|
|
268
|
+
|
|
269
|
+
/// Shared instance — singleton because the overlay set is global to
|
|
270
|
+
/// the (single) AR session, matching `RNSARSession.shared`.
|
|
271
|
+
@objc public static let shared = RNISAROverlayStore()
|
|
272
|
+
|
|
273
|
+
/// Notification posted (on any thread) whenever either namespace
|
|
274
|
+
/// changes. Mounted draw views observe it to mark themselves dirty.
|
|
275
|
+
/// (Used for the imperative / plugin paths; the per-frame redraw in
|
|
276
|
+
/// `RNSARCameraView` already refreshes geometry every frame, so this
|
|
277
|
+
/// is mainly to pick up SET changes between frames.)
|
|
278
|
+
public static let overlaysChanged =
|
|
279
|
+
Notification.Name("RNISAROverlaysChanged")
|
|
280
|
+
|
|
281
|
+
/// JS-set overlays, keyed by id (last-write-wins per id). Ordered
|
|
282
|
+
/// list preserved separately for deterministic draw order.
|
|
283
|
+
private var jsById: [String: RNISAROverlay] = [:]
|
|
284
|
+
private var jsOrder: [String] = []
|
|
285
|
+
|
|
286
|
+
/// Plugin-set overlays, same structure, separate namespace.
|
|
287
|
+
private var pluginById: [String: RNISAROverlay] = [:]
|
|
288
|
+
private var pluginOrder: [String] = []
|
|
289
|
+
|
|
290
|
+
private let lock = NSLock()
|
|
291
|
+
|
|
292
|
+
private override init() { super.init() }
|
|
293
|
+
|
|
294
|
+
// MARK: JS namespace
|
|
295
|
+
|
|
296
|
+
/// Replace the ENTIRE JS overlay set (the declarative `overlays`
|
|
297
|
+
/// prop / imperative `setOverlays`). Plugin overlays are untouched.
|
|
298
|
+
public func setJSOverlays(_ overlays: [RNISAROverlay]) {
|
|
299
|
+
lock.lock()
|
|
300
|
+
jsById.removeAll(keepingCapacity: true)
|
|
301
|
+
jsOrder.removeAll(keepingCapacity: true)
|
|
302
|
+
for o in overlays {
|
|
303
|
+
if jsById[o.id] == nil { jsOrder.append(o.id) }
|
|
304
|
+
jsById[o.id] = o
|
|
305
|
+
}
|
|
306
|
+
lock.unlock()
|
|
307
|
+
postChanged()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Add or replace a single JS overlay (imperative `addOverlay`).
|
|
311
|
+
public func addJSOverlay(_ overlay: RNISAROverlay) {
|
|
312
|
+
lock.lock()
|
|
313
|
+
if jsById[overlay.id] == nil { jsOrder.append(overlay.id) }
|
|
314
|
+
jsById[overlay.id] = overlay
|
|
315
|
+
lock.unlock()
|
|
316
|
+
postChanged()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// Remove a single JS overlay by id (imperative `removeOverlay`).
|
|
320
|
+
/// No-op if absent.
|
|
321
|
+
public func removeJSOverlay(_ id: String) {
|
|
322
|
+
lock.lock()
|
|
323
|
+
jsById.removeValue(forKey: id)
|
|
324
|
+
jsOrder.removeAll { $0 == id }
|
|
325
|
+
lock.unlock()
|
|
326
|
+
postChanged()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// Clear ALL JS overlays (imperative `clearOverlays`). Plugin
|
|
330
|
+
/// overlays are untouched.
|
|
331
|
+
public func clearJSOverlays() {
|
|
332
|
+
lock.lock()
|
|
333
|
+
jsById.removeAll(keepingCapacity: true)
|
|
334
|
+
jsOrder.removeAll(keepingCapacity: true)
|
|
335
|
+
lock.unlock()
|
|
336
|
+
postChanged()
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// MARK: Plugin namespace
|
|
340
|
+
|
|
341
|
+
/// Replace the ENTIRE plugin overlay set. JS overlays are untouched.
|
|
342
|
+
public func setPluginOverlays(_ overlays: [RNISAROverlay]) {
|
|
343
|
+
lock.lock()
|
|
344
|
+
pluginById.removeAll(keepingCapacity: true)
|
|
345
|
+
pluginOrder.removeAll(keepingCapacity: true)
|
|
346
|
+
for o in overlays {
|
|
347
|
+
if pluginById[o.id] == nil { pluginOrder.append(o.id) }
|
|
348
|
+
pluginById[o.id] = o
|
|
349
|
+
}
|
|
350
|
+
lock.unlock()
|
|
351
|
+
postChanged()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Add or replace a single plugin overlay.
|
|
355
|
+
public func addPluginOverlay(_ overlay: RNISAROverlay) {
|
|
356
|
+
lock.lock()
|
|
357
|
+
if pluginById[overlay.id] == nil { pluginOrder.append(overlay.id) }
|
|
358
|
+
pluginById[overlay.id] = overlay
|
|
359
|
+
lock.unlock()
|
|
360
|
+
postChanged()
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Remove a single plugin overlay by id. No-op if absent.
|
|
364
|
+
public func removePluginOverlay(_ id: String) {
|
|
365
|
+
lock.lock()
|
|
366
|
+
pluginById.removeValue(forKey: id)
|
|
367
|
+
pluginOrder.removeAll { $0 == id }
|
|
368
|
+
lock.unlock()
|
|
369
|
+
postChanged()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// Clear ALL plugin overlays. JS overlays are untouched.
|
|
373
|
+
public func clearPluginOverlays() {
|
|
374
|
+
lock.lock()
|
|
375
|
+
pluginById.removeAll(keepingCapacity: true)
|
|
376
|
+
pluginOrder.removeAll(keepingCapacity: true)
|
|
377
|
+
lock.unlock()
|
|
378
|
+
postChanged()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// MARK: Read
|
|
382
|
+
|
|
383
|
+
/// Snapshot of the UNION of both namespaces, in draw order (JS first,
|
|
384
|
+
/// then plugin). Returns a value-type array copy so the caller can
|
|
385
|
+
/// iterate + draw without holding the lock.
|
|
386
|
+
public func snapshot() -> [RNISAROverlay] {
|
|
387
|
+
lock.lock()
|
|
388
|
+
defer { lock.unlock() }
|
|
389
|
+
var out: [RNISAROverlay] = []
|
|
390
|
+
out.reserveCapacity(jsOrder.count + pluginOrder.count)
|
|
391
|
+
for id in jsOrder { if let o = jsById[id] { out.append(o) } }
|
|
392
|
+
for id in pluginOrder { if let o = pluginById[id] { out.append(o) } }
|
|
393
|
+
return out
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// Whether there are no overlays in either namespace. Cheap gate the
|
|
397
|
+
/// draw view can check to skip work.
|
|
398
|
+
public var isEmpty: Bool {
|
|
399
|
+
lock.lock()
|
|
400
|
+
defer { lock.unlock() }
|
|
401
|
+
return jsOrder.isEmpty && pluginOrder.isEmpty
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private func postChanged() {
|
|
405
|
+
NotificationCenter.default.post(
|
|
406
|
+
name: Self.overlaysChanged, object: nil
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
}
|