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
@@ -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,
@@ -272,6 +273,20 @@ export type { ARFrameMeta } from './stitching/ARFrameMeta';
272
273
  // `onArPluginResult` callback (a plugin's out-of-band `registry.emit(...)`
273
274
  // result). The SDK ships only the generic framework — no built-in plugins.
274
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';
275
290
  // NOTE: the host-worklet / frame-stream hooks `useFrameProcessor`,
276
291
  // `useThrottledFrameProcessor` and `useFrameStream` (v0.8–v0.9) were
277
292
  // archived in the batch-keyframe cleanup — they drove the third-party
@@ -0,0 +1,105 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ /**
4
+ * v0.20.0 — the AR OVERLAY / ANNOTATION renderer's data model.
5
+ *
6
+ * An {@link AROverlay} describes a 2D shape (a billboard marker/box or a
7
+ * world-anchored quad) that the native overlay layer draws ON TOP of the AR
8
+ * camera preview (`RNSARCameraView`). Each overlay is anchored to WORLD
9
+ * positions and REPROJECTED to screen on EVERY AR frame from the current
10
+ * camera pose + intrinsics — so it tracks the scene at display rate with no
11
+ * 3D-engine dependency.
12
+ *
13
+ * ## Two ways to anchor an overlay
14
+ *
15
+ * 1. **A single world point** (`worldPosition`) — drawn as a billboard
16
+ * marker/box facing the camera, sized by `sizeMeters` (default a small
17
+ * marker). Use this for a pin on a detected plane anchor, a label on a
18
+ * point of interest, etc.
19
+ * 2. **Explicit world corners** (`worldQuad`, 3–4 points) — drawn as the
20
+ * outline/box connecting the projected corners. Use this for a detected
21
+ * quad (a shelf face, a packet, a door) whose real-world shape you
22
+ * already know.
23
+ *
24
+ * Provide ONE of the two anchor forms. If both are present `worldQuad` wins
25
+ * (it's the more specific description); the native renderers read `worldQuad`
26
+ * first and fall back to `worldPosition` + `sizeMeters`.
27
+ *
28
+ * ## Rendering / reprojection (native)
29
+ *
30
+ * The native side reprojects each overlay's world point(s) to screen with the
31
+ * AR framework's BUILT-IN, correct projection — iOS
32
+ * `ARFrame.camera.projectPoint(_:orientation:viewportSize:)`, Android
33
+ * `viewMatrix · projectionMatrix` → clip → NDC → screen. Points behind the
34
+ * camera or off-screen are hidden. The layer redraws every frame so the
35
+ * outline/box + label stay pinned to the world as the camera moves.
36
+ *
37
+ * ## Where overlays come from
38
+ *
39
+ * Overlays reach the native renderer through two INDEPENDENT, merged sets:
40
+ *
41
+ * - **JS-set** — the declarative `overlays` prop or the imperative ref
42
+ * methods (`setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay`
43
+ * / `clearOverlays`) on `<Camera>` and `<ARCameraView>`.
44
+ * - **Native-plugin-set** — a registered AR plugin places overlays directly
45
+ * via the 0.19 registry (`RNISARPluginRegistry.setOverlays(...)` on iOS /
46
+ * `RNSARPluginRegistry.setOverlays(...)` on Android), with zero JS latency.
47
+ *
48
+ * The native renderer draws the UNION of both sets; the plugin set is
49
+ * namespaced so a JS `setOverlays(...)` never clobbers plugin overlays.
50
+ */
51
+ export interface AROverlay {
52
+ /**
53
+ * Stable identifier. The declarative `overlays` prop diffs the incoming
54
+ * array against the current set BY `id` (add / update / remove); the
55
+ * imperative `updateOverlay` / `removeOverlay` methods key off it too. Must
56
+ * be unique within a set.
57
+ */
58
+ id: string;
59
+
60
+ /**
61
+ * Anchor form 1 — a single world point in METRES (world space `[x, y, z]`).
62
+ * Drawn as a billboard marker/box of `sizeMeters` extent facing the camera.
63
+ * Ignored when `worldQuad` is provided.
64
+ */
65
+ worldPosition?: [number, number, number];
66
+
67
+ /**
68
+ * Box extent in METRES `[width, height]` at `worldPosition`. Only meaningful
69
+ * with `worldPosition`. Defaults to a small marker on the native side when
70
+ * omitted.
71
+ */
72
+ sizeMeters?: [number, number];
73
+
74
+ /**
75
+ * Anchor form 2 — 3 or 4 explicit world corners in METRES (e.g. a detected
76
+ * quad). Each corner is `[x, y, z]` in world space. Drawn as the
77
+ * outline/box connecting the projected corners. Takes precedence over
78
+ * `worldPosition` + `sizeMeters` when both are present.
79
+ */
80
+ worldQuad?: Array<[number, number, number]>;
81
+
82
+ /**
83
+ * Draw style. Default `'outline'` (stroked edges). `'box'` is a filled /
84
+ * boxed marker. Both render in 2D this release.
85
+ */
86
+ shape?: 'box' | 'outline';
87
+
88
+ /** Optional text label drawn at the overlay's anchor point. */
89
+ label?: string;
90
+
91
+ /**
92
+ * Stroke / fill colour as a hex string (e.g. `'#00E5FF'`). Defaults to a
93
+ * theme colour on the native side when omitted.
94
+ */
95
+ color?: string;
96
+
97
+ /**
98
+ * Render mode. Default `'2d'` — a flat shape reprojected to screen.
99
+ * `'3d'` is SCAFFOLD ONLY this release: the data-model field exists and the
100
+ * native renderers leave a marked hook for a future SceneKit (iOS) / Android
101
+ * 3D renderer, but v1 treats `'3d'` as `'2d'` (with a one-time native log
102
+ * warning). Document-only forward compatibility.
103
+ */
104
+ mode?: '2d' | '3d';
105
+ }