react-native-image-stitcher 0.19.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/android/src/main/java/io/imagestitcher/rn/AROverlayRenderer.kt +406 -0
  3. package/android/src/main/java/io/imagestitcher/rn/AROverlayStore.kt +441 -0
  4. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +290 -0
  5. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +30 -5
  6. package/android/src/main/java/io/imagestitcher/rn/RNSARPluginRegistry.kt +68 -0
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +99 -0
  8. package/dist/camera/ARCameraView.d.ts +33 -1
  9. package/dist/camera/ARCameraView.js +33 -2
  10. package/dist/camera/Camera.d.ts +45 -1
  11. package/dist/camera/Camera.js +24 -6
  12. package/dist/camera/arOverlayController.d.ts +52 -0
  13. package/dist/camera/arOverlayController.js +132 -0
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.js +5 -2
  16. package/dist/stitching/AROverlay.d.ts +97 -0
  17. package/dist/stitching/AROverlay.js +4 -0
  18. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +15 -8
  19. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +22 -0
  20. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +14 -0
  21. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +81 -0
  22. package/ios/Sources/RNImageStitcher/RNISARFramePlugin.swift +37 -0
  23. package/ios/Sources/RNImageStitcher/RNISAROverlay.swift +409 -0
  24. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +281 -3
  25. package/package.json +1 -1
  26. package/src/camera/ARCameraView.tsx +73 -2
  27. package/src/camera/Camera.tsx +71 -2
  28. package/src/camera/arOverlayController.ts +184 -0
  29. package/src/index.ts +15 -0
  30. package/src/stitching/AROverlay.ts +105 -0
@@ -68,6 +68,7 @@ exports.ARCameraView = void 0;
68
68
  const react_1 = __importStar(require("react"));
69
69
  const react_native_1 = require("react-native");
70
70
  const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxyInstalled");
71
+ const arOverlayController_1 = require("./arOverlayController");
71
72
  // React Native looks up the component by its NATIVE name.
72
73
  // iOS: comes from `ARCameraViewManager.m`'s
73
74
  // `RCT_EXTERN_MODULE(RNSARCameraViewManager, RCTViewManager)`.
@@ -77,11 +78,21 @@ const ensureStitcherProxyInstalled_1 = require("../stitching/ensureStitcherProxy
77
78
  const NativeARCameraView = react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android'
78
79
  ? (0, react_native_1.requireNativeComponent)('RNSARCameraView')
79
80
  : null;
80
- exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, onArPluginResult, }, ref) {
81
+ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, guidance, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, onArPluginResult, overlays, }, ref) {
81
82
  // Held across the start→stop lifecycle so stopRecording's
82
83
  // resolved VideoFile can be delivered via the same callback
83
84
  // pair vision-camera uses.
84
85
  const recordingCallbacksRef = (0, react_1.useRef)(null);
86
+ // v0.20.0 — AR overlay controller (shared logic with <Camera>). One
87
+ // instance per mount holds the JS-set overlay collection (keyed by id) and
88
+ // pushes the full array to native on every mutation. Both the declarative
89
+ // `overlays` prop (effect below) and the imperative ref methods drive it,
90
+ // so the two APIs can never diverge.
91
+ const overlayControllerRef = (0, react_1.useRef)(null);
92
+ if (overlayControllerRef.current == null) {
93
+ overlayControllerRef.current = (0, arOverlayController_1.createAROverlayController)();
94
+ }
95
+ const overlayController = overlayControllerRef.current;
85
96
  // AR frame-processor registration. Installs the native
86
97
  // `__stitcherProxy` (idempotent) and registers the host worklet so
87
98
  // the AR session's per-frame fan-out invokes it; unregisters on
@@ -205,7 +216,27 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
205
216
  sub.remove();
206
217
  };
207
218
  }, [arPluginResultEnabled]);
