react-native-image-stitcher 0.1.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 +96 -0
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +189 -0
- package/RNImageStitcher.podspec +76 -0
- package/android/build.gradle +224 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +124 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
- package/cpp/ar_frame_pose.h +63 -0
- package/cpp/keyframe_gate.cpp +927 -0
- package/cpp/keyframe_gate.hpp +240 -0
- package/cpp/stitcher.cpp +2207 -0
- package/cpp/stitcher.hpp +275 -0
- package/dist/ar/useARSession.d.ts +102 -0
- package/dist/ar/useARSession.js +133 -0
- package/dist/camera/ARCameraView.d.ts +93 -0
- package/dist/camera/ARCameraView.js +170 -0
- package/dist/camera/Camera.d.ts +134 -0
- package/dist/camera/Camera.js +688 -0
- package/dist/camera/CameraShutter.d.ts +80 -0
- package/dist/camera/CameraShutter.js +237 -0
- package/dist/camera/CameraView.d.ts +65 -0
- package/dist/camera/CameraView.js +117 -0
- package/dist/camera/CaptureControlsBar.d.ts +87 -0
- package/dist/camera/CaptureControlsBar.js +82 -0
- package/dist/camera/CaptureHeader.d.ts +62 -0
- package/dist/camera/CaptureHeader.js +81 -0
- package/dist/camera/CapturePreview.d.ts +70 -0
- package/dist/camera/CapturePreview.js +188 -0
- package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
- package/dist/camera/CaptureStatusOverlay.js +326 -0
- package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
- package/dist/camera/CaptureThumbnailStrip.js +177 -0
- package/dist/camera/IncrementalPanGuide.d.ts +83 -0
- package/dist/camera/IncrementalPanGuide.js +267 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
- package/dist/camera/PanoramaBandOverlay.js +399 -0
- package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
- package/dist/camera/PanoramaConfirmModal.js +128 -0
- package/dist/camera/PanoramaGuidance.d.ts +79 -0
- package/dist/camera/PanoramaGuidance.js +246 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
- package/dist/camera/PanoramaSettingsModal.js +611 -0
- package/dist/camera/ViewportCropOverlay.d.ts +46 -0
- package/dist/camera/ViewportCropOverlay.js +67 -0
- package/dist/camera/useCapture.d.ts +111 -0
- package/dist/camera/useCapture.js +160 -0
- package/dist/camera/useDeviceOrientation.d.ts +48 -0
- package/dist/camera/useDeviceOrientation.js +131 -0
- package/dist/camera/useVideoCapture.d.ts +79 -0
- package/dist/camera/useVideoCapture.js +151 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +39 -0
- package/dist/quality/normaliseOrientation.d.ts +36 -0
- package/dist/quality/normaliseOrientation.js +62 -0
- package/dist/quality/runQualityCheck.d.ts +41 -0
- package/dist/quality/runQualityCheck.js +98 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
- package/dist/sensors/useIMUTranslationGate.js +235 -0
- package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
- package/dist/stitching/IncrementalStitcherView.js +157 -0
- package/dist/stitching/incremental.d.ts +930 -0
- package/dist/stitching/incremental.js +133 -0
- package/dist/stitching/stitchFrames.d.ts +55 -0
- package/dist/stitching/stitchFrames.js +56 -0
- package/dist/stitching/stitchVideo.d.ts +119 -0
- package/dist/stitching/stitchVideo.js +57 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
- package/dist/stitching/useIncrementalJSDriver.js +199 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
- package/dist/stitching/useIncrementalStitcher.js +172 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +15 -0
- package/ios/Package.swift +72 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
- package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
- package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
- package/package.json +73 -0
- package/react-native.config.js +34 -0
- package/scripts/opencv-version.txt +1 -0
- package/scripts/postinstall-fetch-binaries.js +286 -0
- package/src/ar/useARSession.ts +210 -0
- package/src/camera/.gitkeep +0 -0
- package/src/camera/ARCameraView.tsx +256 -0
- package/src/camera/Camera.tsx +1053 -0
- package/src/camera/CameraShutter.tsx +292 -0
- package/src/camera/CameraView.tsx +157 -0
- package/src/camera/CaptureControlsBar.tsx +204 -0
- package/src/camera/CaptureHeader.tsx +184 -0
- package/src/camera/CapturePreview.tsx +318 -0
- package/src/camera/CaptureStatusOverlay.tsx +391 -0
- package/src/camera/CaptureThumbnailStrip.tsx +277 -0
- package/src/camera/IncrementalPanGuide.tsx +328 -0
- package/src/camera/PanoramaBandOverlay.tsx +498 -0
- package/src/camera/PanoramaConfirmModal.tsx +206 -0
- package/src/camera/PanoramaGuidance.tsx +327 -0
- package/src/camera/PanoramaSettingsModal.tsx +1357 -0
- package/src/camera/ViewportCropOverlay.tsx +81 -0
- package/src/camera/useCapture.ts +279 -0
- package/src/camera/useDeviceOrientation.ts +140 -0
- package/src/camera/useVideoCapture.ts +236 -0
- package/src/index.ts +53 -0
- package/src/quality/.gitkeep +0 -0
- package/src/quality/normaliseOrientation.ts +79 -0
- package/src/quality/runQualityCheck.ts +131 -0
- package/src/sensors/useIMUTranslationGate.ts +347 -0
- package/src/stitching/.gitkeep +0 -0
- package/src/stitching/IncrementalStitcherView.tsx +198 -0
- package/src/stitching/incremental.ts +1021 -0
- package/src/stitching/stitchFrames.ts +88 -0
- package/src/stitching/stitchVideo.ts +153 -0
- package/src/stitching/useIncrementalJSDriver.ts +273 -0
- package/src/stitching/useIncrementalStitcher.ts +252 -0
- package/src/types.ts +78 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useIncrementalJSDriver — vision-camera + gyro frame driver for
|
|
5
|
+
* the incremental panorama engine, used in non-AR captures on both
|
|
6
|
+
* iOS and Android.
|
|
7
|
+
*
|
|
8
|
+
* History: previously called `useIncrementalAndroidDriver` because
|
|
9
|
+
* it was Android-only. As of 2026-05-17 (Issue #2), the native
|
|
10
|
+
* `processFrameAtPath` entry point exists on both platforms and the
|
|
11
|
+
* hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
|
|
12
|
+
* that.
|
|
13
|
+
*
|
|
14
|
+
* Why this exists
|
|
15
|
+
* In AR captures the engine consumes frames from the ARSession
|
|
16
|
+
* stream natively (60 Hz pose + image delivery, zero JS
|
|
17
|
+
* involvement once started). In NON-AR captures there is no AR
|
|
18
|
+
* session — vision-camera owns the camera — so the engine needs
|
|
19
|
+
* another frame source. This hook fills the gap:
|
|
20
|
+
*
|
|
21
|
+
* - vision-camera keeps the camera viewport
|
|
22
|
+
* - `takeSnapshot()` runs at ~250 ms intervals during press-hold
|
|
23
|
+
* - `react-native-sensors` gyroscope is integrated to estimate
|
|
24
|
+
* cumulative yaw/pitch (drives the FoV-overlap gate)
|
|
25
|
+
* - Each snapshot path + integrated pose is fed to
|
|
26
|
+
* `IncrementalStitcher.processFrameAtPath()`
|
|
27
|
+
*
|
|
28
|
+
* Trade-off vs the AR path
|
|
29
|
+
* Gyro integration drifts ~1–2° per minute. Acceptable for the
|
|
30
|
+
* typical 5–15 s shelf pan; not great for ambitious 360° captures.
|
|
31
|
+
* Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
|
|
32
|
+
* frame-selection only — the actual image alignment is feature-
|
|
33
|
+
* matched + RANSAC-fit, so quality of the panorama itself isn't
|
|
34
|
+
* bounded by gyro accuracy.
|
|
35
|
+
*
|
|
36
|
+
* Lifecycle
|
|
37
|
+
* `start({ cameraRef })` enables the loop; `stop()` tears down.
|
|
38
|
+
* Both should be called by the host's hold-start / hold-complete
|
|
39
|
+
* handlers. Safe to call on either platform; the hook only
|
|
40
|
+
* activates inside the start/stop block.
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.useIncrementalJSDriver = useIncrementalJSDriver;
|
|
44
|
+
const react_1 = require("react");
|
|
45
|
+
const react_native_1 = require("react-native");
|
|
46
|
+
const react_native_sensors_1 = require("react-native-sensors");
|
|
47
|
+
function getNativeIncremental() {
|
|
48
|
+
const m = react_native_1.NativeModules['IncrementalStitcher'];
|
|
49
|
+
if (!m || typeof m !== 'object')
|
|
50
|
+
return null;
|
|
51
|
+
return m;
|
|
52
|
+
}
|
|
53
|
+
function useIncrementalJSDriver(options = {}) {
|
|
54
|
+
const { snapshotIntervalMs = 250, gyroIntervalMs = 33, fovHorizDegrees = 65, fovVertDegrees = 50, } = options;
|
|
55
|
+
const intervalRef = (0, react_1.useRef)(null);
|
|
56
|
+
const gyroSubRef = (0, react_1.useRef)(null);
|
|
57
|
+
const cameraRef = (0, react_1.useRef)(null);
|
|
58
|
+
// Integrated pose accumulators, in radians. Reset on each
|
|
59
|
+
// start() call. Y-axis = horizontal pan (yaw), X-axis = vertical
|
|
60
|
+
// pan (pitch). Sign convention matches ARKit: counter-clockwise
|
|
61
|
+
// from above is positive yaw.
|
|
62
|
+
const yawRef = (0, react_1.useRef)(0);
|
|
63
|
+
const pitchRef = (0, react_1.useRef)(0);
|
|
64
|
+
const lastGyroAtRef = (0, react_1.useRef)(null);
|
|
65
|
+
// Single in-flight guard so we don't pile up overlapping snapshot
|
|
66
|
+
// promises on slow devices — if last snapshot hasn't finished
|
|
67
|
+
// when the next interval fires, skip.
|
|
68
|
+
const snapshotInFlightRef = (0, react_1.useRef)(false);
|
|
69
|
+
// Module-level "is the driver active right now" — exposed to the
|
|
70
|
+
// host because the hook itself doesn't trigger re-renders.
|
|
71
|
+
const isRunningRef = (0, react_1.useRef)(false);
|
|
72
|
+
const stop = (0, react_1.useCallback)(() => {
|
|
73
|
+
if (intervalRef.current) {
|
|
74
|
+
clearInterval(intervalRef.current);
|
|
75
|
+
intervalRef.current = null;
|
|
76
|
+
}
|
|
77
|
+
if (gyroSubRef.current) {
|
|
78
|
+
gyroSubRef.current.unsubscribe();
|
|
79
|
+
gyroSubRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
cameraRef.current = null;
|
|
82
|
+
isRunningRef.current = false;
|
|
83
|
+
}, []);
|
|
84
|
+
const start = (0, react_1.useCallback)((cameraRefArg) => {
|
|
85
|
+
// 2026-05-17 (Issue #2) — removed the Android-only platform
|
|
86
|
+
// guard. iOS now also exposes `processFrameAtPath` (see the
|
|
87
|
+
// Swift bridge), so the same driver feeds both platforms in
|
|
88
|
+
// non-AR mode.
|
|
89
|
+
if (isRunningRef.current)
|
|
90
|
+
return;
|
|
91
|
+
const native = getNativeIncremental();
|
|
92
|
+
if (!native)
|
|
93
|
+
return;
|
|
94
|
+
cameraRef.current = cameraRefArg;
|
|
95
|
+
yawRef.current = 0;
|
|
96
|
+
pitchRef.current = 0;
|
|
97
|
+
lastGyroAtRef.current = null;
|
|
98
|
+
snapshotInFlightRef.current = false;
|
|
99
|
+
isRunningRef.current = true;
|
|
100
|
+
// Gyro integration. Each sample carries angular velocity in
|
|
101
|
+
// rad/s; multiply by elapsed time to accumulate angular
|
|
102
|
+
// displacement. Note: the gyro axes are device-local; we use
|
|
103
|
+
// y for yaw and x for pitch on a device held in portrait.
|
|
104
|
+
// Landscape would swap, but the FoV-overlap gate is dominant-
|
|
105
|
+
// axis based on the .mm side, so the convention matters less
|
|
106
|
+
// than consistency.
|
|
107
|
+
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
|
|
108
|
+
gyroSubRef.current = react_native_sensors_1.gyroscope.subscribe({
|
|
109
|
+
next: ({ x, y }) => {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (lastGyroAtRef.current === null) {
|
|
112
|
+
lastGyroAtRef.current = now;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const dt = (now - lastGyroAtRef.current) / 1000.0;
|
|
116
|
+
lastGyroAtRef.current = now;
|
|
117
|
+
yawRef.current += y * dt;
|
|
118
|
+
pitchRef.current += x * dt;
|
|
119
|
+
},
|
|
120
|
+
error: (err) => {
|
|
121
|
+
// eslint-disable-next-line no-console
|
|
122
|
+
console.warn('[useIncrementalJSDriver] gyro error', err);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
// Snapshot loop.
|
|
126
|
+
const tick = async () => {
|
|
127
|
+
if (snapshotInFlightRef.current)
|
|
128
|
+
return;
|
|
129
|
+
const cam = cameraRef.current?.current;
|
|
130
|
+
if (!cam)
|
|
131
|
+
return;
|
|
132
|
+
snapshotInFlightRef.current = true;
|
|
133
|
+
try {
|
|
134
|
+
const snap = await cam.takeSnapshot({ quality: 70 });
|
|
135
|
+
if (!snap?.path)
|
|
136
|
+
return;
|
|
137
|
+
// Synthesise a quaternion from integrated yaw + pitch.
|
|
138
|
+
// Yaw rotates about world Y (gravity), pitch about world X
|
|
139
|
+
// (perpendicular to gravity in the device's frame).
|
|
140
|
+
// Combined as q = q_yaw · q_pitch.
|
|
141
|
+
const halfYaw = yawRef.current / 2;
|
|
142
|
+
const halfPitch = pitchRef.current / 2;
|
|
143
|
+
const cy_ = Math.cos(halfYaw);
|
|
144
|
+
const sy_ = Math.sin(halfYaw);
|
|
145
|
+
const cp = Math.cos(halfPitch);
|
|
146
|
+
const sp = Math.sin(halfPitch);
|
|
147
|
+
// q_yaw = (0, sy, 0, cy)
|
|
148
|
+
// q_pitch = (sp, 0, 0, cp)
|
|
149
|
+
// q = q_yaw * q_pitch:
|
|
150
|
+
const qx = cy_ * sp;
|
|
151
|
+
const qy = sy_ * cp;
|
|
152
|
+
const qz = -sy_ * sp;
|
|
153
|
+
const qw = cy_ * cp;
|
|
154
|
+
// Vision-camera v4 doesn't expose camera intrinsics on
|
|
155
|
+
// Android, so we estimate fx/fy from the snapshot's pixel
|
|
156
|
+
// dimensions + assumed FoV. cx/cy at image centre. This
|
|
157
|
+
// is approximate; the proper Android live path is the
|
|
158
|
+
// ARCameraView, where ARCore gives us the real intrinsics.
|
|
159
|
+
const w = snap.width ?? 1920;
|
|
160
|
+
const h = snap.height ?? 1440;
|
|
161
|
+
const fx = w / (2.0 * Math.tan(((fovHorizDegrees * Math.PI) / 180) / 2));
|
|
162
|
+
const fy = h / (2.0 * Math.tan(((fovVertDegrees * Math.PI) / 180) / 2));
|
|
163
|
+
const cx = w / 2;
|
|
164
|
+
const cy = h / 2;
|
|
165
|
+
await native.processFrameAtPath({
|
|
166
|
+
path: snap.path,
|
|
167
|
+
yaw: yawRef.current,
|
|
168
|
+
pitch: pitchRef.current,
|
|
169
|
+
fovHorizDegrees,
|
|
170
|
+
fovVertDegrees,
|
|
171
|
+
trackingPoor: false,
|
|
172
|
+
qx, qy, qz, qw,
|
|
173
|
+
fx, fy, cx, cy,
|
|
174
|
+
imageWidth: w, imageHeight: h,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
// Swallow per-frame errors so the loop keeps running.
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.warn('[useIncrementalJSDriver] processFrame failed', err);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
snapshotInFlightRef.current = false;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
// Kick off an immediate first frame so the engine doesn't sit
|
|
187
|
+
// idle for the first interval period.
|
|
188
|
+
tick();
|
|
189
|
+
intervalRef.current = setInterval(tick, snapshotIntervalMs);
|
|
190
|
+
}, [snapshotIntervalMs, gyroIntervalMs, fovHorizDegrees, fovVertDegrees]);
|
|
191
|
+
return {
|
|
192
|
+
start,
|
|
193
|
+
stop,
|
|
194
|
+
get isRunning() {
|
|
195
|
+
return isRunningRef.current;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=useIncrementalJSDriver.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useIncrementalStitcher — React hook driving the live panorama
|
|
3
|
+
* engine.
|
|
4
|
+
*
|
|
5
|
+
* Lifecycle:
|
|
6
|
+
* 1. Host calls `useARSession().start()` to put the AR session in
|
|
7
|
+
* tracking mode. (Works for AR-supported devices only — non-AR
|
|
8
|
+
* fallback comes in a later phase.)
|
|
9
|
+
* 2. Host calls `start()` from this hook. The native engine
|
|
10
|
+
* registers itself as the AR session's frame consumer.
|
|
11
|
+
* 3. Native emits a state event for every ARFrame the engine
|
|
12
|
+
* processes (~60 Hz, mostly trivially-skipped). The hook
|
|
13
|
+
* mirrors this into React state so a `<IncrementalStitcherView>`
|
|
14
|
+
* or any other consumer can render the live panorama + UX hints.
|
|
15
|
+
* 4. Host calls `finalize(outputPath)` when the user releases the
|
|
16
|
+
* shutter; resolves with the final panorama path + stats.
|
|
17
|
+
* 5. Host calls `cancel()` if the user dismisses the capture.
|
|
18
|
+
*/
|
|
19
|
+
import { type IncrementalState, type IncrementalStartOptions, type IncrementalFinalizeResult } from './incremental';
|
|
20
|
+
export type IncrementalHint = 'slow-down' | 'scene-uniform' | 'alignment-lost' | 'tracking-poor' | null;
|
|
21
|
+
export interface UseIncrementalStitcherReturn {
|
|
22
|
+
/** Whether the native engine is registered. False = no fallback wiring. */
|
|
23
|
+
isAvailable: boolean;
|
|
24
|
+
/** True between successful `start()` and `finalize()`/`cancel()`. */
|
|
25
|
+
isRunning: boolean;
|
|
26
|
+
/** Latest state pushed by the native engine, or null pre-start. */
|
|
27
|
+
state: IncrementalState | null;
|
|
28
|
+
/**
|
|
29
|
+
* Convenience: which UX hint to show, derived from the latest
|
|
30
|
+
* state.outcome. null when nothing should be shown (silent
|
|
31
|
+
* accepts, skips inside the overlap window).
|
|
32
|
+
*/
|
|
33
|
+
hint: IncrementalHint;
|
|
34
|
+
/**
|
|
35
|
+
* Convenience: 'high' | 'medium' | null based on the last accept.
|
|
36
|
+
* Drives confidence-ring rendering in the live preview.
|
|
37
|
+
*/
|
|
38
|
+
confidenceLevel: 'high' | 'medium' | null;
|
|
39
|
+
/** Begin a new capture. Throws if the AR session isn't running. */
|
|
40
|
+
start: (options?: IncrementalStartOptions) => Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* End the capture and write the final panorama. When `outputPath`
|
|
43
|
+
* is omitted or empty, the native side picks a path under the
|
|
44
|
+
* app's tmp directory and returns it in the result.
|
|
45
|
+
*
|
|
46
|
+
* `captureOrientation` (optional) — pass the user's CURRENT
|
|
47
|
+
* device orientation at finalize time. The engine prefers this
|
|
48
|
+
* value over the start-time snapshot for the bake-rotation pass,
|
|
49
|
+
* so cross-orientation captures (user opened screen in portrait,
|
|
50
|
+
* captured in landscape) bake correctly. Omit to keep the legacy
|
|
51
|
+
* behaviour (start-time orientation).
|
|
52
|
+
*/
|
|
53
|
+
finalize: (outputPath?: string, quality?: number, captureOrientation?: string) => Promise<IncrementalFinalizeResult>;
|
|
54
|
+
/** Abort the capture without producing output. */
|
|
55
|
+
cancel: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function useIncrementalStitcher(): UseIncrementalStitcherReturn;
|
|
58
|
+
//# sourceMappingURL=useIncrementalStitcher.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useIncrementalStitcher — React hook driving the live panorama
|
|
5
|
+
* engine.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle:
|
|
8
|
+
* 1. Host calls `useARSession().start()` to put the AR session in
|
|
9
|
+
* tracking mode. (Works for AR-supported devices only — non-AR
|
|
10
|
+
* fallback comes in a later phase.)
|
|
11
|
+
* 2. Host calls `start()` from this hook. The native engine
|
|
12
|
+
* registers itself as the AR session's frame consumer.
|
|
13
|
+
* 3. Native emits a state event for every ARFrame the engine
|
|
14
|
+
* processes (~60 Hz, mostly trivially-skipped). The hook
|
|
15
|
+
* mirrors this into React state so a `<IncrementalStitcherView>`
|
|
16
|
+
* or any other consumer can render the live panorama + UX hints.
|
|
17
|
+
* 4. Host calls `finalize(outputPath)` when the user releases the
|
|
18
|
+
* shutter; resolves with the final panorama path + stats.
|
|
19
|
+
* 5. Host calls `cancel()` if the user dismisses the capture.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.useIncrementalStitcher = useIncrementalStitcher;
|
|
23
|
+
const react_1 = require("react");
|
|
24
|
+
const incremental_1 = require("./incremental");
|
|
25
|
+
/**
|
|
26
|
+
* Map raw outcome → user-facing hint string. null = no banner.
|
|
27
|
+
*/
|
|
28
|
+
function outcomeToHint(outcome) {
|
|
29
|
+
switch (outcome) {
|
|
30
|
+
case incremental_1.IncrementalOutcome.RejectedTooFar:
|
|
31
|
+
return 'slow-down';
|
|
32
|
+
case incremental_1.IncrementalOutcome.RejectedSceneUniform:
|
|
33
|
+
return 'scene-uniform';
|
|
34
|
+
case incremental_1.IncrementalOutcome.RejectedAlignmentLost:
|
|
35
|
+
return 'alignment-lost';
|
|
36
|
+
case incremental_1.IncrementalOutcome.SkippedTrackingPoor:
|
|
37
|
+
return 'tracking-poor';
|
|
38
|
+
case incremental_1.IncrementalOutcome.AcceptedHigh:
|
|
39
|
+
case incremental_1.IncrementalOutcome.AcceptedMedium:
|
|
40
|
+
case incremental_1.IncrementalOutcome.SkippedTooClose:
|
|
41
|
+
default:
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function outcomeToConfidence(outcome) {
|
|
46
|
+
if (outcome === incremental_1.IncrementalOutcome.AcceptedHigh)
|
|
47
|
+
return 'high';
|
|
48
|
+
if (outcome === incremental_1.IncrementalOutcome.AcceptedMedium)
|
|
49
|
+
return 'medium';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function useIncrementalStitcher() {
|
|
53
|
+
const native = (0, incremental_1.getIncrementalNativeModule)();
|
|
54
|
+
const isAvailable = (0, incremental_1.incrementalStitcherIsAvailable)();
|
|
55
|
+
const [isRunning, setIsRunning] = (0, react_1.useState)(false);
|
|
56
|
+
const [state, setState] = (0, react_1.useState)(null);
|
|
57
|
+
// Keep the latest hint/confidence sticky for a few frames after a
|
|
58
|
+
// skip — otherwise the UI flickers since SkippedTooClose returns
|
|
59
|
+
// a "silent" outcome between every accept. We collapse this by
|
|
60
|
+
// only updating hint when the new outcome is itself a hint or an
|
|
61
|
+
// accept, leaving non-hint skips alone.
|
|
62
|
+
const lastHintRef = (0, react_1.useRef)(null);
|
|
63
|
+
// Subscribe to native events on mount. The subscription itself
|
|
64
|
+
// is cheap; the native side gates `hasListeners` so events are
|
|
65
|
+
// only emitted when JS is listening.
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
if (!native)
|
|
68
|
+
return undefined;
|
|
69
|
+
const sub = (0, incremental_1.subscribeIncrementalState)((nextState) => {
|
|
70
|
+
// Sticky-snapshot merge: the native side emits a state event
|
|
71
|
+
// for EVERY ARFrame the engine processes (~60 Hz), most of
|
|
72
|
+
// which are SkippedTooClose with NO snapshot path. A naive
|
|
73
|
+
// `setState(nextState)` would wipe the panoramaPath to null
|
|
74
|
+
// 60 times per second, blanking the live preview between
|
|
75
|
+
// accepts. Keep the last-good snapshot fields so the PiP
|
|
76
|
+
// shows the most recent panorama continuously between accepts;
|
|
77
|
+
// other fields (outcome, confidence, hint) update normally.
|
|
78
|
+
setState((prev) => {
|
|
79
|
+
if (!nextState.panoramaPath && prev?.panoramaPath) {
|
|
80
|
+
return {
|
|
81
|
+
...nextState,
|
|
82
|
+
panoramaPath: prev.panoramaPath,
|
|
83
|
+
width: prev.width,
|
|
84
|
+
height: prev.height,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return nextState;
|
|
88
|
+
});
|
|
89
|
+
const newHint = outcomeToHint(nextState.outcome);
|
|
90
|
+
if (newHint !== null) {
|
|
91
|
+
lastHintRef.current = newHint;
|
|
92
|
+
}
|
|
93
|
+
else if (nextState.outcome === incremental_1.IncrementalOutcome.AcceptedHigh ||
|
|
94
|
+
nextState.outcome === incremental_1.IncrementalOutcome.AcceptedMedium) {
|
|
95
|
+
// An accept clears any prior hint — operator's back on track.
|
|
96
|
+
lastHintRef.current = null;
|
|
97
|
+
}
|
|
98
|
+
// Else: SkippedTooClose etc. — leave the hint alone.
|
|
99
|
+
});
|
|
100
|
+
return () => sub?.remove();
|
|
101
|
+
}, [native]);
|
|
102
|
+
const start = (0, react_1.useCallback)(async (options = {}) => {
|
|
103
|
+
if (!native) {
|
|
104
|
+
throw new Error('useIncrementalStitcher: IncrementalStitcher native '
|
|
105
|
+
+ 'module is not registered. Ensure the SDK pod has been '
|
|
106
|
+
+ 'rebuilt against the host app.');
|
|
107
|
+
}
|
|
108
|
+
await native.start(options);
|
|
109
|
+
setIsRunning(true);
|
|
110
|
+
setState(null);
|
|
111
|
+
lastHintRef.current = null;
|
|
112
|
+
}, [native]);
|
|
113
|
+
const finalize = (0, react_1.useCallback)(async (outputPath, quality = 90, captureOrientation) => {
|
|
114
|
+
if (!native) {
|
|
115
|
+
throw new Error('useIncrementalStitcher: native module unavailable');
|
|
116
|
+
}
|
|
117
|
+
const result = await native.finalize({
|
|
118
|
+
outputPath: outputPath ?? '',
|
|
119
|
+
quality,
|
|
120
|
+
// 2026-05-18 (iOS cross-orientation fix) — fresh orientation
|
|
121
|
+
// at finalize time. Engine uses this for bake-rotation
|
|
122
|
+
// instead of the start-time snapshot. Undefined = keep
|
|
123
|
+
// legacy start-time behaviour.
|
|
124
|
+
captureOrientation,
|
|
125
|
+
});
|
|
126
|
+
setIsRunning(false);
|
|
127
|
+
// Clear React state on finalize so the next start doesn't
|
|
128
|
+
// briefly show stale frame counts / hint banners from the
|
|
129
|
+
// previous capture. Without this, the IncrementalStitcherView
|
|
130
|
+
// displayed acceptedCount from the prior pan if a late event
|
|
131
|
+
// had already updated state.
|
|
132
|
+
setState(null);
|
|
133
|
+
lastHintRef.current = null;
|
|
134
|
+
return result;
|
|
135
|
+
}, [native]);
|
|
136
|
+
const cancel = (0, react_1.useCallback)(async () => {
|
|
137
|
+
if (!native)
|
|
138
|
+
return;
|
|
139
|
+
await native.cancel();
|
|
140
|
+
setIsRunning(false);
|
|
141
|
+
setState(null);
|
|
142
|
+
lastHintRef.current = null;
|
|
143
|
+
}, [native]);
|
|
144
|
+
// Cleanup-on-unmount that actually works. The previous version
|
|
145
|
+
// captured `isRunning` from the initial render (false), so the
|
|
146
|
+
// cancel never fired. Reading from a ref keeps the latest
|
|
147
|
+
// value visible at unmount time.
|
|
148
|
+
const isRunningRef = (0, react_1.useRef)(false);
|
|
149
|
+
(0, react_1.useEffect)(() => {
|
|
150
|
+
isRunningRef.current = isRunning;
|
|
151
|
+
}, [isRunning]);
|
|
152
|
+
(0, react_1.useEffect)(() => {
|
|
153
|
+
return () => {
|
|
154
|
+
if (native && isRunningRef.current) {
|
|
155
|
+
native.cancel().catch(() => undefined);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
159
|
+
}, []);
|
|
160
|
+
const confidenceLevel = state ? outcomeToConfidence(state.outcome) : null;
|
|
161
|
+
return {
|
|
162
|
+
isAvailable,
|
|
163
|
+
isRunning,
|
|
164
|
+
state,
|
|
165
|
+
hint: lastHintRef.current,
|
|
166
|
+
confidenceLevel,
|
|
167
|
+
start,
|
|
168
|
+
finalize,
|
|
169
|
+
cancel,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=useIncrementalStitcher.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal type definitions for `react-native-image-stitcher`.
|
|
3
|
+
*
|
|
4
|
+
* These are NOT re-exported from `src/index.ts` — they are consumed
|
|
5
|
+
* by internal modules (`useCapture`, `runQualityCheck`) and adapted
|
|
6
|
+
* to the public surface (e.g., `CameraCaptureResult` in
|
|
7
|
+
* `src/camera/Camera.tsx`) before reaching consumers.
|
|
8
|
+
*
|
|
9
|
+
* If something here needs to become public, expose it deliberately
|
|
10
|
+
* from `src/index.ts` rather than encouraging deep imports.
|
|
11
|
+
*/
|
|
12
|
+
export interface QualityThresholds {
|
|
13
|
+
/** Minimum Laplacian variance for blur detection */
|
|
14
|
+
minBlurScore: number;
|
|
15
|
+
/** Minimum brightness (0-255) */
|
|
16
|
+
minBrightness: number;
|
|
17
|
+
/** Maximum brightness (0-255) */
|
|
18
|
+
maxBrightness: number;
|
|
19
|
+
}
|
|
20
|
+
export interface QualityReport {
|
|
21
|
+
passed: boolean;
|
|
22
|
+
blurScore: number;
|
|
23
|
+
brightnessScore: number;
|
|
24
|
+
issues: QualityIssue[];
|
|
25
|
+
}
|
|
26
|
+
export interface QualityIssue {
|
|
27
|
+
type: 'blur' | 'brightness_low' | 'brightness_high' | 'framing';
|
|
28
|
+
message: string;
|
|
29
|
+
severity: 'warning' | 'error';
|
|
30
|
+
}
|
|
31
|
+
export interface DeviceMetadata {
|
|
32
|
+
platform: 'ios' | 'android';
|
|
33
|
+
osVersion: string;
|
|
34
|
+
deviceModel: string;
|
|
35
|
+
cameraId: string;
|
|
36
|
+
flashEnabled: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface CaptureResult {
|
|
39
|
+
/** Unique device-generated UUID */
|
|
40
|
+
deviceUuid: string;
|
|
41
|
+
/** Local file path to compressed image */
|
|
42
|
+
compressedUri: string;
|
|
43
|
+
/** Local file path to original image (if retained) */
|
|
44
|
+
originalUri?: string;
|
|
45
|
+
/** Image width in pixels, after EXIF orientation correction. */
|
|
46
|
+
width: number;
|
|
47
|
+
/** Image height in pixels, after EXIF orientation correction. */
|
|
48
|
+
height: number;
|
|
49
|
+
/** Whether this is a stitched panoramic image */
|
|
50
|
+
isStitched: boolean;
|
|
51
|
+
/** Capture timestamp (ISO 8601) */
|
|
52
|
+
capturedAt: string;
|
|
53
|
+
/** Quality check results (if enabled) */
|
|
54
|
+
qualityReport?: QualityReport;
|
|
55
|
+
/** Device metadata at capture time */
|
|
56
|
+
deviceMetadata: DeviceMetadata;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Internal type definitions for `react-native-image-stitcher`.
|
|
5
|
+
*
|
|
6
|
+
* These are NOT re-exported from `src/index.ts` — they are consumed
|
|
7
|
+
* by internal modules (`useCapture`, `runQualityCheck`) and adapted
|
|
8
|
+
* to the public surface (e.g., `CameraCaptureResult` in
|
|
9
|
+
* `src/camera/Camera.tsx`) before reaching consumers.
|
|
10
|
+
*
|
|
11
|
+
* If something here needs to become public, expose it deliberately
|
|
12
|
+
* from `src/index.ts` rather than encouraging deep imports.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// swift-tools-version:5.9
|
|
2
|
+
//
|
|
3
|
+
// Package.swift — SwiftPM manifest used **only for command-line testing**
|
|
4
|
+
// of the algorithm layer (QualityChecker.swift). Production builds
|
|
5
|
+
// don't go through SwiftPM; the host iOS app pulls these same source
|
|
6
|
+
// files via the `RNImageStitcher.podspec` at the SDK package
|
|
7
|
+
// root.
|
|
8
|
+
//
|
|
9
|
+
// Why bother with SwiftPM at all when production uses CocoaPods?
|
|
10
|
+
// * `swift test` runs from the command line, in CI, in 2 seconds.
|
|
11
|
+
// * XCTest in CocoaPods needs an Xcode workspace and an iOS
|
|
12
|
+
// simulator boot — slow, flaky, and ergonomically bad.
|
|
13
|
+
// * The bridge layer (QualityCheckerBridge.swift) deliberately
|
|
14
|
+
// guards its React import behind `#if canImport(React)` so SwiftPM
|
|
15
|
+
// can compile the package WITHOUT React being available.
|
|
16
|
+
//
|
|
17
|
+
// Run from this directory:
|
|
18
|
+
//
|
|
19
|
+
// cd retailens-capture-sdk/ios
|
|
20
|
+
// swift test
|
|
21
|
+
|
|
22
|
+
import PackageDescription
|
|
23
|
+
|
|
24
|
+
let package = Package(
|
|
25
|
+
name: "RNImageStitcher",
|
|
26
|
+
platforms: [
|
|
27
|
+
.iOS(.v14),
|
|
28
|
+
// macOS target needed so `swift test` can run on a Mac without
|
|
29
|
+
// an iOS simulator. The algorithm layer uses Accelerate + Core
|
|
30
|
+
// Image — both of which are available on macOS too.
|
|
31
|
+
.macOS(.v12),
|
|
32
|
+
],
|
|
33
|
+
products: [
|
|
34
|
+
.library(name: "RNImageStitcher", targets: ["RNImageStitcher"]),
|
|
35
|
+
],
|
|
36
|
+
dependencies: [],
|
|
37
|
+
targets: [
|
|
38
|
+
.target(
|
|
39
|
+
name: "RNImageStitcher",
|
|
40
|
+
path: "Sources/RNImageStitcher",
|
|
41
|
+
// Excluded from `swift test` because they depend on either
|
|
42
|
+
// React (which isn't a SwiftPM dep) or OpenCV (which only
|
|
43
|
+
// ships as an iOS XCFramework via the podspec — no macOS
|
|
44
|
+
// build). The host app's CocoaPods workspace picks them up.
|
|
45
|
+
exclude: [
|
|
46
|
+
// React-dependent
|
|
47
|
+
"QualityCheckerBridge.swift",
|
|
48
|
+
"QualityCheckerBridge.m",
|
|
49
|
+
"StitcherBridge.swift",
|
|
50
|
+
"StitcherBridge.m",
|
|
51
|
+
// OpenCV-dependent (Phase 2 stitcher)
|
|
52
|
+
"OpenCVStitcher.h",
|
|
53
|
+
"OpenCVStitcher.mm",
|
|
54
|
+
// OpenCV-dependent (V16 Phase 1 keyframe collector)
|
|
55
|
+
"OpenCVKeyframeCollector.h",
|
|
56
|
+
"OpenCVKeyframeCollector.mm",
|
|
57
|
+
// Stitcher.swift is `#if canImport(UIKit)`-gated so it
|
|
58
|
+
// compiles to nothing on macOS; including it keeps the
|
|
59
|
+
// file available to the Pods build without breaking
|
|
60
|
+
// `swift test`.
|
|
61
|
+
]
|
|
62
|
+
),
|
|
63
|
+
.testTarget(
|
|
64
|
+
name: "RNImageStitcherTests",
|
|
65
|
+
dependencies: ["RNImageStitcher"],
|
|
66
|
+
path: "Tests/RNImageStitcherTests",
|
|
67
|
+
resources: [
|
|
68
|
+
.copy("Fixtures"),
|
|
69
|
+
]
|
|
70
|
+
),
|
|
71
|
+
]
|
|
72
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// ARCameraViewManager.m
|
|
4
|
+
//
|
|
5
|
+
// RN bridge declaration for the Swift `RNSARCameraViewManager`.
|
|
6
|
+
// Without this file the JS side's `requireNativeComponent('RNSARCameraView')`
|
|
7
|
+
// would resolve to undefined because RN's component registry is
|
|
8
|
+
// populated by RCT_EXTERN_MODULE / RCT_EXPORT_VIEW_PROPERTY macros,
|
|
9
|
+
// not Swift @objc decorators alone.
|
|
10
|
+
//
|
|
11
|
+
// View name semantics:
|
|
12
|
+
// `RCT_EXTERN_MODULE(<ManagerName>, RCTViewManager)` registers the
|
|
13
|
+
// Swift manager class. RN auto-derives the JS-visible component
|
|
14
|
+
// name by stripping the trailing "Manager" — so this manager
|
|
15
|
+
// exposes a component named "RNSARCameraView" on the JS side.
|
|
16
|
+
// That name MUST match what `requireNativeComponent('...')` looks
|
|
17
|
+
// up in `ARCameraView.tsx`.
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
#import <React/RCTViewManager.h>
|
|
21
|
+
|
|
22
|
+
@interface RCT_EXTERN_MODULE(RNSARCameraViewManager, RCTViewManager)
|
|
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.
|
|
32
|
+
|
|
33
|
+
@end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// ARCameraViewManager — RCTViewManager that vends RNSARCameraView
|
|
4
|
+
// instances to React Native.
|
|
5
|
+
//
|
|
6
|
+
// React Native discovers native UI components via subclasses of
|
|
7
|
+
// RCTViewManager (or its newer ComponentDescriptor/ShadowNode siblings
|
|
8
|
+
// for Fabric). We're on the bridge architecture (RN 0.84 Paper),
|
|
9
|
+
// so a thin RCTViewManager subclass is enough: override `view()` to
|
|
10
|
+
// return a fresh view instance, declare props via RCT_EXPORT_VIEW_PROPERTY
|
|
11
|
+
// in the .m bridge file, and React Native handles the rest of the
|
|
12
|
+
// view-tree integration.
|
|
13
|
+
//
|
|
14
|
+
// The class itself does almost nothing — view lifecycle (start/stop AR
|
|
15
|
+
// session) lives on the view, props are bridged via the .m file. Most
|
|
16
|
+
// of the value here is the @objc(RNSARCameraViewManager) name
|
|
17
|
+
// matching the JS-side `requireNativeComponent` call.
|
|
18
|
+
|
|
19
|
+
#if canImport(React)
|
|
20
|
+
import Foundation
|
|
21
|
+
import React
|
|
22
|
+
import UIKit
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@objc(RNSARCameraViewManager)
|
|
26
|
+
public final class RNSARCameraViewManager: RCTViewManager {
|
|
27
|
+
|
|
28
|
+
/// Vends a new view instance per React Native mount. RN reuses
|
|
29
|
+
/// view instances when possible (recycler) but during initial
|
|
30
|
+
/// hookup this is called once per `<ARCameraView>` element.
|
|
31
|
+
public override func view() -> UIView! {
|
|
32
|
+
return RNSARCameraView()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// AR-camera view setup needs the main thread (UIKit + ARSCNView).
|
|
36
|
+
public override class func requiresMainQueueSetup() -> Bool {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
#endif
|