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,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
|