219
+ // v0.20.0 — declarative `overlays` prop → native. Each render pushes the
220
+ // resolved array through the controller (which replaces the JS-set
221
+ // collection wholesale and dispatches to `RNSARSession.setOverlays`). The
222
+ // controller dedups identical native dispatches at the wire level is NOT
223
+ // attempted here — React only re-runs this when `overlays` identity
224
+ // changes, and native overlay set is cheap (a handful of shapes). When the
225
+ // prop is omitted we DON'T touch the controller, so a host driving overlays
226
+ // purely imperatively (via the ref) isn't clobbered by an undefined prop.
227
+ (0, react_1.useEffect)(() => {
228
+ if (overlays == null) {
229
+ return;
230
+ }
231
+ overlayController.setOverlays(overlays);
232
+ }, [overlays, overlayController]);
208
233
  (0, react_1.useImperativeHandle)(ref, () => ({
234
+ setOverlays: overlayController.setOverlays,
235
+ addOverlay: overlayController.addOverlay,
236
+ updateOverlay: overlayController.updateOverlay,
237
+ removeOverlay: overlayController.removeOverlay,
238
+ clearOverlays: overlayController.clearOverlays,
239
+ raycast: overlayController.raycast,
209
240
  takePhoto: async (options = {}) => {
210
241
  const native = react_native_1.NativeModules.RNSARSession;
211
242
  if (!native?.takePhoto) {
@@ -249,7 +280,7 @@ exports.ARCameraView = (0, react_1.forwardRef)(function ARCameraView({ style, gu
249
280
  callbacks.onRecordingError?.(err);
250
281
  }
251
282
  },
252
- }), []);
283
+ }), [overlayController]);
253
284
  if (!NativeARCameraView
254
285
  || (react_native_1.Platform.OS !== 'ios' && react_native_1.Platform.OS !== 'android')) {
255
286
  // Web / unsupported platforms get a clear "not available here"
@@ -43,6 +43,8 @@ import { type StyleProp, type ViewStyle } from 'react-native';
43
43
  import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
44
44
  import type { CameraFrameProcessor } from '../stitching/CameraFrame';
45
45
  import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta';
46
+ import type { AROverlay } from '../stitching/AROverlay';
47
+ import type { AROverlayMethods } from './arOverlayController';
46
48
  import { type CaptureHeaderProps } from './CaptureHeader';
47
49
  import { type CapturePreviewAction } from './CapturePreview';
48
50
  import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
@@ -675,6 +677,25 @@ export interface CameraProps {
675
677
  * plugins, so this never fires unless the host registers native plugins.
676
678
  */
677
679
  onArPluginResult?: (e: ARPluginResult) => void;
680
+ /**
681
+ * v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D
682
+ * shapes drawn ON TOP of the AR camera preview, each anchored to WORLD
683
+ * positions and REPROJECTED to screen on every AR frame from the current
684
+ * camera pose + intrinsics (smooth display-rate tracking, no 3D engine).
685
+ * Only meaningful in AR capture (`captureSource === 'ar'`); `<Camera>`
686
+ * threads this straight through to the underlying `<ARCameraView>`.
687
+ *
688
+ * State-driven: pass a React-state array and update it as your world points
689
+ * change (e.g. from {@link CameraProps.onArFrame} plane anchors). The set is
690
+ * diffed against the current overlays BY `id`. For zero-render-latency
691
+ * mutations use the imperative ref methods on the `<Camera>` handle instead
692
+ * ({@link CameraHandle}: `setOverlays` / `addOverlay` / `updateOverlay` /
693
+ * `removeOverlay` / `clearOverlays`) — both paths funnel through the same
694
+ * native channel. JS-set overlays merge on the native side with overlays a
695
+ * registered AR plugin placed directly (namespaced so neither clobbers the
696
+ * other). See {@link AROverlay} for the shape.
697
+ */
698
+ overlays?: AROverlay[];
678
699
  /**
679
700
  * Which device holds the non-AR panorama capture accepts.
680
701
  *
@@ -753,10 +774,33 @@ export interface CameraProps {
753
774
  */
754
775
  guidanceCopy?: Partial<GuidanceCopy>;
755
776
  }
777
+ /**
778
+ * v0.20.0 — imperative handle exposed via the `<Camera>` ref.
779
+ *
780
+ * Currently scoped to the AR-overlay methods ({@link AROverlayMethods}:
781
+ * `setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
782
+ * `clearOverlays`), which forward to the underlying `<ARCameraView>`'s overlay
783
+ * channel when AR mode is mounted. They are no-ops while the camera is in
784
+ * non-AR mode (no `<ARCameraView>` is mounted, and overlays only render over
785
+ * the AR preview) — use the declarative {@link CameraProps.overlays} prop for
786
+ * a set that survives AR↔non-AR transitions, since it re-applies automatically
787
+ * whenever `<ARCameraView>` (re)mounts.
788
+ *
789
+ * The shape is identical to {@link ARCameraViewHandle}'s overlay subset so a
790
+ * host can use either component with the same overlay code. Photo / panorama
791
+ * capture remain driven by the built-in shutter (no imperative capture methods
792
+ * on this handle — see the component docstring's scope note).
793
+ */
794
+ export interface CameraHandle extends AROverlayMethods {
795
+ }
756
796
  /**
757
797
  * The public `<Camera>` component.
798
+ *
799
+ * v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR
800
+ * overlay methods); existing callers that don't pass a ref are unaffected
801
+ * (`forwardRef` makes the ref optional).
758
802
  */
759
- export declare function Camera(props: CameraProps): React.JSX.Element;
803
+ export declare const Camera: React.ForwardRefExoticComponent<CameraProps & React.RefAttributes<CameraHandle>>;
760
804
  /**
761
805
  * v0.12.0 — JS edge corresponding to the physical home-indicator
762
806
  * side of the device. This is where the shutter + controls anchor
@@ -74,8 +74,7 @@ var __importStar = (this && this.__importStar) || (function () {
74
74
  };
75
75
  })();
76
76
  Object.defineProperty(exports, "__esModule", { value: true });
77
- exports._cameraShouldUnmountForTests = exports._isSideEdgeForTests = exports._homeIndicatorEdgeForTests = exports.CameraError = void 0;
78
- exports.Camera = Camera;
77
+ exports._cameraShouldUnmountForTests = exports._isSideEdgeForTests = exports._homeIndicatorEdgeForTests = exports.Camera = exports.CameraError = void 0;
79
78
  const react_1 = __importStar(require("react"));
80
79
  const react_native_1 = require("react-native");
81
80
  const react_native_safe_area_context_1 = require("react-native-safe-area-context");
@@ -308,9 +307,13 @@ function extractPanoramaOverrides(props) {
308
307
  // `<Image>` requires the scheme; iOS is lenient).
309
308
  /**
310
309
  * The public `<Camera>` component.
310
+ *
311
+ * v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR
312
+ * overlay methods); existing callers that don't pass a ref are unaffected
313
+ * (`forwardRef` makes the ref optional).
311
314
  */
312
- function Camera(props) {
313
- const { defaultCaptureSource = 'non-ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, onArPluginResult, engine = 'batch-keyframe',
315
+ exports.Camera = (0, react_1.forwardRef)(function Camera(props, ref) {
316
+ const { defaultCaptureSource = 'non-ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, arFrameProcessor, enableDepth, enableAnchors, enableMesh, planeDetection, onArFrame, arFrameMetaInterval, onArPluginResult, overlays, engine = 'batch-keyframe',
314
317
  // ── Panorama GUIDANCE (feature/pano-ux-guidance) ──────────────
315
318
  panMode = 'vertical', panGuidance = true, maxPanDurationMs = 0, panTooFastThreshold, lateralBudgetCm = 4, rectCrop = false, showPreview = false, guidanceCopy, } = props;
316
319
  // Derived guidance state. The landscape-only gate decision itself is
@@ -513,6 +516,21 @@ function Camera(props) {
513
516
  const incremental = (0, useIncrementalStitcher_1.useIncrementalStitcher)();
514
517
  const visionCameraRef = (0, react_1.useRef)(null);
515
518
  const arViewRef = (0, react_1.useRef)(null);
519
+ // v0.20.0 — AR overlay imperative handle. `<Camera>` itself renders no
520
+ // overlay layer; the overlay methods forward to the mounted
521
+ // `<ARCameraView>`'s handle (which owns the controller + native dispatch).
522
+ // No-op when AR mode isn't mounted (`arViewRef.current === null`), matching
523
+ // the CameraHandle docstring — the declarative `overlays` prop is the path
524
+ // that survives AR↔non-AR transitions. The `overlays` prop is also threaded
525
+ // straight to `<ARCameraView>` below, so a host can use either API.
526
+ (0, react_1.useImperativeHandle)(ref, () => ({
527
+ setOverlays: (o) => arViewRef.current?.setOverlays(o),
528
+ addOverlay: (o) => arViewRef.current?.addOverlay(o),
529
+ updateOverlay: (id, patch) => arViewRef.current?.updateOverlay(id, patch),
530
+ removeOverlay: (id) => arViewRef.current?.removeOverlay(id),
531
+ clearOverlays: () => arViewRef.current?.clearOverlays(),
532
+ raycast: () => arViewRef.current?.raycast() ?? Promise.resolve(null),
533
+ }), []);
516
534
  // Effect that does the async transition work whenever the settled
517
535
  // refs disagree with the current isAR/lens. Order matters:
518
536
  // 1. Set the cameraTransitioning state so the gate stays closed
@@ -1425,7 +1443,7 @@ function Camera(props) {
1425
1443
  // (V12.14.8 OOM fix). The CaptureStatusOverlay renders the
1426
1444
  // "Stitching…" state on top, so no placeholder label is needed
1427
1445
  // in that case — only for the camera-switch transition.
1428
- react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] }, statusPhase === 'stitching' ? null : (react_1.default.createElement(react_native_1.Text, { style: styles.transitionLabel }, "Switching camera\u2026")))) : isAR ? (react_1.default.createElement(ARCameraView_1.ARCameraView, { ref: arViewRef, style: react_native_1.StyleSheet.absoluteFill, arFrameProcessor: arFrameProcessor, enableDepth: enableDepth, enableAnchors: enableAnchors, enableMesh: enableMesh, planeDetection: planeDetection, onArFrame: onArFrame, arFrameMetaInterval: arFrameMetaInterval, onArPluginResult: onArPluginResult })) : (react_1.default.createElement(CameraView_1.CameraView, { ref: visionCameraRef, device: capture.device, isActive: true,
1446
+ react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] }, statusPhase === 'stitching' ? null : (react_1.default.createElement(react_native_1.Text, { style: styles.transitionLabel }, "Switching camera\u2026")))) : isAR ? (react_1.default.createElement(ARCameraView_1.ARCameraView, { ref: arViewRef, style: react_native_1.StyleSheet.absoluteFill, arFrameProcessor: arFrameProcessor, enableDepth: enableDepth, enableAnchors: enableAnchors, enableMesh: enableMesh, planeDetection: planeDetection, onArFrame: onArFrame, arFrameMetaInterval: arFrameMetaInterval, onArPluginResult: onArPluginResult, overlays: overlays })) : (react_1.default.createElement(CameraView_1.CameraView, { ref: visionCameraRef, device: capture.device, isActive: true,
1429
1447
  // `video={true}` is REQUIRED for takeSnapshot to work on iOS.
1430
1448
  // vision-camera v4's iOS implementation of takeSnapshot waits
1431
1449
  // for a frame on the video pipeline; with video disabled, the
@@ -1590,7 +1608,7 @@ function Camera(props) {
1590
1608
  setCropPending(null);
1591
1609
  }
1592
1610
  } })));
