react-native-image-stitcher 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -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 +290 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +30 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +68 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +99 -0
- package/dist/camera/ARCameraView.d.ts +33 -1
- package/dist/camera/ARCameraView.js +33 -2
- package/dist/camera/Camera.d.ts +45 -1
- 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 +4 -1
- package/dist/index.js +5 -2
- 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 +81 -0
- package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +37 -0
- package/ios/Sources/RNImageStitcher/RNISAROverlay.swift +409 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +301 -3
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +70 -65
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +73 -2
- package/src/camera/Camera.tsx +71 -2
- package/src/camera/arOverlayController.ts +184 -0
- package/src/index.ts +15 -0
- package/src/stitching/AROverlay.ts +105 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
package io.imagestitcher.rn
|
|
3
|
+
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
|
+
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
import com.facebook.react.bridge.ReadableType
|
|
8
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 0.20.0 — Android twin of the shared `AROverlay` data model
|
|
12
|
+
* (TS: `src/stitching/AROverlay.ts`; iOS: `AROverlay.swift`).
|
|
13
|
+
*
|
|
14
|
+
* One immutable description of a 2D annotation to render ON TOP of the AR
|
|
15
|
+
* camera preview, anchored to a WORLD position (or explicit world corners)
|
|
16
|
+
* and reprojected to screen every AR frame from the live camera pose +
|
|
17
|
+
* intrinsics (see [AROverlayRenderer]).
|
|
18
|
+
*
|
|
19
|
+
* Mirrors the TS contract EXACTLY:
|
|
20
|
+
*
|
|
21
|
+
* interface AROverlay {
|
|
22
|
+
* id: string;
|
|
23
|
+
* worldPosition?: [x, y, z]; // world metres (billboard marker/box)
|
|
24
|
+
* sizeMeters?: [w, h]; // box extent at worldPosition
|
|
25
|
+
* worldQuad?: Array<[x, y, z]>; // 3-4 explicit world corners
|
|
26
|
+
* shape?: 'box' | 'outline'; // default 'outline'
|
|
27
|
+
* label?: string;
|
|
28
|
+
* color?: string; // hex; default a theme colour
|
|
29
|
+
* mode?: '2d' | '3d'; // default '2d'; '3d' is SCAFFOLD ONLY
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* A valid overlay carries EITHER a [worldPosition] (with optional
|
|
33
|
+
* [sizeMeters]) OR a [worldQuad] of 3-4 corners. [worldQuad] wins when
|
|
34
|
+
* both are present (the renderer projects the explicit corners directly).
|
|
35
|
+
*
|
|
36
|
+
* PUBLIC because it is part of the native-plugin SPI: a host app constructs
|
|
37
|
+
* [AROverlayData] instances and hands them to [RNSARPluginRegistry.setOverlays]
|
|
38
|
+
* (native→native overlay placement). JS callers never touch this type — they
|
|
39
|
+
* pass plain maps via the bridge, parsed by [fromReadableMap].
|
|
40
|
+
*/
|
|
41
|
+
data class AROverlayData(
|
|
42
|
+
val id: String,
|
|
43
|
+
/// Single world anchor point [x,y,z] in metres, or null when a
|
|
44
|
+
/// [worldQuad] is supplied instead.
|
|
45
|
+
val worldPosition: FloatArray?,
|
|
46
|
+
/// Box extent [width, height] in metres at [worldPosition]; defaults
|
|
47
|
+
/// to a small marker ([DEFAULT_MARKER_SIZE_M]) when omitted.
|
|
48
|
+
val sizeMeters: FloatArray,
|
|
49
|
+
/// Explicit world corners (3-4 points, each [x,y,z]), or null when a
|
|
50
|
+
/// [worldPosition] is supplied instead.
|
|
51
|
+
val worldQuad: Array<FloatArray>?,
|
|
52
|
+
/// 'box' (filled translucent + stroked) or 'outline' (stroked only).
|
|
53
|
+
val shape: String,
|
|
54
|
+
/// Optional text drawn near the overlay's screen centroid.
|
|
55
|
+
val label: String?,
|
|
56
|
+
/// ARGB int parsed from the hex `color` string (default theme cyan).
|
|
57
|
+
val colorArgb: Int,
|
|
58
|
+
/// '2d' (rendered) or '3d' (SCAFFOLD ONLY this release — treated as
|
|
59
|
+
/// '2d' with a one-time warning; see [AROverlayRenderer]).
|
|
60
|
+
val mode: String,
|
|
61
|
+
) {
|
|
62
|
+
// data class with array fields — override equals/hashCode by `id` only.
|
|
63
|
+
// Identity for diffing the declarative `overlays` prop / imperative set
|
|
64
|
+
// is purely by `id` (the contract diffs "by id"); two overlays with the
|
|
65
|
+
// same id are considered the same slot regardless of field contents.
|
|
66
|
+
override fun equals(other: Any?): Boolean =
|
|
67
|
+
this === other || (other is AROverlayData && other.id == id)
|
|
68
|
+
|
|
69
|
+
override fun hashCode(): Int = id.hashCode()
|
|
70
|
+
|
|
71
|
+
companion object {
|
|
72
|
+
/// Default box extent (metres) for a [worldPosition]-only overlay
|
|
73
|
+
/// with no [sizeMeters] — a small ~6 cm marker.
|
|
74
|
+
const val DEFAULT_MARKER_SIZE_M = 0.06f
|
|
75
|
+
|
|
76
|
+
/// Default overlay colour when `color` is absent / unparseable —
|
|
77
|
+
/// the theme cyan used across the SDK's AR UI.
|
|
78
|
+
const val DEFAULT_COLOR_ARGB = 0xFF00E5FF.toInt()
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse one [ReadableMap] (the JS `AROverlay` shape) into an
|
|
82
|
+
* [AROverlayData], or null when it carries neither a usable
|
|
83
|
+
* `worldPosition` nor a 3-4-point `worldQuad`, or has no `id`.
|
|
84
|
+
*
|
|
85
|
+
* Defensive on every field — a malformed entry is dropped (returns
|
|
86
|
+
* null) rather than crashing the bridge call.
|
|
87
|
+
*/
|
|
88
|
+
fun fromReadableMap(map: ReadableMap?): AROverlayData? {
|
|
89
|
+
if (map == null) return null
|
|
90
|
+
val id = if (map.hasKey("id") && map.getType("id") == ReadableType.String)
|
|
91
|
+
map.getString("id") else null
|
|
92
|
+
if (id.isNullOrEmpty()) return null
|
|
93
|
+
|
|
94
|
+
val worldQuad = readQuad(map)
|
|
95
|
+
val worldPosition = if (worldQuad == null) readVec3(map, "worldPosition") else null
|
|
96
|
+
// Must anchor to SOMETHING — drop entries with neither.
|
|
97
|
+
if (worldQuad == null && worldPosition == null) return null
|
|
98
|
+
|
|
99
|
+
val sizeMeters = readVec2(map, "sizeMeters")
|
|
100
|
+
?: floatArrayOf(DEFAULT_MARKER_SIZE_M, DEFAULT_MARKER_SIZE_M)
|
|
101
|
+
|
|
102
|
+
val shape = if (map.hasKey("shape") && map.getType("shape") == ReadableType.String) {
|
|
103
|
+
when (map.getString("shape")) {
|
|
104
|
+
"box" -> "box"
|
|
105
|
+
else -> "outline"
|
|
106
|
+
}
|
|
107
|
+
} else "outline"
|
|
108
|
+
|
|
109
|
+
val label = if (map.hasKey("label") && map.getType("label") == ReadableType.String)
|
|
110
|
+
map.getString("label") else null
|
|
111
|
+
|
|
112
|
+
val colorArgb = if (map.hasKey("color") && map.getType("color") == ReadableType.String)
|
|
113
|
+
parseColor(map.getString("color")) else DEFAULT_COLOR_ARGB
|
|
114
|
+
|
|
115
|
+
val mode = if (map.hasKey("mode") && map.getType("mode") == ReadableType.String) {
|
|
116
|
+
when (map.getString("mode")) {
|
|
117
|
+
"3d" -> "3d"
|
|
118
|
+
else -> "2d"
|
|
119
|
+
}
|
|
120
|
+
} else "2d"
|
|
121
|
+
|
|
122
|
+
return AROverlayData(
|
|
123
|
+
id = id,
|
|
124
|
+
worldPosition = worldPosition,
|
|
125
|
+
sizeMeters = sizeMeters,
|
|
126
|
+
worldQuad = worldQuad,
|
|
127
|
+
shape = shape,
|
|
128
|
+
label = label,
|
|
129
|
+
colorArgb = colorArgb,
|
|
130
|
+
mode = mode,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Parse a whole [ReadableArray] of overlay maps, dropping any
|
|
135
|
+
/// malformed entries. Returns an empty list for a null/empty array.
|
|
136
|
+
fun fromReadableArray(arr: ReadableArray?): List<AROverlayData> {
|
|
137
|
+
if (arr == null || arr.size() == 0) return emptyList()
|
|
138
|
+
val out = ArrayList<AROverlayData>(arr.size())
|
|
139
|
+
for (i in 0 until arr.size()) {
|
|
140
|
+
if (arr.getType(i) != ReadableType.Map) continue
|
|
141
|
+
fromReadableMap(arr.getMap(i))?.let { out.add(it) }
|
|
142
|
+
}
|
|
143
|
+
return out
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Read a length-3 number array under [key] → FloatArray[3], or null.
|
|
147
|
+
private fun readVec3(map: ReadableMap, key: String): FloatArray? {
|
|
148
|
+
if (!map.hasKey(key) || map.getType(key) != ReadableType.Array) return null
|
|
149
|
+
val a = map.getArray(key) ?: return null
|
|
150
|
+
if (a.size() < 3) return null
|
|
151
|
+
return floatArrayOf(
|
|
152
|
+
a.getDouble(0).toFloat(),
|
|
153
|
+
a.getDouble(1).toFloat(),
|
|
154
|
+
a.getDouble(2).toFloat(),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Read a length-2 number array under [key] → FloatArray[2], or null.
|
|
159
|
+
private fun readVec2(map: ReadableMap, key: String): FloatArray? {
|
|
160
|
+
if (!map.hasKey(key) || map.getType(key) != ReadableType.Array) return null
|
|
161
|
+
val a = map.getArray(key) ?: return null
|
|
162
|
+
if (a.size() < 2) return null
|
|
163
|
+
return floatArrayOf(a.getDouble(0).toFloat(), a.getDouble(1).toFloat())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Read `worldQuad` → Array of 3-4 FloatArray[3] corners, or null
|
|
167
|
+
/// when absent / fewer than 3 valid corners.
|
|
168
|
+
private fun readQuad(map: ReadableMap): Array<FloatArray>? {
|
|
169
|
+
if (!map.hasKey("worldQuad") || map.getType("worldQuad") != ReadableType.Array) {
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
val outer = map.getArray("worldQuad") ?: return null
|
|
173
|
+
if (outer.size() < 3) return null
|
|
174
|
+
val corners = ArrayList<FloatArray>(outer.size())
|
|
175
|
+
for (i in 0 until outer.size()) {
|
|
176
|
+
if (outer.getType(i) != ReadableType.Array) continue
|
|
177
|
+
val c = outer.getArray(i) ?: continue
|
|
178
|
+
if (c.size() < 3) continue
|
|
179
|
+
corners.add(
|
|
180
|
+
floatArrayOf(
|
|
181
|
+
c.getDouble(0).toFloat(),
|
|
182
|
+
c.getDouble(1).toFloat(),
|
|
183
|
+
c.getDouble(2).toFloat(),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
// Cap at 4 corners (the contract says 3-4); ignore extras.
|
|
188
|
+
return if (corners.size < 3) null
|
|
189
|
+
else corners.take(4).toTypedArray()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse a hex colour string (`#RGB`, `#RRGGBB`, `#AARRGGBB`, or any
|
|
194
|
+
* form [Color.parseColor] accepts) → ARGB int. Falls back to the
|
|
195
|
+
* theme default on any parse failure.
|
|
196
|
+
*/
|
|
197
|
+
private fun parseColor(hex: String?): Int {
|
|
198
|
+
if (hex.isNullOrBlank()) return DEFAULT_COLOR_ARGB
|
|
199
|
+
return try {
|
|
200
|
+
Color.parseColor(hex.trim())
|
|
201
|
+
} catch (_: Throwable) {
|
|
202
|
+
DEFAULT_COLOR_ARGB
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 0.20.0 — process-wide store of [AROverlayData] split into two namespaces:
|
|
210
|
+
*
|
|
211
|
+
* - **JS overlays** — set/added/updated/removed via the JS imperative API
|
|
212
|
+
* (`RNSARSession.setOverlays(...)`) or the declarative `overlays` prop.
|
|
213
|
+
* - **Plugin overlays** — placed directly by native plugins via
|
|
214
|
+
* [RNSARPluginRegistry.setOverlays] (zero JS latency, native→native).
|
|
215
|
+
*
|
|
216
|
+
* The renderer draws the **UNION** of both sets, so a JS `setOverlays`
|
|
217
|
+
* never clobbers plugin-placed overlays and vice-versa (the spec's
|
|
218
|
+
* "namespace plugin overlays" requirement). Within a namespace, overlays
|
|
219
|
+
* are keyed by `id`; a later set/add with an existing id REPLACES it.
|
|
220
|
+
*
|
|
221
|
+
* ## Threading
|
|
222
|
+
*
|
|
223
|
+
* Both namespaces are held as immutable lists behind [AtomicReference]s;
|
|
224
|
+
* writes (JS bridge thread or a plugin's queue) swap in a fresh list,
|
|
225
|
+
* reads (the GL render thread, once per frame, via [snapshot]) take the
|
|
226
|
+
* current references. The union list is computed lazily on read. No
|
|
227
|
+
* locks; the AtomicReferences give a consistent per-namespace snapshot.
|
|
228
|
+
*
|
|
229
|
+
* A [version] counter bumps on every mutation so the renderer can cheaply
|
|
230
|
+
* detect "did the overlay SET change since I last rebuilt" without diffing
|
|
231
|
+
* lists (the per-frame reprojection always runs regardless; this is only
|
|
232
|
+
* used to skip the list rebuild when nothing changed).
|
|
233
|
+
*
|
|
234
|
+
* PUBLIC because [RNSARCameraView.overlayStore] (a public val on the public
|
|
235
|
+
* view) exposes it to the native-plugin path; host plugin code never
|
|
236
|
+
* constructs one directly (the SDK owns the single instance per view).
|
|
237
|
+
*/
|
|
238
|
+
class AROverlayStore {
|
|
239
|
+
|
|
240
|
+
private val jsOverlays = AtomicReference<List<AROverlayData>>(emptyList())
|
|
241
|
+
private val pluginOverlays = AtomicReference<List<AROverlayData>>(emptyList())
|
|
242
|
+
|
|
243
|
+
@Volatile
|
|
244
|
+
var version: Long = 0L
|
|
245
|
+
private set
|
|
246
|
+
|
|
247
|
+
private fun bump() { version++ }
|
|
248
|
+
|
|
249
|
+
// ── JS namespace ────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/// Replace the ENTIRE JS overlay set (declarative prop / imperative
|
|
252
|
+
/// `setOverlays`). Plugin overlays are untouched.
|
|
253
|
+
fun setJsOverlays(overlays: List<AROverlayData>) {
|
|
254
|
+
jsOverlays.set(overlays.toList())
|
|
255
|
+
bump()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Add or replace one JS overlay by id.
|
|
259
|
+
fun addJsOverlay(overlay: AROverlayData) {
|
|
260
|
+
jsOverlays.updateAndGet { cur -> upsert(cur, overlay) }
|
|
261
|
+
bump()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Patch one JS overlay's fields by id (no-op if the id is unknown).
|
|
265
|
+
/// `patch` carries only the fields the caller wants to change; absent
|
|
266
|
+
/// fields keep their current value.
|
|
267
|
+
fun updateJsOverlay(id: String, patch: ReadableMap) {
|
|
268
|
+
jsOverlays.updateAndGet { cur ->
|
|
269
|
+
val idx = cur.indexOfFirst { it.id == id }
|
|
270
|
+
if (idx < 0) cur
|
|
271
|
+
else {
|
|
272
|
+
val merged = applyPatch(cur[idx], patch)
|
|
273
|
+
cur.toMutableList().also { it[idx] = merged }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
bump()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Remove one JS overlay by id (no-op if unknown).
|
|
280
|
+
fun removeJsOverlay(id: String) {
|
|
281
|
+
jsOverlays.updateAndGet { cur -> cur.filterNot { it.id == id } }
|
|
282
|
+
bump()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// Clear ALL JS overlays (plugin overlays untouched).
|
|
286
|
+
fun clearJsOverlays() {
|
|
287
|
+
jsOverlays.set(emptyList())
|
|
288
|
+
bump()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Plugin namespace ────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
fun setPluginOverlays(overlays: List<AROverlayData>) {
|
|
294
|
+
pluginOverlays.set(overlays.toList())
|
|
295
|
+
bump()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fun addPluginOverlay(overlay: AROverlayData) {
|
|
299
|
+
pluginOverlays.updateAndGet { cur -> upsert(cur, overlay) }
|
|
300
|
+
bump()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fun removePluginOverlay(id: String) {
|
|
304
|
+
pluginOverlays.updateAndGet { cur -> cur.filterNot { it.id == id } }
|
|
305
|
+
bump()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fun clearPluginOverlays() {
|
|
309
|
+
pluginOverlays.set(emptyList())
|
|
310
|
+
bump()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Read ────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The UNION of JS + plugin overlays for this frame's draw. JS overlays
|
|
317
|
+
* come first, then plugin overlays; if an id collides across namespaces
|
|
318
|
+
* BOTH are kept (different namespaces own independent slots — collisions
|
|
319
|
+
* are a host bug but we don't silently drop either).
|
|
320
|
+
*/
|
|
321
|
+
fun snapshot(): List<AROverlayData> {
|
|
322
|
+
val js = jsOverlays.get()
|
|
323
|
+
val plugin = pluginOverlays.get()
|
|
324
|
+
if (js.isEmpty()) return plugin
|
|
325
|
+
if (plugin.isEmpty()) return js
|
|
326
|
+
val out = ArrayList<AROverlayData>(js.size + plugin.size)
|
|
327
|
+
out.addAll(js)
|
|
328
|
+
out.addAll(plugin)
|
|
329
|
+
return out
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// True when BOTH namespaces are empty — cheap fast-path for the
|
|
333
|
+
/// renderer / per-frame matrix snapshot to skip all work.
|
|
334
|
+
fun isEmpty(): Boolean = jsOverlays.get().isEmpty() && pluginOverlays.get().isEmpty()
|
|
335
|
+
|
|
336
|
+
companion object {
|
|
337
|
+
/// Upsert by id into an immutable list, returning a new list.
|
|
338
|
+
private fun upsert(
|
|
339
|
+
cur: List<AROverlayData>,
|
|
340
|
+
overlay: AROverlayData,
|
|
341
|
+
): List<AROverlayData> {
|
|
342
|
+
val idx = cur.indexOfFirst { it.id == overlay.id }
|
|
343
|
+
return if (idx < 0) cur + overlay
|
|
344
|
+
else cur.toMutableList().also { it[idx] = overlay }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Merge a partial `patch` map onto an existing overlay, keeping
|
|
348
|
+
/// the prior value for every field the patch omits. `id` is never
|
|
349
|
+
/// changed (the patch targets an existing id).
|
|
350
|
+
private fun applyPatch(base: AROverlayData, patch: ReadableMap): AROverlayData {
|
|
351
|
+
// Re-parse the patch as a full overlay re-using the base id, but
|
|
352
|
+
// fall back to each base field when the patch omits it. This
|
|
353
|
+
// keeps the parsing rules (vec3/quad/colour) in ONE place
|
|
354
|
+
// (AROverlayData.fromReadableMap) without duplicating them here.
|
|
355
|
+
val hasQuad = patch.hasKey("worldQuad") &&
|
|
356
|
+
patch.getType("worldQuad") == ReadableType.Array
|
|
357
|
+
val hasPos = patch.hasKey("worldPosition") &&
|
|
358
|
+
patch.getType("worldPosition") == ReadableType.Array
|
|
359
|
+
|
|
360
|
+
// If the patch supplies a new anchor (quad or position), parse it
|
|
361
|
+
// through the full parser (which also re-reads size/shape/etc from
|
|
362
|
+
// the patch where present). Otherwise patch individual fields.
|
|
363
|
+
val parsed = if (hasQuad || hasPos) {
|
|
364
|
+
AROverlayData.fromReadableMap(withId(patch, base.id))
|
|
365
|
+
} else null
|
|
366
|
+
|
|
367
|
+
if (parsed != null) {
|
|
368
|
+
// Anchor changed; for fields the patch omitted, prefer base.
|
|
369
|
+
return parsed.copy(
|
|
370
|
+
sizeMeters = if (patchHasVec2(patch, "sizeMeters")) parsed.sizeMeters else base.sizeMeters,
|
|
371
|
+
shape = if (patch.hasKey("shape")) parsed.shape else base.shape,
|
|
372
|
+
label = if (patch.hasKey("label")) parsed.label else base.label,
|
|
373
|
+
colorArgb = if (patch.hasKey("color")) parsed.colorArgb else base.colorArgb,
|
|
374
|
+
mode = if (patch.hasKey("mode")) parsed.mode else base.mode,
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// No anchor change — patch the scalar/array fields individually.
|
|
379
|
+
val size = if (patchHasVec2(patch, "sizeMeters"))
|
|
380
|
+
floatArrayOf(
|
|
381
|
+
patch.getArray("sizeMeters")!!.getDouble(0).toFloat(),
|
|
382
|
+
patch.getArray("sizeMeters")!!.getDouble(1).toFloat(),
|
|
383
|
+
)
|
|
384
|
+
else base.sizeMeters
|
|
385
|
+
|
|
386
|
+
val shape = if (patch.hasKey("shape") && patch.getType("shape") == ReadableType.String)
|
|
387
|
+
(if (patch.getString("shape") == "box") "box" else "outline")
|
|
388
|
+
else base.shape
|
|
389
|
+
|
|
390
|
+
val label = if (patch.hasKey("label")) {
|
|
391
|
+
if (patch.getType("label") == ReadableType.String) patch.getString("label") else null
|
|
392
|
+
} else base.label
|
|
393
|
+
|
|
394
|
+
val color = if (patch.hasKey("color") && patch.getType("color") == ReadableType.String)
|
|
395
|
+
AROverlayData.fromReadableMap(colorOnly(patch.getString("color")))?.colorArgb
|
|
396
|
+
?: base.colorArgb
|
|
397
|
+
else base.colorArgb
|
|
398
|
+
|
|
399
|
+
val mode = if (patch.hasKey("mode") && patch.getType("mode") == ReadableType.String)
|
|
400
|
+
(if (patch.getString("mode") == "3d") "3d" else "2d")
|
|
401
|
+
else base.mode
|
|
402
|
+
|
|
403
|
+
return base.copy(
|
|
404
|
+
sizeMeters = size,
|
|
405
|
+
shape = shape,
|
|
406
|
+
label = label,
|
|
407
|
+
colorArgb = color,
|
|
408
|
+
mode = mode,
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private fun patchHasVec2(patch: ReadableMap, key: String): Boolean {
|
|
413
|
+
if (!patch.hasKey(key) || patch.getType(key) != ReadableType.Array) return false
|
|
414
|
+
val a = patch.getArray(key) ?: return false
|
|
415
|
+
return a.size() >= 2
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/// Wrap a patch map so the full parser sees the right `id`. RN's
|
|
419
|
+
/// ReadableMap is read-only, so we route through a tiny JavaOnlyMap
|
|
420
|
+
/// copy with the id forced in.
|
|
421
|
+
private fun withId(patch: ReadableMap, id: String): ReadableMap {
|
|
422
|
+
val m = com.facebook.react.bridge.JavaOnlyMap()
|
|
423
|
+
m.merge(patch)
|
|
424
|
+
m.putString("id", id)
|
|
425
|
+
return m
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Build a minimal map carrying just `id` + `color` so we can reuse
|
|
429
|
+
/// the parser's colour logic for a colour-only patch.
|
|
430
|
+
private fun colorOnly(color: String?): ReadableMap {
|
|
431
|
+
val m = com.facebook.react.bridge.JavaOnlyMap()
|
|
432
|
+
m.putString("id", "_")
|
|
433
|
+
// Give it a dummy anchor so the parser accepts it.
|
|
434
|
+
val pos = com.facebook.react.bridge.JavaOnlyArray()
|
|
435
|
+
pos.pushDouble(0.0); pos.pushDouble(0.0); pos.pushDouble(0.0)
|
|
436
|
+
m.putArray("worldPosition", pos)
|
|
437
|
+
if (color != null) m.putString("color", color)
|
|
438
|
+
return m
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|