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.
Files changed (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. package/src/types.ts +78 -0
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ /**
4
+ * postinstall-fetch-binaries.js — fetch OpenCV binaries matching this
5
+ * package's version from GitHub Releases.
6
+ *
7
+ * Runs after `npm install` / `yarn install` in the consumer app.
8
+ * Reads `package.json#version`, fetches the matching iOS xcframework
9
+ * + Android per-ABI archives, extracts them into the expected
10
+ * locations so `pod install` and `./gradlew` find them on the next
11
+ * native build.
12
+ *
13
+ * The mechanism is fault-tolerant:
14
+ * - Already-on-disk binaries → skip fetch (matched by .opencv-fetched
15
+ * marker file recording the version).
16
+ * - Transient network failures → retry up to 3× with exponential
17
+ * backoff.
18
+ * - Unreachable GH Releases (e.g., offline install of a fresh
19
+ * package version) → exit cleanly with a warning + instructions
20
+ * to re-run `npm install` later. This lets `npm install` itself
21
+ * succeed (the native build will fail later with a clear error,
22
+ * vs blocking the JS-only `npm install` step here).
23
+ *
24
+ * Env overrides:
25
+ * OPENCV_BINARY_BASE_URL — override the default GH Releases URL
26
+ * (useful for internal mirrors).
27
+ * SKIP_OPENCV_FETCH=1 — bail out (used by CI builds where the
28
+ * binaries are pre-staged manually).
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const https = require('https');
34
+ const { execSync } = require('child_process');
35
+ const zlib = require('zlib');
36
+
37
+ const PKG = require(path.join(__dirname, '..', 'package.json'));
38
+ const VERSION = PKG.version;
39
+ const TAG = `v${VERSION}`;
40
+
41
+ // When this script is moved to the public lib repo, BASE_URL becomes
42
+ // `https://github.com/bhargavkanda/react-native-image-stitcher/releases/download`.
43
+ // Until then (development in the monorepo), allow override via env.
44
+ const DEFAULT_BASE_URL =
45
+ 'https://github.com/bhargavkanda/react-native-image-stitcher/releases/download';
46
+ const BASE_URL = process.env.OPENCV_BINARY_BASE_URL || DEFAULT_BASE_URL;
47
+
48
+ const PKG_ROOT = path.join(__dirname, '..');
49
+ const IOS_DEST = path.join(PKG_ROOT, 'ios', 'Frameworks');
50
+ const ANDROID_DEST = path.join(PKG_ROOT, 'android', 'vendor');
51
+ const MARKER = path.join(PKG_ROOT, '.opencv-fetched');
52
+
53
+ const IOS_ASSET = `RNImageStitcher-${TAG}-ios.zip`;
54
+ const ANDROID_ASSET = `RNImageStitcher-${TAG}-android.zip`;
55
+
56
+ function log(...args) {
57
+ console.log('[react-native-image-stitcher postinstall]', ...args);
58
+ }
59
+
60
+ function warn(...args) {
61
+ console.warn('[react-native-image-stitcher postinstall]', ...args);
62
+ }
63
+
64
+ function alreadyFetched() {
65
+ try {
66
+ const v = fs.readFileSync(MARKER, 'utf8').trim();
67
+ return v === VERSION;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function markFetched() {
74
+ fs.writeFileSync(MARKER, `${VERSION}\n`, 'utf8');
75
+ }
76
+
77
+ function downloadWithRedirects(url, destPath, maxRedirects = 6) {
78
+ return new Promise((resolve, reject) => {
79
+ function attempt(currentUrl, redirectsLeft) {
80
+ const req = https.get(currentUrl, (res) => {
81
+ // Follow 301 / 302 / 307.
82
+ if (
83
+ res.statusCode &&
84
+ res.statusCode >= 300 &&
85
+ res.statusCode < 400 &&
86
+ res.headers.location &&
87
+ redirectsLeft > 0
88
+ ) {
89
+ res.resume();
90
+ return attempt(res.headers.location, redirectsLeft - 1);
91
+ }
92
+ if (res.statusCode !== 200) {
93
+ return reject(
94
+ new Error(`HTTP ${res.statusCode} for ${currentUrl}`),
95
+ );
96
+ }
97
+ const file = fs.createWriteStream(destPath);
98
+ res.pipe(file);
99
+ file.on('finish', () => file.close(() => resolve(destPath)));
100
+ file.on('error', reject);
101
+ });
102
+ req.on('error', reject);
103
+ req.setTimeout(60000, () => {
104
+ req.destroy(new Error('Download timed out'));
105
+ });
106
+ }
107
+ attempt(url, maxRedirects);
108
+ });
109
+ }
110
+
111
+ async function downloadWithRetries(url, destPath) {
112
+ const maxAttempts = 3;
113
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
114
+ try {
115
+ return await downloadWithRedirects(url, destPath);
116
+ } catch (err) {
117
+ if (attempt === maxAttempts) throw err;
118
+ const wait = 1000 * Math.pow(2, attempt - 1);
119
+ warn(`Download failed (attempt ${attempt}/${maxAttempts}): ${err.message}`);
120
+ warn(`Retrying in ${wait} ms…`);
121
+ await new Promise((r) => setTimeout(r, wait));
122
+ }
123
+ }
124
+ }
125
+
126
+ function ensureDir(p) {
127
+ fs.mkdirSync(p, { recursive: true });
128
+ }
129
+
130
+ function unzip(zipPath, dest) {
131
+ // Prefer the system `unzip` binary when available (POSIX hosts).
132
+ // Falls back to a pure-JS implementation so Windows hosts (which
133
+ // don't ship `unzip` by default) can install the package too.
134
+ const isWindows = process.platform === 'win32';
135
+ if (!isWindows) {
136
+ try {
137
+ execSync(`unzip -oq ${JSON.stringify(zipPath)} -d ${JSON.stringify(dest)}`);
138
+ return;
139
+ } catch (err) {
140
+ warn(`system unzip failed (${err.message}); falling back to pure-JS extractor.`);
141
+ }
142
+ }
143
+ // Pure-JS fallback using Node's built-in zlib + a minimal local
144
+ // implementation of the ZIP central-directory format. Handles the
145
+ // subset of ZIP features that our release tarballs use (stored or
146
+ // deflate, no encryption, no archive splits).
147
+ extractZipPureJs(zipPath, dest);
148
+ }
149
+
150
+ function extractZipPureJs(zipPath, dest) {
151
+ // Minimal ZIP reader: walks the central directory and inflates
152
+ // each entry into `dest`. Designed to handle only the archive
153
+ // shape our CI produces (created by `zip` on macos-14 / ubuntu-22.04
154
+ // runners; no encryption, no zip64, no archive splits). If you need
155
+ // to support exotic ZIPs, swap in `adm-zip` or `yauzl`.
156
+ const buffer = fs.readFileSync(zipPath);
157
+ // EOCD is at the end of the file; scan back up to 64KB for its
158
+ // signature 0x06054b50.
159
+ const EOCD_SIG = 0x06054b50;
160
+ let eocdOffset = -1;
161
+ const searchStart = Math.max(0, buffer.length - 65557);
162
+ for (let i = buffer.length - 22; i >= searchStart; i--) {
163
+ if (buffer.readUInt32LE(i) === EOCD_SIG) { eocdOffset = i; break; }
164
+ }
165
+ if (eocdOffset < 0) throw new Error(`No EOCD found in ${zipPath}`);
166
+ const cdEntries = buffer.readUInt16LE(eocdOffset + 10);
167
+ const cdOffset = buffer.readUInt32LE(eocdOffset + 16);
168
+ let p = cdOffset;
169
+ for (let i = 0; i < cdEntries; i++) {
170
+ if (buffer.readUInt32LE(p) !== 0x02014b50) {
171
+ throw new Error(`Bad central directory header at ${p}`);
172
+ }
173
+ const compression = buffer.readUInt16LE(p + 10);
174
+ const compressedSize = buffer.readUInt32LE(p + 20);
175
+ const uncompressedSize = buffer.readUInt32LE(p + 24);
176
+ const nameLen = buffer.readUInt16LE(p + 28);
177
+ const extraLen = buffer.readUInt16LE(p + 30);
178
+ const commentLen = buffer.readUInt16LE(p + 32);
179
+ const localHeaderOffset = buffer.readUInt32LE(p + 42);
180
+ const name = buffer.slice(p + 46, p + 46 + nameLen).toString('utf8');
181
+ p += 46 + nameLen + extraLen + commentLen;
182
+
183
+ // Parse local file header to get its own variable-length fields.
184
+ if (buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50) {
185
+ throw new Error(`Bad local file header at ${localHeaderOffset}`);
186
+ }
187
+ const localNameLen = buffer.readUInt16LE(localHeaderOffset + 26);
188
+ const localExtraLen = buffer.readUInt16LE(localHeaderOffset + 28);
189
+ const dataStart = localHeaderOffset + 30 + localNameLen + localExtraLen;
190
+ const compressed = buffer.slice(dataStart, dataStart + compressedSize);
191
+
192
+ const outPath = path.join(dest, name);
193
+ if (name.endsWith('/')) {
194
+ fs.mkdirSync(outPath, { recursive: true });
195
+ continue;
196
+ }
197
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
198
+ let data;
199
+ if (compression === 0) {
200
+ data = compressed;
201
+ } else if (compression === 8) {
202
+ data = zlib.inflateRawSync(compressed);
203
+ } else {
204
+ throw new Error(`Unsupported compression method ${compression} in ${name}`);
205
+ }
206
+ if (data.length !== uncompressedSize) {
207
+ throw new Error(`Size mismatch for ${name}: expected ${uncompressedSize}, got ${data.length}`);
208
+ }
209
+ fs.writeFileSync(outPath, data);
210
+ }
211
+ }
212
+
213
+ async function main() {
214
+ if (process.env.SKIP_OPENCV_FETCH === '1') {
215
+ log('SKIP_OPENCV_FETCH=1 set — skipping.');
216
+ return;
217
+ }
218
+
219
+ if (alreadyFetched()) {
220
+ log(`OpenCV ${VERSION} already on disk; skipping fetch.`);
221
+ return;
222
+ }
223
+
224
+ log(`Fetching OpenCV binaries for ${TAG}…`);
225
+ log(`Base URL: ${BASE_URL}`);
226
+
227
+ ensureDir(IOS_DEST);
228
+ ensureDir(ANDROID_DEST);
229
+ ensureDir(path.join(PKG_ROOT, '.tmp'));
230
+
231
+ try {
232
+ const iosZip = path.join(PKG_ROOT, '.tmp', IOS_ASSET);
233
+ const androidZip = path.join(PKG_ROOT, '.tmp', ANDROID_ASSET);
234
+
235
+ await Promise.all([
236
+ downloadWithRetries(`${BASE_URL}/${TAG}/${IOS_ASSET}`, iosZip),
237
+ downloadWithRetries(`${BASE_URL}/${TAG}/${ANDROID_ASSET}`, androidZip),
238
+ ]);
239
+
240
+ log('Extracting iOS xcframework…');
241
+ unzip(iosZip, IOS_DEST);
242
+ log('Extracting Android per-ABI binaries…');
243
+ unzip(androidZip, ANDROID_DEST);
244
+
245
+ // Post-extract layout validation — fail loudly if the release
246
+ // archives didn't unpack into the shape the podspec /
247
+ // build.gradle expect. Without this, a malformed release would
248
+ // silently mark-fetched and then the next native build would
249
+ // fail confusingly far from the actual cause.
250
+ const iosFrameworkInfo = path.join(IOS_DEST, 'opencv2.xcframework', 'Info.plist');
251
+ const androidJavaSo = path.join(
252
+ ANDROID_DEST, 'OpenCV-android-sdk', 'sdk', 'native', 'libs',
253
+ 'arm64-v8a', 'libopencv_java4.so',
254
+ );
255
+ if (!fs.existsSync(iosFrameworkInfo)) {
256
+ throw new Error(
257
+ `iOS xcframework missing after extract: expected ${iosFrameworkInfo}. ` +
258
+ `The release asset ${IOS_ASSET} may be malformed; please open an issue.`,
259
+ );
260
+ }
261
+ if (!fs.existsSync(androidJavaSo)) {
262
+ throw new Error(
263
+ `Android OpenCV SDK missing after extract: expected ${androidJavaSo}. ` +
264
+ `The release asset ${ANDROID_ASSET} may be malformed; please open an issue.`,
265
+ );
266
+ }
267
+
268
+ markFetched();
269
+ log('OpenCV binaries ready.');
270
+ } catch (err) {
271
+ warn(
272
+ 'Could not fetch OpenCV binaries. This is non-fatal for now — '
273
+ + '`npm install` succeeds. Your next native build will fail '
274
+ + 'with a clear error. Recovery:',
275
+ );
276
+ warn(` 1. Verify ${BASE_URL}/${TAG}/${IOS_ASSET} is reachable.`);
277
+ warn(' 2. Re-run `npm install` once network is available.');
278
+ warn(`Underlying error: ${err.message}`);
279
+ }
280
+ }
281
+
282
+ main().catch((err) => {
283
+ warn(`Unexpected error: ${err.message}`);
284
+ // Exit code 0 — don't block `npm install` on a download failure.
285
+ process.exit(0);
286
+ });
@@ -0,0 +1,210 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * useARSession — React hook for the SDK's ARKit (iOS) / ARCore
4
+ * (Android) session foundation.
5
+ *
6
+ * Phase 4 of the AR measurement plan
7
+ * (docs/site-content/design/2026-04-29-ar-measurement-and-detection.md).
8
+ *
9
+ * What this gives the host:
10
+ * - `isAvailable`: whether the device can run AR at all
11
+ * - `trackingState`: current AR tracking quality (mirrors Apple's
12
+ * enum values exactly — same numeric ids on both platforms)
13
+ * - `start()` / `stop()`: lifecycle controls
14
+ * - `getFramePoses()`: snapshot the per-frame pose log captured
15
+ * during the most recent session, used by Phase 5 stitching
16
+ * and Phase 6 measurement
17
+ *
18
+ * What this does NOT give:
19
+ * The hook is camera-agnostic. It just runs the AR tracking
20
+ * session. Frame display + capture happen via the SDK's
21
+ * AR-backed `<CameraView>` (Phase 4.4 — coming) or the existing
22
+ * vision-camera-backed view if AR is unavailable.
23
+ */
24
+
25
+ import { useCallback, useEffect, useRef, useState } from 'react';
26
+ import { NativeModules } from 'react-native';
27
+
28
+
29
+ /**
30
+ * AR tracking state. Numeric values mirror iOS' enum and the
31
+ * Android constants in `RNSARSession.companion`. Cross-
32
+ * platform identical; no branching needed in JS.
33
+ */
34
+ export enum ARTrackingState {
35
+ /** AR not running, not supported, or session was stopped. */
36
+ NotAvailable = 0,
37
+ /** Session running but tracking quality not yet usable. */
38
+ Initialising = 1,
39
+ /** Session running with usable tracking — poses are good. */
40
+ Tracking = 2,
41
+ /** Tracking quality dropped mid-session. Poses degraded. */
42
+ Limited = 3,
43
+ }
44
+
45
+
46
+ /**
47
+ * One captured frame's pose. Coordinates are in the AR session's
48
+ * world frame (right-handed, Y-up on iOS / Y-up on Android), with
49
+ * translation in metres. Rotation is a unit quaternion; w is the
50
+ * real component.
51
+ */
52
+ export interface FramePose {
53
+ tx: number; ty: number; tz: number;
54
+ qx: number; qy: number; qz: number; qw: number;
55
+ /** Camera intrinsics (focal length + principal point) in pixels. */
56
+ fx: number; fy: number; cx: number; cy: number;
57
+ imageWidth: number;
58
+ imageHeight: number;
59
+ /** Frame timestamp in ms relative to AR session start. */
60
+ timestampMs: number;
61
+ trackingState: ARTrackingState;
62
+ }
63
+
64
+
65
+ export interface UseARSessionReturn {
66
+ /**
67
+ * Whether the device can run AR. Set after the first `start()`
68
+ * call (or by the explicit `checkAvailability()`). False on
69
+ * older iPhones, simulators, and unsupported Android devices.
70
+ */
71
+ isAvailable: boolean;
72
+ /**
73
+ * Whether the session is currently running. True between
74
+ * `start()` and `stop()`.
75
+ */
76
+ isRunning: boolean;
77
+ /** Current tracking quality. Polled every 500ms while running. */
78
+ trackingState: ARTrackingState;
79
+ /**
80
+ * Start the AR session. On Android, may trigger a Play Services
81
+ * for AR install dialog the first time it runs. Throws if the
82
+ * device doesn't support AR.
83
+ */
84
+ start: () => Promise<void>;
85
+ /**
86
+ * Stop the AR session and clear the pose log. Idempotent;
87
+ * calling on a stopped session is a no-op.
88
+ */
89
+ stop: () => Promise<void>;
90
+ /**
91
+ * Snapshot the per-frame pose log captured since the last
92
+ * `start()`. Used by the stitcher and measurement APIs after
93
+ * recording stops.
94
+ */
95
+ getFramePoses: () => Promise<FramePose[]>;
96
+ /**
97
+ * Drop everything in the pose log. Call before each new
98
+ * panorama capture so the log doesn't carry stale poses from
99
+ * an earlier session.
100
+ */
101
+ clearPoseLog: () => Promise<void>;
102
+ }
103
+
104
+
105
+ interface NativeARSessionModule {
106
+ isSupported(): Promise<boolean>;
107
+ start(): Promise<void>;
108
+ stop(): Promise<void>;
109
+ getState(): Promise<{ isRunning: boolean; trackingState: number }>;
110
+ snapshotPoseLog(): Promise<FramePose[]>;
111
+ clearPoseLog(): Promise<void>;
112
+ }
113
+
114
+
115
+ function getNativeModule(): NativeARSessionModule | null {
116
+ const m = (NativeModules as Record<string, unknown>)['RNSARSession'];
117
+ if (!m || typeof m !== 'object') return null;
118
+ return m as NativeARSessionModule;
119
+ }
120
+
121
+
122
+ /**
123
+ * Polling interval for tracking-state updates. 500ms is enough to
124
+ * feel responsive in the UI without flooding the bridge.
125
+ */
126
+ const STATE_POLL_INTERVAL_MS = 500;
127
+
128
+
129
+ export function useARSession(): UseARSessionReturn {
130
+ const [isAvailable, setIsAvailable] = useState(false);
131
+ const [isRunning, setIsRunning] = useState(false);
132
+ const [trackingState, setTrackingState] = useState<ARTrackingState>(
133
+ ARTrackingState.NotAvailable,
134
+ );
135
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
136
+
137
+ const native = getNativeModule();
138
+
139
+ // Probe availability once on mount. Running on a device without
140
+ // AR support shouldn't crash anything — `isAvailable` stays
141
+ // false and the rest of the SDK falls back to vision-camera.
142
+ useEffect(() => {
143
+ if (!native) return;
144
+ native.isSupported().then(setIsAvailable).catch((err) => {
145
+ // eslint-disable-next-line no-console
146
+ console.warn('[useARSession] isSupported failed', err);
147
+ });
148
+ }, [native]);
149
+
150
+ const stopPolling = useCallback(() => {
151
+ if (pollRef.current !== null) {
152
+ clearInterval(pollRef.current);
153
+ pollRef.current = null;
154
+ }
155
+ }, []);
156
+
157
+ const startPolling = useCallback(() => {
158
+ stopPolling();
159
+ if (!native) return;
160
+ pollRef.current = setInterval(async () => {
161
+ try {
162
+ const state = await native.getState();
163
+ setIsRunning(state.isRunning);
164
+ setTrackingState(state.trackingState as ARTrackingState);
165
+ } catch {
166
+ // Bridge errors during tear-down are expected; ignore.
167
+ }
168
+ }, STATE_POLL_INTERVAL_MS);
169
+ }, [native, stopPolling]);
170
+
171
+ // Always tear down the poll on unmount.
172
+ useEffect(() => stopPolling, [stopPolling]);
173
+
174
+ const start = useCallback(async () => {
175
+ if (!native) {
176
+ throw new Error('useARSession: RNSARSession native module unavailable');
177
+ }
178
+ await native.start();
179
+ setIsRunning(true);
180
+ startPolling();
181
+ }, [native, startPolling]);
182
+
183
+ const stop = useCallback(async () => {
184
+ if (!native) return;
185
+ stopPolling();
186
+ await native.stop();
187
+ setIsRunning(false);
188
+ setTrackingState(ARTrackingState.NotAvailable);
189
+ }, [native, stopPolling]);
190
+
191
+ const getFramePoses = useCallback(async (): Promise<FramePose[]> => {
192
+ if (!native) return [];
193
+ return native.snapshotPoseLog();
194
+ }, [native]);
195
+
196
+ const clearPoseLog = useCallback(async (): Promise<void> => {
197
+ if (!native) return;
198
+ await native.clearPoseLog();
199
+ }, [native]);
200
+
201
+ return {
202
+ isAvailable,
203
+ isRunning,
204
+ trackingState,
205
+ start,
206
+ stop,
207
+ getFramePoses,
208
+ clearPoseLog,
209
+ };
210
+ }
File without changes