1593
- }
1611
+ });
1594
1612
  function noop() {
1595
1613
  /* no-op handler used when panorama mode is disabled */
1596
1614
  }
@@ -0,0 +1,52 @@
1
+ import type { AROverlay } from '../stitching/AROverlay';
2
+ /**
3
+ * The imperative overlay methods exposed on both `<ARCameraView>` and
4
+ * `<Camera>` refs. Identical shape on both so a host can swap components
5
+ * without rewriting overlay code.
6
+ */
7
+ export interface AROverlayMethods {
8
+ /** Replace the entire JS-set overlay collection. */
9
+ setOverlays: (overlays: AROverlay[]) => void;
10
+ /** Add one overlay (replaces any existing overlay with the same `id`). */
11
+ addOverlay: (overlay: AROverlay) => void;
12
+ /**
13
+ * Shallow-merge a patch into the overlay with `id`. No-op if no overlay
14
+ * with that `id` is currently set.
15
+ */
16
+ updateOverlay: (id: string, patch: Partial<AROverlay>) => void;
17
+ /** Remove the overlay with `id` (no-op if absent). */
18
+ removeOverlay: (id: string) => void;
19
+ /** Remove all JS-set overlays. */
20
+ clearOverlays: () => void;
21
+ /**
22
+ * Raycast from the screen centre (the crosshair) to the first real-world
23
+ * surface and resolve its world position `[x, y, z]` in metres (ARKit/ARCore
24
+ * world frame), or `null` when nothing is hit (e.g. a featureless wall before
25
+ * any plane is detected). Use it to place an overlay ON the aimed surface at
26
+ * the real distance — pass the result as a `worldPosition` to
27
+ * {@link setOverlays} / {@link addOverlay} — instead of guessing a distance.
28
+ * Resolves `null` (never throws) when the native module / method is absent.
29
+ */
30
+ raycast: () => Promise<[number, number, number] | null>;
31
+ }
32
+ /** The `RNSARSession` native-module method name overlays dispatch through. */
33
+ export declare const AR_OVERLAY_SET_METHOD: "setOverlays";
34
+ /**
35
+ * The agreed UIManager view-command name for native sides that drive overlays
36
+ * via per-view command dispatch instead of the module method. The JS layer
37
+ * dispatches through the module method (there's only one AR view), but the
38
+ * name is pinned here so the native side can match if it chooses commands.
39
+ */
40
+ export declare const AR_OVERLAY_VIEW_COMMAND: "RNSARCameraViewOverlays";
41
+ /**
42
+ * Build an overlay controller backed by an in-memory ordered set keyed by
43
+ * `id`. Every mutating call resolves the new full array and pushes it to
44
+ * native via `RNSARSession.setOverlays`. The controller is the single source
45
+ * of truth for BOTH the imperative ref methods and the declarative `overlays`
46
+ * prop (the prop's effect calls `setOverlays` with the prop value).
47
+ */
48
+ export declare function createAROverlayController(): AROverlayMethods & {
49
+ /** Current JS-set overlays in insertion order (used by tests / diffing). */
50
+ getOverlays: () => AROverlay[];
51
+ };
52
+ //# sourceMappingURL=arOverlayController.d.ts.map
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.AR_OVERLAY_VIEW_COMMAND = exports.AR_OVERLAY_SET_METHOD = void 0;
5
+ exports.createAROverlayController = createAROverlayController;
6
+ /**
7
+ * v0.20.0 — shared JS→native plumbing for the AR overlay renderer.
8
+ *
9
+ * `<ARCameraView>` and `<Camera>` expose an IDENTICAL imperative overlay API
10
+ * (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` /
11
+ * `clearOverlays`) plus a declarative `overlays` prop. Rather than duplicate
12
+ * the diff + native-dispatch logic in each component, both build their handle
13
+ * from {@link createAROverlayController} — DRY, single source of truth for the
14
+ * wire format and the merge-by-id semantics.
15
+ *
16
+ * ## Native mechanism (agreed cross-platform contract)
17
+ *
18
+ * Every AR-session setting in this SDK already flows through the
19
+ * `RNSARSession` native-module singleton (`setPlaneDetection`,
20
+ * `setArFrameMetaEnabled`, `setSceneReconstructionEnabled`) because
21
+ * `RNSARSession.shared` drives the single mounted `RNSARCameraView`. Overlays
22
+ * follow the same pattern: a single `setOverlays(overlays)` method on
23
+ * `RNSARSession` carries the FULL current JS-set overlay array each time it
24
+ * changes. Native replaces its JS-set overlay collection wholesale (the merge
25
+ * with the namespaced native-plugin set happens on the native side) and the
26
+ * overlay layer redraws every AR frame.
27
+ *
28
+ * The declarative `overlays` prop and the imperative methods both ultimately
29
+ * call this same `setOverlays` with the resolved array, so the two APIs are
30
+ * interchangeable and can't diverge.
31
+ *
32
+ * Why a module method (not a UIManager view command)? It matches every other
33
+ * AR setting in this codebase and there is only ever ONE `RNSARCameraView`
34
+ * mounted (ARKit/ARCore can't share the camera), so there's nothing to key by
35
+ * view tag. The equivalent UIManager view-command name, for native sides that
36
+ * prefer per-view dispatch, is documented as `RNSARCameraViewOverlays`.
37
+ */
38
+ const react_native_1 = require("react-native");
39
+ /** The `RNSARSession` native-module method name overlays dispatch through. */
40
+ exports.AR_OVERLAY_SET_METHOD = 'setOverlays';
41
+ /**
42
+ * The agreed UIManager view-command name for native sides that drive overlays
43
+ * via per-view command dispatch instead of the module method. The JS layer
44
+ * dispatches through the module method (there's only one AR view), but the
45
+ * name is pinned here so the native side can match if it chooses commands.
46
+ */
47
+ exports.AR_OVERLAY_VIEW_COMMAND = 'RNSARCameraViewOverlays';
48
+ /**
49
+ * Build an overlay controller backed by an in-memory ordered set keyed by
50
+ * `id`. Every mutating call resolves the new full array and pushes it to
51
+ * native via `RNSARSession.setOverlays`. The controller is the single source
52
+ * of truth for BOTH the imperative ref methods and the declarative `overlays`
53
+ * prop (the prop's effect calls `setOverlays` with the prop value).
54
+ */
55
+ function createAROverlayController() {
56
+ // Insertion-ordered map: preserves the order overlays were added so the
57
+ // native render order is stable + predictable.
58
+ const overlaysById = new Map();
59
+ const flush = () => {
60
+ const native = react_native_1.NativeModules
61
+ .RNSARSession;
62
+ // Native module / method unavailable (web, or a native build predating the
63
+ // overlay channel): no-op, no crash — mirrors the other AR setters.
64
+ const ret = native?.setOverlays?.(Array.from(overlaysById.values()));
65
+ // iOS returns a Promise (the native method is Promise-typed); swallow any
66
+ // rejection so a transient native error never surfaces as an unhandled
67
+ // rejection. Android returns void — the optional chain skips the catch.
68
+ ret?.catch?.(() => undefined);
69
+ };
70
+ return {
71
+ getOverlays: () => Array.from(overlaysById.values()),
72
+ setOverlays: (overlays) => {
73
+ overlaysById.clear();
74
+ for (const o of overlays) {
75
+ // Last-writer-wins on duplicate ids in the incoming array.
76
+ overlaysById.set(o.id, o);
77
+ }
78
+ flush();
79
+ },
80
+ addOverlay: (overlay) => {
81
+ // Re-set to move an existing id to the end? No — preserve original slot
82
+ // by deleting first only when absent. Map#set keeps the existing slot
83
+ // when the key already exists, so a plain set is the right "replace in
84
+ // place" behaviour.
85
+ overlaysById.set(overlay.id, overlay);
86
+ flush();
87
+ },
88
+ updateOverlay: (id, patch) => {
89
+ const existing = overlaysById.get(id);
90
+ if (existing == null) {
91
+ return;
92
+ }
93
+ // Shallow-merge; `id` is preserved from the existing overlay regardless
94
+ // of what the patch carries (the map key must stay consistent).
95
+ overlaysById.set(id, { ...existing, ...patch, id });
96
+ flush();
97
+ },
98
+ removeOverlay: (id) => {
99
+ if (overlaysById.delete(id)) {
100
+ flush();
101
+ }
102
+ },
103
+ clearOverlays: () => {
104
+ if (overlaysById.size > 0) {
105
+ overlaysById.clear();
106
+ flush();
107
+ }
108
+ },
109
+ raycast: async () => {
110
+ const native = react_native_1.NativeModules
111
+ .RNSARSession;
112
+ const fn = native?.raycast;
113
+ // Native module / method unavailable (web, or a native build predating
114
+ // the raycast channel): resolve null — the caller falls back.
115
+ if (typeof fn !== 'function') {
116
+ return null;
117
+ }
118
+ try {
119
+ const res = await fn();
120
+ const wp = res?.worldPosition;
121
+ if (Array.isArray(wp) && wp.length >= 3) {
122
+ return [Number(wp[0]), Number(wp[1]), Number(wp[2])];
123
+ }
124
+ return null;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ },
130
+ };
131
+ }
132
+ //# sourceMappingURL=arOverlayController.js.map
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * adds RetaiLens-specific features on top.
21
21
  */
22
22
  export { Camera, CameraError } from './camera/Camera';
23
- export type { CameraProps, CameraCaptureResult, PanoramaCaptureResult, CameraErrorCode, CaptureSource, CaptureSourcesMode, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
23
+ export type { CameraProps, CameraHandle, CameraCaptureResult, PanoramaCaptureResult, CameraErrorCode, CaptureSource, CaptureSourcesMode, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
24
24
  export type { CaptureWarning, CaptureWarningCode, CaptureWarningCopy, } from './camera/captureWarnings';
25
25
  export { DEFAULT_CAPTURE_WARNING_COPY } from './camera/captureWarnings';
26
26
  export { userFacingStitchError, RECOVERABLE_STITCH_GUIDANCE, RECOVERABLE_STITCH_CODES, } from './camera/cameraErrorMessages';
@@ -94,6 +94,9 @@ export { useKeyframeStream } from './stitching/useKeyframeStream';
94
94
  export type { CameraFrame, CameraFrameProcessor, ARAnchor, } from './stitching/CameraFrame';
95
95
  export type { ARFrameMeta } from './stitching/ARFrameMeta';
96
96
  export type { ARPluginResult } from './stitching/ARFrameMeta';
97
+ export type { AROverlay } from './stitching/AROverlay';
98
+ export type { AROverlayMethods } from './camera/arOverlayController';
99
+ export { AR_OVERLAY_SET_METHOD, AR_OVERLAY_VIEW_COMMAND, } from './camera/arOverlayController';
97
100
  export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
98
101
  export type { UseFrameProcessorDriverOptions, FrameProcessorDriverHandle, } from './stitching/useFrameProcessorDriver';
99
102
  export { useStitcherWorklet } from './stitching/useStitcherWorklet';
package/dist/index.js CHANGED
@@ -22,8 +22,8 @@
22
22
  * adds RetaiLens-specific features on top.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.useFrameProcessorDriver = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.cropQuad = exports.RectCropPreview = exports.LateralMotionModal = exports.CaptureFrameCounterOverlay = exports.CaptureCountdownOverlay = exports.PanHowToOverlay = exports.RotateToLandscapePrompt = exports.usePanMotion = exports.DEFAULT_GUIDANCE_COPY = exports.OrientationDriftModal = exports.useOrientationDrift = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaBandOverlay = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.RECOVERABLE_STITCH_CODES = exports.RECOVERABLE_STITCH_GUIDANCE = exports.userFacingStitchError = exports.DEFAULT_CAPTURE_WARNING_COPY = exports.CameraError = exports.Camera = void 0;
26
- exports.stitchVideo = exports.useStitcherWorklet = void 0;
25
+ exports.AR_OVERLAY_SET_METHOD = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.cropQuad = exports.RectCropPreview = exports.LateralMotionModal = exports.CaptureFrameCounterOverlay = exports.CaptureCountdownOverlay = exports.PanHowToOverlay = exports.RotateToLandscapePrompt = exports.usePanMotion = exports.DEFAULT_GUIDANCE_COPY = exports.OrientationDriftModal = exports.useOrientationDrift = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaBandOverlay = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.RECOVERABLE_STITCH_CODES = exports.RECOVERABLE_STITCH_GUIDANCE = exports.userFacingStitchError = exports.DEFAULT_CAPTURE_WARNING_COPY = exports.CameraError = exports.Camera = void 0;
26
+ exports.stitchVideo = exports.useStitcherWorklet = exports.useFrameProcessorDriver = exports.AR_OVERLAY_VIEW_COMMAND = void 0;
27
27
  // ─────────────────────────────────────────────────────────────────────
28
28
  // Layer 1 — the high-level <Camera> component
29
29
  // ─────────────────────────────────────────────────────────────────────
@@ -189,6 +189,9 @@ Object.defineProperty(exports, "useIncrementalStitcher", { enumerable: true, get
189
189
  // keyframe, packet detection, server-side analysis, etc.).
190
190
  var useKeyframeStream_1 = require("./stitching/useKeyframeStream");
191
191
  Object.defineProperty(exports, "useKeyframeStream", { enumerable: true, get: function () { return useKeyframeStream_1.useKeyframeStream; } });
192
+ var arOverlayController_1 = require("./camera/arOverlayController");
193
+ Object.defineProperty(exports, "AR_OVERLAY_SET_METHOD", { enumerable: true, get: function () { return arOverlayController_1.AR_OVERLAY_SET_METHOD; } });
194
+ Object.defineProperty(exports, "AR_OVERLAY_VIEW_COMMAND", { enumerable: true, get: function () { return arOverlayController_1.AR_OVERLAY_VIEW_COMMAND; } });
192
195
  // NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
193
196
  // `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
194
197
  // archived in the batch-keyframe cleanup — they drove the third-party
@@ -0,0 +1,97 @@
1
+ /**
2
+ * v0.20.0 — the AR OVERLAY / ANNOTATION renderer's data model.
3
+ *
4
+ * An {@link AROverlay} describes a 2D shape (a billboard marker/box or a
5
+ * world-anchored quad) that the native overlay layer draws ON TOP of the AR
6
+ * camera preview (`RNSARCameraView`). Each overlay is anchored to WORLD
7
+ * positions and REPROJECTED to screen on EVERY AR frame from the current
8
+ * camera pose + intrinsics — so it tracks the scene at display rate with no
9
+ * 3D-engine dependency.
10
+ *
11
+ * ## Two ways to anchor an overlay
12
+ *
13
+ * 1. **A single world point** (`worldPosition`) — drawn as a billboard
14
+ * marker/box facing the camera, sized by `sizeMeters` (default a small
15
+ * marker). Use this for a pin on a detected plane anchor, a label on a
16
+ * point of interest, etc.
17
+ * 2. **Explicit world corners** (`worldQuad`, 3–4 points) — drawn as the
18
+ * outline/box connecting the projected corners. Use this for a detected
19
+ * quad (a shelf face, a packet, a door) whose real-world shape you
20
+ * already know.
21
+ *
22
+ * Provide ONE of the two anchor forms. If both are present `worldQuad` wins
23
+ * (it's the more specific description); the native renderers read `worldQuad`
24
+ * first and fall back to `worldPosition` + `sizeMeters`.
25
+ *
26
+ * ## Rendering / reprojection (native)
27
+ *
28
+ * The native side reprojects each overlay's world point(s) to screen with the
29
+ * AR framework's BUILT-IN, correct projection — iOS
30
+ * `ARFrame.camera.projectPoint(_:orientation:viewportSize:)`, Android
31
+ * `viewMatrix · projectionMatrix` → clip → NDC → screen. Points behind the
32
+ * camera or off-screen are hidden. The layer redraws every frame so the
33
+ * outline/box + label stay pinned to the world as the camera moves.
34
+ *
35
+ * ## Where overlays come from
36
+ *
37
+ * Overlays reach the native renderer through two INDEPENDENT, merged sets:
38
+ *
39
+ * - **JS-set** — the declarative `overlays` prop or the imperative ref
40
+ * methods (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay`
41
+ * / `clearOverlays`) on `<Camera>` and `<ARCameraView>`.
42
+ * - **Native-plugin-set** — a registered AR plugin places overlays directly
43
+ * via the 0.19 registry (`RNISARPluginRegistry.setOverlays(...)` on iOS /
44
+ * `RNSARPluginRegistry.setOverlays(...)` on Android), with zero JS latency.
45
+ *
46
+ * The native renderer draws the UNION of both sets; the plugin set is
47
+ * namespaced so a JS `setOverlays(...)` never clobbers plugin overlays.
48
+ */
49
+ export interface AROverlay {
50
+ /**
51
+ * Stable identifier. The declarative `overlays` prop diffs the incoming
52
+ * array against the current set BY `id` (add / update / remove); the
53
+ * imperative `updateOverlay` / `removeOverlay` methods key off it too. Must
54
+ * be unique within a set.
55
+ */
56
+ id: string;
57
+ /**
58
+ * Anchor form 1 — a single world point in METRES (world space `[x, y, z]`).
59
+ * Drawn as a billboard marker/box of `sizeMeters` extent facing the camera.
60
+ * Ignored when `worldQuad` is provided.
61
+ */
62
+ worldPosition?: [number, number, number];
63
+ /**
64
+ * Box extent in METRES `[width, height]` at `worldPosition`. Only meaningful
65
+ * with `worldPosition`. Defaults to a small marker on the native side when
66
+ * omitted.
67
+ */
68
+ sizeMeters?: [number, number];
69
+ /**
70
+ * Anchor form 2 — 3 or 4 explicit world corners in METRES (e.g. a detected
71
+ * quad). Each corner is `[x, y, z]` in world space. Drawn as the
72
+ * outline/box connecting the projected corners. Takes precedence over
73
+ * `worldPosition` + `sizeMeters` when both are present.
74
+ */
75
+ worldQuad?: Array<[number, number, number]>;
76
+ /**
77
+ * Draw style. Default `'outline'` (stroked edges). `'box'` is a filled /
78
+ * boxed marker. Both render in 2D this release.
79
+ */
80
+ shape?: 'box' | 'outline';
81
+ /** Optional text label drawn at the overlay's anchor point. */
82
+ label?: string;
83
+ /**
84
+ * Stroke / fill colour as a hex string (e.g. `'#00E5FF'`). Defaults to a
85
+ * theme colour on the native side when omitted.
86
+ */
87
+ color?: string;
88
+ /**
89
+ * Render mode. Default `'2d'` — a flat shape reprojected to screen.
90
+ * `'3d'` is SCAFFOLD ONLY this release: the data-model field exists and the
91
+ * native renderers leave a marked hook for a future SceneKit (iOS) / Android
92
+ * 3D renderer, but v1 treats `'3d'` as `'2d'` (with a one-time native log
93
+ * warning). Document-only forward compatibility.
94
+ */
95
+ mode?: '2d' | '3d';
96
+ }
97
+ //# sourceMappingURL=AROverlay.d.ts.map
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ //# sourceMappingURL=AROverlay.js.map
@@ -21,13 +21,20 @@
21
21
 
22
22
  @interface RCT_EXTERN_MODULE(RNSARCameraViewManager, RCTViewManager)
23
23
 
24
- // No exposed view props for Phase 4.4 the view's behaviour is
25
- // fully driven by mount/unmount lifecycle (the AR session
26
- // starts/stops automatically when the view enters/leaves the
27
- // window hierarchy). Future phases may add props for:
28
- // - tracking-state HUD visibility
29
- // - exposure / focus controls
30
- // - debug overlay (feature points, planes)
31
- // Each of these will land here as RCT_EXPORT_VIEW_PROPERTY lines.
24
+ // v0.20.0 declarative AR overlay set. React-state-driven array of
25
+ // overlay dictionaries (the JS `AROverlay[]` shape). RN sets this via KVC
26
+ // on the VIEW (`RNSARCameraView.overlays`); the view's `@objc` setter
27
+ // forwards the array to the JS namespace of `RNISAROverlayStore.shared`,
28
+ // which the per-frame draw view reprojects + strokes. (We forward through
29
+ // the view rather than store per-view state because the overlay set is
30
+ // global to the single AR session.)
31
+ //
32
+ // The IMPERATIVE overlay API (setOverlays / addOverlay / updateOverlay /
33
+ // removeOverlay / clearOverlays on the ref) is NOT a view command — per
34
+ // the shared contract (src/camera/arOverlayController.ts) it dispatches
35
+ // through the `RNSARSession.setOverlays(_:)` native MODULE method (see
36
+ // ARSessionBridge.{swift,m}), matching every other AR setting. Only the
37
+ // declarative prop lives on the view manager.
38
+ RCT_EXPORT_VIEW_PROPERTY(overlays, NSArray)
32
39
 
33
40
  @end
@@ -36,5 +36,27 @@ public final class RNSARCameraViewManager: RCTViewManager {
36
36
  public override class func requiresMainQueueSetup() -> Bool {
37
37
  return true
38
38
  }
39
+
40
+ // MARK: - v0.20.0 — AR overlays
41
+ //
42
+ // OVERLAY WIRE PATH (shared cross-platform contract, see
43
+ // `src/camera/arOverlayController.ts`): the JS imperative methods
44
+ // (setOverlays / addOverlay / updateOverlay / removeOverlay /
45
+ // clearOverlays) AND the declarative `overlays` prop both resolve, in
46
+ // JS, to the FULL current overlay array and dispatch it through the
47
+ // `RNSARSession.setOverlays(_:)` native MODULE method (see
48
+ // `ARSessionBridge.swift`). Native replaces its JS-overlay namespace
49
+ // in `RNISAROverlayStore` wholesale and merges with the SEPARATE
50
+ // plugin-overlay namespace (`RNISARPluginRegistry.setOverlays`) — the
51
+ // draw view renders the UNION every ARFrame.
52
+ //
53
+ // The module method is the chosen mechanism (matching every other AR
54
+ // setting: setPlaneDetection / setArFrameMetaEnabled /
55
+ // setSceneReconstructionEnabled) because there is exactly ONE
56
+ // `RNSARCameraView` mounted (ARKit can't share the camera), so there's
57
+ // nothing to key by view tag. We ALSO honor the declarative
58
+ // `overlays` view prop here on the view (`RNSARCameraView.overlays`
59
+ // KVC setter) so a host can drive overlays purely declaratively
60
+ // without the module — both land in the same store namespace.
39
61
  }
40
62
  #endif
@@ -56,6 +56,20 @@ RCT_EXTERN_METHOD(setArFrameMetaEnabled:(nonnull NSNumber *)enabled
56
56
  resolver:(RCTPromiseResolveBlock)resolver
57
57
  rejecter:(RCTPromiseRejectBlock)rejecter)
58
58
 
59
+ // v0.20.0 — AR overlay renderer. Replace the entire JS-set overlay
60
+ // collection (the shared arOverlayController sends the full array every
61
+ // mutation). Native merges with the namespaced plugin-overlay set + the
62
+ // RNSARCameraView draw view reprojects them each ARFrame.
63
+ RCT_EXTERN_METHOD(setOverlays:(nonnull NSArray *)overlays
64
+ resolver:(RCTPromiseResolveBlock)resolver
65
+ rejecter:(RCTPromiseRejectBlock)rejecter)
66
+
67
+ // v0.20.0 — raycast from the crosshair (screen centre) to the first real
68
+ // surface hit → { worldPosition: [x,y,z] } or null. Used to place an
69
+ // overlay ON the aimed surface (then anchor it), vs a guessed distance ahead.
70
+ RCT_EXTERN_METHOD(raycast:(RCTPromiseResolveBlock)resolver
71
+ rejecter:(RCTPromiseRejectBlock)rejecter)
72
+
59
73
  RCT_EXTERN_METHOD(snapshotPoseLog:(RCTPromiseResolveBlock)resolver
60
74
  rejecter:(RCTPromiseRejectBlock)rejecter)
61
75