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
|
@@ -44,7 +44,12 @@ import {
|
|
|
44
44
|
|
|
45
45
|
import { ensureStitcherProxyInstalled } from '../stitching/ensureStitcherProxyInstalled';
|
|
46
46
|
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
47
|
-
import type { ARFrameMeta } from '../stitching/ARFrameMeta';
|
|
47
|
+
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
|
|
48
|
+
import type { AROverlay } from '../stitching/AROverlay';
|
|
49
|
+
import {
|
|
50
|
+
createAROverlayController,
|
|
51
|
+
type AROverlayMethods,
|
|
52
|
+
} from './arOverlayController';
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
// React Native looks up the component by its NATIVE name.
|
|
@@ -145,6 +150,54 @@ export interface ARCameraViewProps {
|
|
|
145
150
|
* (≈ 10 Hz). No effect unless `onArFrame` is provided.
|
|
146
151
|
*/
|
|
147
152
|
arFrameMetaInterval?: number;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* v0.19.0 — ASYNCHRONOUS AR-plugin result callback, invoked on the JS MAIN
|
|
156
|
+
* thread (NOT a worklet). Part of the AR plugin framework: host-registered
|
|
157
|
+
* native plugins (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) can
|
|
158
|
+
* offload heavy per-frame work to their own queue and later push a result
|
|
159
|
+
* via `registry.emit(name, result)`. The SDK routes that to JS as a
|
|
160
|
+
* `RNImageStitcherARPluginResult` device event; when this prop is provided,
|
|
161
|
+
* this component subscribes and invokes the handler with
|
|
162
|
+
* `{ plugin, result }`.
|
|
163
|
+
*
|
|
164
|
+
* SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
|
|
165
|
+
* the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins} —
|
|
166
|
+
* read them there. This callback is ONLY for the out-of-band async channel.
|
|
167
|
+
*
|
|
168
|
+
* The subscription is independent of {@link onArFrame}: a host can read
|
|
169
|
+
* sync results via `onArFrame` and async results via `onArPluginResult`,
|
|
170
|
+
* either, or both. Wiring mirrors `onArFrame` exactly (latest handler held
|
|
171
|
+
* in a ref so the subscription effect depends only on whether a handler is
|
|
172
|
+
* present; cleanup on unmount / when the handler is removed).
|
|
173
|
+
*/
|
|
174
|
+
onArPluginResult?: (e: ARPluginResult) => void;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
|
|
178
|
+
* shapes the native overlay layer draws ON TOP of the AR camera preview,
|
|
179
|
+
* each anchored to WORLD positions and REPROJECTED to screen on every AR
|
|
180
|
+
* frame from the current camera pose + intrinsics (smooth, display-rate
|
|
181
|
+
* tracking; no 3D engine).
|
|
182
|
+
*
|
|
183
|
+
* State-driven: pass a React-state array and update it as your world points
|
|
184
|
+
* change. The set is diffed against the current overlays BY `id` (add /
|
|
185
|
+
* update / remove), so re-passing the same ids is cheap. Each render pushes
|
|
186
|
+
* the resolved array to native via `RNSARSession.setOverlays`.
|
|
187
|
+
*
|
|
188
|
+
* For zero-render-latency / fire-and-forget mutations use the imperative ref
|
|
189
|
+
* methods instead ({@link ARCameraViewHandle.setOverlays} etc.) — both paths
|
|
190
|
+
* funnel through the same native channel and stay consistent. JS-set
|
|
191
|
+
* overlays are merged on the native side with any overlays a registered AR
|
|
192
|
+
* plugin placed directly (`RNISARPluginRegistry.setOverlays` /
|
|
193
|
+
* `RNSARPluginRegistry.setOverlays`); the two sets are namespaced so neither
|
|
194
|
+
* clobbers the other.
|
|
195
|
+
*
|
|
196
|
+
* See {@link AROverlay} for the shape (single world point + size, or explicit
|
|
197
|
+
* world quad; `outline` / `box`; optional label + colour; `mode:'3d'` is a
|
|
198
|
+
* documented scaffold this release and renders as `'2d'`).
|
|
199
|
+
*/
|
|
200
|
+
overlays?: AROverlay[];
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
|
|
@@ -158,8 +211,13 @@ export interface ARCameraViewProps {
|
|
|
158
211
|
* Note we do NOT exhaustively mirror vision-camera's API surface —
|
|
159
212
|
* only the methods the panorama capture flow uses today. As the
|
|
160
213
|
* SDK grows AR-aware features, methods are added here.
|
|
214
|
+
*
|
|
215
|
+
* v0.20.0 — also exposes the imperative AR-overlay methods
|
|
216
|
+
* ({@link AROverlayMethods}: `setOverlays` / `addOverlay` / `updateOverlay` /
|
|
217
|
+
* `removeOverlay` / `clearOverlays`) so a host can drive overlays without a
|
|
218
|
+
* render (the declarative `overlays` prop is the React-state alternative).
|
|
161
219
|
*/
|
|
162
|
-
export interface ARCameraViewHandle {
|
|
220
|
+
export interface ARCameraViewHandle extends AROverlayMethods {
|
|
163
221
|
/**
|
|
164
222
|
* Capture the latest ARFrame as a JPEG. Resolves with a
|
|
165
223
|
* vision-camera-compatible PhotoFile (`{ path, width, height,
|
|
@@ -237,6 +295,8 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
237
295
|
planeDetection,
|
|
238
296
|
onArFrame,
|
|
239
297
|
arFrameMetaInterval,
|
|
298
|
+
onArPluginResult,
|
|
299
|
+
overlays,
|
|
240
300
|
},
|
|
241
301
|
ref,
|
|
242
302
|
): React.JSX.Element {
|
|
@@ -245,6 +305,19 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
245
305
|
// pair vision-camera uses.
|
|
246
306
|
const recordingCallbacksRef = useRef<RecordingCallbacks | null>(null);
|
|
247
307
|
|
|
308
|
+
// v0.20.0 — AR overlay controller (shared logic with <Camera>). One
|
|
309
|
+
// instance per mount holds the JS-set overlay collection (keyed by id) and
|
|
310
|
+
// pushes the full array to native on every mutation. Both the declarative
|
|
311
|
+
// `overlays` prop (effect below) and the imperative ref methods drive it,
|
|
312
|
+
// so the two APIs can never diverge.
|
|
313
|
+
const overlayControllerRef = useRef<
|
|
314
|
+
ReturnType<typeof createAROverlayController> | null
|
|
315
|
+
>(null);
|
|
316
|
+
if (overlayControllerRef.current == null) {
|
|
317
|
+
overlayControllerRef.current = createAROverlayController();
|
|
318
|
+
}
|
|
319
|
+
const overlayController = overlayControllerRef.current;
|
|
320
|
+
|
|
248
321
|
// AR frame-processor registration. Installs the native
|
|
249
322
|
// `__stitcherProxy` (idempotent) and registers the host worklet so
|
|
250
323
|
// the AR session's per-frame fan-out invokes it; unregisters on
|
|
@@ -362,7 +435,70 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
362
435
|
};
|
|
363
436
|
}, [arFrameEnabled, arFrameMetaInterval]);
|
|
364
437
|
|
|
438
|
+
// v0.19.0 — onArPluginResult device-event wiring (worklet-free, main
|
|
439
|
+
// thread). Mirrors the onArFrame subscription above: the latest handler
|
|
440
|
+
// is held in a ref so the subscription effect depends only on WHETHER a
|
|
441
|
+
// handler is present, not its (per-render-changing) identity — so the
|
|
442
|
+
// native event subscription isn't torn down + re-established every render.
|
|
443
|
+
//
|
|
444
|
+
// This is a PURELY-JS subscription: unlike onArFrame there's no native
|
|
445
|
+
// "enable" toggle to flip. Native emits `RNImageStitcherARPluginResult`
|
|
446
|
+
// whenever a registered plugin calls `registry.emit(...)`; the registry is
|
|
447
|
+
// empty unless the host registered plugins, so an app with no plugins
|
|
448
|
+
// never sees an event even if this prop is wired.
|
|
449
|
+
const onArPluginResultRef = useRef<
|
|
450
|
+
((e: ARPluginResult) => void) | undefined
|
|
451
|
+
>(onArPluginResult);
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
onArPluginResultRef.current = onArPluginResult;
|
|
454
|
+
}, [onArPluginResult]);
|
|
455
|
+
|
|
456
|
+
const arPluginResultEnabled = onArPluginResult != null;
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
if (!arPluginResultEnabled) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const native = (NativeModules as Record<string, unknown>)
|
|
462
|
+
.RNSARSession;
|
|
463
|
+
if (native == null) {
|
|
464
|
+
// Native module unavailable (e.g. web, or a native build predating
|
|
465
|
+
// the plugin event channel): no-op, no crash.
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
const emitter = new NativeEventEmitter(native as never);
|
|
469
|
+
const sub = emitter.addListener(
|
|
470
|
+
'RNImageStitcherARPluginResult',
|
|
471
|
+
(e: ARPluginResult) => {
|
|
472
|
+
onArPluginResultRef.current?.(e);
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
return () => {
|
|
476
|
+
sub.remove();
|
|
477
|
+
};
|
|
478
|
+
}, [arPluginResultEnabled]);
|
|
479
|
+
|
|
480
|
+
// v0.20.0 — declarative `overlays` prop → native. Each render pushes the
|
|
481
|
+
// resolved array through the controller (which replaces the JS-set
|
|
482
|
+
// collection wholesale and dispatches to `RNSARSession.setOverlays`). The
|
|
483
|
+
// controller dedups identical native dispatches at the wire level is NOT
|
|
484
|
+
// attempted here — React only re-runs this when `overlays` identity
|
|
485
|
+
// changes, and native overlay set is cheap (a handful of shapes). When the
|
|
486
|
+
// prop is omitted we DON'T touch the controller, so a host driving overlays
|
|
487
|
+
// purely imperatively (via the ref) isn't clobbered by an undefined prop.
|
|
488
|
+
useEffect(() => {
|
|
489
|
+
if (overlays == null) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
overlayController.setOverlays(overlays);
|
|
493
|
+
}, [overlays, overlayController]);
|
|
494
|
+
|
|
365
495
|
useImperativeHandle(ref, () => ({
|
|
496
|
+
setOverlays: overlayController.setOverlays,
|
|
497
|
+
addOverlay: overlayController.addOverlay,
|
|
498
|
+
updateOverlay: overlayController.updateOverlay,
|
|
499
|
+
removeOverlay: overlayController.removeOverlay,
|
|
500
|
+
clearOverlays: overlayController.clearOverlays,
|
|
501
|
+
raycast: overlayController.raycast,
|
|
366
502
|
takePhoto: async (options = {}) => {
|
|
367
503
|
const native: any =
|
|
368
504
|
(NativeModules as Record<string, unknown>).RNSARSession;
|
|
@@ -414,7 +550,7 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
414
550
|
callbacks.onRecordingError?.(err as Error);
|
|
415
551
|
}
|
|
416
552
|
},
|
|
417
|
-
}), []);
|
|
553
|
+
}), [overlayController]);
|
|
418
554
|
|
|
419
555
|
if (!NativeARCameraView
|
|
420
556
|
|| (Platform.OS !== 'ios' && Platform.OS !== 'android')) {
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
import React, {
|
|
44
|
+
forwardRef,
|
|
44
45
|
useCallback,
|
|
45
46
|
useEffect,
|
|
47
|
+
useImperativeHandle,
|
|
46
48
|
useMemo,
|
|
47
49
|
useRef,
|
|
48
50
|
useState,
|
|
@@ -67,7 +69,9 @@ import type {
|
|
|
67
69
|
|
|
68
70
|
import { useARSession } from '../ar/useARSession';
|
|
69
71
|
import type { CameraFrameProcessor } from '../stitching/CameraFrame';
|
|
70
|
-
import type { ARFrameMeta } from '../stitching/ARFrameMeta';
|
|
72
|
+
import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
|
|
73
|
+
import type { AROverlay } from '../stitching/AROverlay';
|
|
74
|
+
import type { AROverlayMethods } from './arOverlayController';
|
|
71
75
|
import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
|
|
72
76
|
import { CameraShutter } from './CameraShutter';
|
|
73
77
|
import { CameraView } from './CameraView';
|
|
@@ -814,6 +818,46 @@ export interface CameraProps {
|
|
|
814
818
|
*/
|
|
815
819
|
arFrameMetaInterval?: number;
|
|
816
820
|
|
|
821
|
+
/**
|
|
822
|
+
* v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin
|
|
823
|
+
* framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in
|
|
824
|
+
* AR capture (`captureSource === 'ar'`). Host-registered native plugins
|
|
825
|
+
* (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy
|
|
826
|
+
* per-frame work to their own queue push results via
|
|
827
|
+
* `registry.emit(name, result)`; `<Camera>` threads this handler to
|
|
828
|
+
* `<ARCameraView>`, which subscribes to the `RNImageStitcherARPluginResult`
|
|
829
|
+
* device event and invokes it with `{ plugin, result }`.
|
|
830
|
+
*
|
|
831
|
+
* SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride
|
|
832
|
+
* the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}.
|
|
833
|
+
* Use `onArFrame` for the in-band sync channel and `onArPluginResult` for
|
|
834
|
+
* the out-of-band async channel — a host can wire either or both.
|
|
835
|
+
*
|
|
836
|
+
* The SDK ships ONLY the generic plugin framework; there are no built-in
|
|
837
|
+
* plugins, so this never fires unless the host registers native plugins.
|
|
838
|
+
*/
|
|
839
|
+
onArPluginResult?: (e: ARPluginResult) => void;
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
|
|
843
|
+
* shapes drawn ON TOP of the AR camera preview, each anchored to WORLD
|
|
844
|
+
* positions and REPROJECTED to screen on every AR frame from the current
|
|
845
|
+
* camera pose + intrinsics (smooth display-rate tracking, no 3D engine).
|
|
846
|
+
* Only meaningful in AR capture (`captureSource === 'ar'`); `<Camera>`
|
|
847
|
+
* threads this straight through to the underlying `<ARCameraView>`.
|
|
848
|
+
*
|
|
849
|
+
* State-driven: pass a React-state array and update it as your world points
|
|
850
|
+
* change (e.g. from {@link CameraProps.onArFrame} plane anchors). The set is
|
|
851
|
+
* diffed against the current overlays BY `id`. For zero-render-latency
|
|
852
|
+
* mutations use the imperative ref methods on the `<Camera>` handle instead
|
|
853
|
+
* ({@link CameraHandle}: `setOverlays` / `addOverlay` / `updateOverlay` /
|
|
854
|
+
* `removeOverlay` / `clearOverlays`) — both paths funnel through the same
|
|
855
|
+
* native channel. JS-set overlays merge on the native side with overlays a
|
|
856
|
+
* registered AR plugin placed directly (namespaced so neither clobbers the
|
|
857
|
+
* other). See {@link AROverlay} for the shape.
|
|
858
|
+
*/
|
|
859
|
+
overlays?: AROverlay[];
|
|
860
|
+
|
|
817
861
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────────
|
|
818
862
|
/**
|
|
819
863
|
* Which device holds the non-AR panorama capture accepts.
|
|
@@ -902,6 +946,26 @@ export interface CameraProps {
|
|
|
902
946
|
}
|
|
903
947
|
|
|
904
948
|
|
|
949
|
+
/**
|
|
950
|
+
* v0.20.0 — imperative handle exposed via the `<Camera>` ref.
|
|
951
|
+
*
|
|
952
|
+
* Currently scoped to the AR-overlay methods ({@link AROverlayMethods}:
|
|
953
|
+
* `setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
|
|
954
|
+
* `clearOverlays`), which forward to the underlying `<ARCameraView>`'s overlay
|
|
955
|
+
* channel when AR mode is mounted. They are no-ops while the camera is in
|
|
956
|
+
* non-AR mode (no `<ARCameraView>` is mounted, and overlays only render over
|
|
957
|
+
* the AR preview) — use the declarative {@link CameraProps.overlays} prop for
|
|
958
|
+
* a set that survives AR↔non-AR transitions, since it re-applies automatically
|
|
959
|
+
* whenever `<ARCameraView>` (re)mounts.
|
|
960
|
+
*
|
|
961
|
+
* The shape is identical to {@link ARCameraViewHandle}'s overlay subset so a
|
|
962
|
+
* host can use either component with the same overlay code. Photo / panorama
|
|
963
|
+
* capture remain driven by the built-in shutter (no imperative capture methods
|
|
964
|
+
* on this handle — see the component docstring's scope note).
|
|
965
|
+
*/
|
|
966
|
+
export interface CameraHandle extends AROverlayMethods {}
|
|
967
|
+
|
|
968
|
+
|
|
905
969
|
// ─── Sub-components ─────────────────────────────────────────────────
|
|
906
970
|
|
|
907
971
|
/**
|
|
@@ -1187,8 +1251,15 @@ function extractPanoramaOverrides(props: CameraProps): PanoramaPropOverrides {
|
|
|
1187
1251
|
|
|
1188
1252
|
/**
|
|
1189
1253
|
* The public `<Camera>` component.
|
|
1254
|
+
*
|
|
1255
|
+
* v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR
|
|
1256
|
+
* overlay methods); existing callers that don't pass a ref are unaffected
|
|
1257
|
+
* (`forwardRef` makes the ref optional).
|
|
1190
1258
|
*/
|
|
1191
|
-
export
|
|
1259
|
+
export const Camera = forwardRef<CameraHandle, CameraProps>(function Camera(
|
|
1260
|
+
props: CameraProps,
|
|
1261
|
+
ref,
|
|
1262
|
+
): React.JSX.Element {
|
|
1192
1263
|
const {
|
|
1193
1264
|
defaultCaptureSource = 'non-ar',
|
|
1194
1265
|
defaultLens = '1x',
|
|
@@ -1227,6 +1298,8 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1227
1298
|
planeDetection,
|
|
1228
1299
|
onArFrame,
|
|
1229
1300
|
arFrameMetaInterval,
|
|
1301
|
+
onArPluginResult,
|
|
1302
|
+
overlays,
|
|
1230
1303
|
engine = 'batch-keyframe',
|
|
1231
1304
|
// ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
|
|
1232
1305
|
panMode = 'vertical',
|
|
@@ -1490,6 +1563,22 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1490
1563
|
const visionCameraRef = useRef<VisionCamera | null>(null);
|
|
1491
1564
|
const arViewRef = useRef<ARCameraViewHandle | null>(null);
|
|
1492
1565
|
|
|
1566
|
+
// v0.20.0 — AR overlay imperative handle. `<Camera>` itself renders no
|
|
1567
|
+
// overlay layer; the overlay methods forward to the mounted
|
|
1568
|
+
// `<ARCameraView>`'s handle (which owns the controller + native dispatch).
|
|
1569
|
+
// No-op when AR mode isn't mounted (`arViewRef.current === null`), matching
|
|
1570
|
+
// the CameraHandle docstring — the declarative `overlays` prop is the path
|
|
1571
|
+
// that survives AR↔non-AR transitions. The `overlays` prop is also threaded
|
|
1572
|
+
// straight to `<ARCameraView>` below, so a host can use either API.
|
|
1573
|
+
useImperativeHandle(ref, (): CameraHandle => ({
|
|
1574
|
+
setOverlays: (o) => arViewRef.current?.setOverlays(o),
|
|
1575
|
+
addOverlay: (o) => arViewRef.current?.addOverlay(o),
|
|
1576
|
+
updateOverlay: (id, patch) => arViewRef.current?.updateOverlay(id, patch),
|
|
1577
|
+
removeOverlay: (id) => arViewRef.current?.removeOverlay(id),
|
|
1578
|
+
clearOverlays: () => arViewRef.current?.clearOverlays(),
|
|
1579
|
+
raycast: () => arViewRef.current?.raycast() ?? Promise.resolve(null),
|
|
1580
|
+
}), []);
|
|
1581
|
+
|
|
1493
1582
|
// Effect that does the async transition work whenever the settled
|
|
1494
1583
|
// refs disagree with the current isAR/lens. Order matters:
|
|
1495
1584
|
// 1. Set the cameraTransitioning state so the gate stays closed
|
|
@@ -2497,6 +2586,8 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
2497
2586
|
planeDetection={planeDetection}
|
|
2498
2587
|
onArFrame={onArFrame}
|
|
2499
2588
|
arFrameMetaInterval={arFrameMetaInterval}
|
|
2589
|
+
onArPluginResult={onArPluginResult}
|
|
2590
|
+
overlays={overlays}
|
|
2500
2591
|
/>
|
|
2501
2592
|
) : (
|
|
2502
2593
|
<CameraView
|
|
@@ -2993,7 +3084,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
2993
3084
|
/>
|
|
2994
3085
|
</View>
|
|
2995
3086
|
);
|
|
2996
|
-
}
|
|
3087
|
+
});
|
|
2997
3088
|
|
|
2998
3089
|
|
|
2999
3090
|
function noop(): void {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* v0.20.0 — shared JS→native plumbing for the AR overlay renderer.
|
|
5
|
+
*
|
|
6
|
+
* `<ARCameraView>` and `<Camera>` expose an IDENTICAL imperative overlay API
|
|
7
|
+
* (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
|
|
8
|
+
* `clearOverlays`) plus a declarative `overlays` prop. Rather than duplicate
|
|
9
|
+
* the diff + native-dispatch logic in each component, both build their handle
|
|
10
|
+
* from {@link createAROverlayController} — DRY, single source of truth for the
|
|
11
|
+
* wire format and the merge-by-id semantics.
|
|
12
|
+
*
|
|
13
|
+
* ## Native mechanism (agreed cross-platform contract)
|
|
14
|
+
*
|
|
15
|
+
* Every AR-session setting in this SDK already flows through the
|
|
16
|
+
* `RNSARSession` native-module singleton (`setPlaneDetection`,
|
|
17
|
+
* `setArFrameMetaEnabled`, `setSceneReconstructionEnabled`) because
|
|
18
|
+
* `RNSARSession.shared` drives the single mounted `RNSARCameraView`. Overlays
|
|
19
|
+
* follow the same pattern: a single `setOverlays(overlays)` method on
|
|
20
|
+
* `RNSARSession` carries the FULL current JS-set overlay array each time it
|
|
21
|
+
* changes. Native replaces its JS-set overlay collection wholesale (the merge
|
|
22
|
+
* with the namespaced native-plugin set happens on the native side) and the
|
|
23
|
+
* overlay layer redraws every AR frame.
|
|
24
|
+
*
|
|
25
|
+
* The declarative `overlays` prop and the imperative methods both ultimately
|
|
26
|
+
* call this same `setOverlays` with the resolved array, so the two APIs are
|
|
27
|
+
* interchangeable and can't diverge.
|
|
28
|
+
*
|
|
29
|
+
* Why a module method (not a UIManager view command)? It matches every other
|
|
30
|
+
* AR setting in this codebase and there is only ever ONE `RNSARCameraView`
|
|
31
|
+
* mounted (ARKit/ARCore can't share the camera), so there's nothing to key by
|
|
32
|
+
* view tag. The equivalent UIManager view-command name, for native sides that
|
|
33
|
+
* prefer per-view dispatch, is documented as `RNSARCameraViewOverlays`.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { NativeModules } from 'react-native';
|
|
37
|
+
|
|
38
|
+
import type { AROverlay } from '../stitching/AROverlay';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The imperative overlay methods exposed on both `<ARCameraView>` and
|
|
42
|
+
* `<Camera>` refs. Identical shape on both so a host can swap components
|
|
43
|
+
* without rewriting overlay code.
|
|
44
|
+
*/
|
|
45
|
+
export interface AROverlayMethods {
|
|
46
|
+
/** Replace the entire JS-set overlay collection. */
|
|
47
|
+
setOverlays: (overlays: AROverlay[]) => void;
|
|
48
|
+
/** Add one overlay (replaces any existing overlay with the same `id`). */
|
|
49
|
+
addOverlay: (overlay: AROverlay) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Shallow-merge a patch into the overlay with `id`. No-op if no overlay
|
|
52
|
+
* with that `id` is currently set.
|
|
53
|
+
*/
|
|
54
|
+
updateOverlay: (id: string, patch: Partial<AROverlay>) => void;
|
|
55
|
+
/** Remove the overlay with `id` (no-op if absent). */
|
|
56
|
+
removeOverlay: (id: string) => void;
|
|
57
|
+
/** Remove all JS-set overlays. */
|
|
58
|
+
clearOverlays: () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Raycast from the screen centre (the crosshair) to the first real-world
|
|
61
|
+
* surface and resolve its world position `[x, y, z]` in metres (ARKit/ARCore
|
|
62
|
+
* world frame), or `null` when nothing is hit (e.g. a featureless wall before
|
|
63
|
+
* any plane is detected). Use it to place an overlay ON the aimed surface at
|
|
64
|
+
* the real distance — pass the result as a `worldPosition` to
|
|
65
|
+
* {@link setOverlays} / {@link addOverlay} — instead of guessing a distance.
|
|
66
|
+
* Resolves `null` (never throws) when the native module / method is absent.
|
|
67
|
+
*/
|
|
68
|
+
raycast: () => Promise<[number, number, number] | null>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface RNSARSessionOverlayModule {
|
|
72
|
+
// On iOS the native `setOverlays` is a Promise method (resolver/rejecter
|
|
73
|
+
// RN-injected); on Android it's `void`. We call it fire-and-forget but
|
|
74
|
+
// type it as possibly-thenable so the defensive `.catch` below compiles.
|
|
75
|
+
setOverlays?: (overlays: AROverlay[]) => void | Promise<unknown>;
|
|
76
|
+
// Raycast resolves `{ worldPosition: [x,y,z] }` on a hit, or `null`.
|
|
77
|
+
raycast?: () => Promise<{ worldPosition?: number[] } | null>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The `RNSARSession` native-module method name overlays dispatch through. */
|
|
81
|
+
export const AR_OVERLAY_SET_METHOD = 'setOverlays' as const;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The agreed UIManager view-command name for native sides that drive overlays
|
|
85
|
+
* via per-view command dispatch instead of the module method. The JS layer
|
|
86
|
+
* dispatches through the module method (there's only one AR view), but the
|
|
87
|
+
* name is pinned here so the native side can match if it chooses commands.
|
|
88
|
+
*/
|
|
89
|
+
export const AR_OVERLAY_VIEW_COMMAND = 'RNSARCameraViewOverlays' as const;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build an overlay controller backed by an in-memory ordered set keyed by
|
|
93
|
+
* `id`. Every mutating call resolves the new full array and pushes it to
|
|
94
|
+
* native via `RNSARSession.setOverlays`. The controller is the single source
|
|
95
|
+
* of truth for BOTH the imperative ref methods and the declarative `overlays`
|
|
96
|
+
* prop (the prop's effect calls `setOverlays` with the prop value).
|
|
97
|
+
*/
|
|
98
|
+
export function createAROverlayController(): AROverlayMethods & {
|
|
99
|
+
/** Current JS-set overlays in insertion order (used by tests / diffing). */
|
|
100
|
+
getOverlays: () => AROverlay[];
|
|
101
|
+
} {
|
|
102
|
+
// Insertion-ordered map: preserves the order overlays were added so the
|
|
103
|
+
// native render order is stable + predictable.
|
|
104
|
+
const overlaysById = new Map<string, AROverlay>();
|
|
105
|
+
|
|
106
|
+
const flush = (): void => {
|
|
107
|
+
const native = (NativeModules as Record<string, unknown>)
|
|
108
|
+
.RNSARSession as RNSARSessionOverlayModule | undefined;
|
|
109
|
+
// Native module / method unavailable (web, or a native build predating the
|
|
110
|
+
// overlay channel): no-op, no crash — mirrors the other AR setters.
|
|
111
|
+
const ret = native?.setOverlays?.(Array.from(overlaysById.values()));
|
|
112
|
+
// iOS returns a Promise (the native method is Promise-typed); swallow any
|
|
113
|
+
// rejection so a transient native error never surfaces as an unhandled
|
|
114
|
+
// rejection. Android returns void — the optional chain skips the catch.
|
|
115
|
+
(ret as Promise<unknown> | undefined)?.catch?.(() => undefined);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
getOverlays: () => Array.from(overlaysById.values()),
|
|
120
|
+
|
|
121
|
+
setOverlays: (overlays: AROverlay[]) => {
|
|
122
|
+
overlaysById.clear();
|
|
123
|
+
for (const o of overlays) {
|
|
124
|
+
// Last-writer-wins on duplicate ids in the incoming array.
|
|
125
|
+
overlaysById.set(o.id, o);
|
|
126
|
+
}
|
|
127
|
+
flush();
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
addOverlay: (overlay: AROverlay) => {
|
|
131
|
+
// Re-set to move an existing id to the end? No — preserve original slot
|
|
132
|
+
// by deleting first only when absent. Map#set keeps the existing slot
|
|
133
|
+
// when the key already exists, so a plain set is the right "replace in
|
|
134
|
+
// place" behaviour.
|
|
135
|
+
overlaysById.set(overlay.id, overlay);
|
|
136
|
+
flush();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
updateOverlay: (id: string, patch: Partial<AROverlay>) => {
|
|
140
|
+
const existing = overlaysById.get(id);
|
|
141
|
+
if (existing == null) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Shallow-merge; `id` is preserved from the existing overlay regardless
|
|
145
|
+
// of what the patch carries (the map key must stay consistent).
|
|
146
|
+
overlaysById.set(id, { ...existing, ...patch, id });
|
|
147
|
+
flush();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
removeOverlay: (id: string) => {
|
|
151
|
+
if (overlaysById.delete(id)) {
|
|
152
|
+
flush();
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
clearOverlays: () => {
|
|
157
|
+
if (overlaysById.size > 0) {
|
|
158
|
+
overlaysById.clear();
|
|
159
|
+
flush();
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
raycast: async (): Promise<[number, number, number] | null> => {
|
|
164
|
+
const native = (NativeModules as Record<string, unknown>)
|
|
165
|
+
.RNSARSession as RNSARSessionOverlayModule | undefined;
|
|
166
|
+
const fn = native?.raycast;
|
|
167
|
+
// Native module / method unavailable (web, or a native build predating
|
|
168
|
+
// the raycast channel): resolve null — the caller falls back.
|
|
169
|
+
if (typeof fn !== 'function') {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const res = await fn();
|
|
174
|
+
const wp = res?.worldPosition;
|
|
175
|
+
if (Array.isArray(wp) && wp.length >= 3) {
|
|
176
|
+
return [Number(wp[0]), Number(wp[1]), Number(wp[2])];
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
export { Camera, CameraError } from './camera/Camera';
|
|
28
28
|
export type {
|
|
29
29
|
CameraProps,
|
|
30
|
+
CameraHandle,
|
|
30
31
|
CameraCaptureResult,
|
|
31
32
|
PanoramaCaptureResult,
|
|
32
33
|
CameraErrorCode,
|
|
@@ -265,8 +266,27 @@ export type {
|
|
|
265
266
|
} from './stitching/CameraFrame';
|
|
266
267
|
// v0.18.0 — LIGHT per-frame AR metadata delivered via the `onArFrame`
|
|
267
268
|
// callback (main-thread, worklet-free). See the type's docstring for why
|
|
268
|
-
// it bypasses the worklet path.
|
|
269
|
+
// it bypasses the worklet path. v0.19.0 adds `plugins` (sync results from
|
|
270
|
+
// host-registered AR plugins ride this same throttled event).
|
|
269
271
|
export type { ARFrameMeta } from './stitching/ARFrameMeta';
|
|
272
|
+
// v0.19.0 — the AR plugin framework's ASYNC result type, delivered via the
|
|
273
|
+
// `onArPluginResult` callback (a plugin's out-of-band `registry.emit(...)`
|
|
274
|
+
// result). The SDK ships only the generic framework — no built-in plugins.
|
|
275
|
+
export type { ARPluginResult } from './stitching/ARFrameMeta';
|
|
276
|
+
// v0.20.0 — AR OVERLAY / ANNOTATION renderer data model. A 2D shape anchored
|
|
277
|
+
// to a world point (or world quad) and reprojected to screen every AR frame.
|
|
278
|
+
// Drive it via the declarative `overlays` prop or the imperative ref methods
|
|
279
|
+
// (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
|
|
280
|
+
// `clearOverlays`) on both `<Camera>` and `<ARCameraView>`.
|
|
281
|
+
export type { AROverlay } from './stitching/AROverlay';
|
|
282
|
+
// The shared imperative-overlay method signatures (the `<Camera>` /
|
|
283
|
+
// `<ARCameraView>` ref handles extend this). Plus the agreed native channel
|
|
284
|
+
// names, for hosts / native plugins matching the wire contract.
|
|
285
|
+
export type { AROverlayMethods } from './camera/arOverlayController';
|
|
286
|
+
export {
|
|
287
|
+
AR_OVERLAY_SET_METHOD,
|
|
288
|
+
AR_OVERLAY_VIEW_COMMAND,
|
|
289
|
+
} from './camera/arOverlayController';
|
|
270
290
|
// NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
|
|
271
291
|
// `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
|
|
272
292
|
// archived in the batch-keyframe cleanup — they drove the third-party
|
|
@@ -104,4 +104,54 @@ export interface ARFrameMeta {
|
|
|
104
104
|
vertexCount: number;
|
|
105
105
|
faceCount: number;
|
|
106
106
|
} | null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* v0.19.0 — SYNCHRONOUS results from host-registered AR frame plugins
|
|
110
|
+
* (the AR plugin framework). Keyed by each plugin's `name()`; the value
|
|
111
|
+
* is the light JSON result the plugin's `process(ctx)` returned on the AR
|
|
112
|
+
* thread (`nil`/`null`-returning plugins are omitted). Only present when
|
|
113
|
+
* the native plugin registry is non-empty AND at least one plugin returned
|
|
114
|
+
* a sync result for this frame; otherwise omitted entirely (zero-plugin
|
|
115
|
+
* apps pay nothing — native skips building the context).
|
|
116
|
+
*
|
|
117
|
+
* The SDK ships ONLY the generic framework — there are no built-in
|
|
118
|
+
* plugins. Hosts register native plugins via `RNISARPluginRegistry`
|
|
119
|
+
* (iOS) / `RNSARPluginRegistry` (Android) at startup; each plugin is
|
|
120
|
+
* called once per AR frame while the registry is non-empty. Result
|
|
121
|
+
* values are `unknown` because each plugin defines its own shape — cast
|
|
122
|
+
* after reading the entry you care about (e.g.
|
|
123
|
+
* `meta.plugins?.brightness as number`).
|
|
124
|
+
*
|
|
125
|
+
* ## Sync vs async results
|
|
126
|
+
*
|
|
127
|
+
* This field carries only the LIGHT, in-band SYNC results (computed fast
|
|
128
|
+
* enough to ride the throttled `onArFrame` event). Plugins that offload
|
|
129
|
+
* heavy work to their own queue deliver results out-of-band via
|
|
130
|
+
* `registry.emit(name, result)`, which surfaces through the separate
|
|
131
|
+
* `onArPluginResult` callback (the `RNImageStitcherARPluginResult`
|
|
132
|
+
* event) — NOT here.
|
|
133
|
+
*/
|
|
134
|
+
plugins?: { [name: string]: unknown };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* v0.19.0 — an ASYNCHRONOUS result from a host-registered AR frame plugin,
|
|
140
|
+
* delivered via the `onArPluginResult` callback.
|
|
141
|
+
*
|
|
142
|
+
* Unlike the in-band SYNC results carried on {@link ARFrameMeta.plugins}
|
|
143
|
+
* (which ride the throttled `onArFrame` event), a plugin produces an async
|
|
144
|
+
* result by offloading heavy work to its own queue and later calling
|
|
145
|
+
* `registry.emit(name, result)` on the native side. The SDK routes that to
|
|
146
|
+
* JS as a `RNImageStitcherARPluginResult` device event; `<ARCameraView>`
|
|
147
|
+
* subscribes and invokes `onArPluginResult` on the JS MAIN thread.
|
|
148
|
+
*
|
|
149
|
+
* `result` is `unknown` because each plugin defines its own result shape —
|
|
150
|
+
* branch on `plugin` (the emitting plugin's `name()`) and cast accordingly.
|
|
151
|
+
*/
|
|
152
|
+
export interface ARPluginResult {
|
|
153
|
+
/** The `name()` of the plugin that emitted this result. */
|
|
154
|
+
plugin: string;
|
|
155
|
+
/** The plugin-defined result payload (cast after branching on `plugin`). */
|
|
156
|
+
result: unknown;
|
|
107
157
|
}
|