react-native-image-stitcher 0.2.1 → 0.4.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 +511 -1
- package/README.md +1 -1
- package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
- package/cpp/stitcher.cpp +101 -1
- package/cpp/stitcher.hpp +8 -0
- package/dist/camera/Camera.d.ts +9 -0
- package/dist/camera/Camera.js +165 -43
- package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
- package/dist/camera/CaptureDebugOverlay.js +146 -0
- package/dist/camera/CaptureKeyframePill.d.ts +28 -0
- package/dist/camera/CaptureKeyframePill.js +60 -0
- package/dist/camera/CaptureMemoryPill.d.ts +28 -0
- package/dist/camera/CaptureMemoryPill.js +109 -0
- package/dist/camera/CaptureOrientationPill.d.ts +22 -0
- package/dist/camera/CaptureOrientationPill.js +44 -0
- package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
- package/dist/camera/CaptureStitchStatsToast.js +133 -0
- package/dist/camera/PanoramaSettings.d.ts +478 -0
- package/dist/camera/PanoramaSettings.js +120 -0
- package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
- package/dist/camera/PanoramaSettingsBridge.js +208 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
- package/dist/camera/PanoramaSettingsModal.js +189 -354
- package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
- package/dist/camera/buildPanoramaInitialSettings.js +97 -0
- package/dist/camera/lowMemDevice.d.ts +24 -0
- package/dist/camera/lowMemDevice.js +69 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +37 -2
- package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
- package/dist/sensors/useIMUTranslationGate.js +83 -1
- package/dist/stitching/incremental.d.ts +25 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
- package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
- package/package.json +6 -2
- package/src/camera/Camera.tsx +220 -54
- package/src/camera/CaptureDebugOverlay.tsx +180 -0
- package/src/camera/CaptureKeyframePill.tsx +77 -0
- package/src/camera/CaptureMemoryPill.tsx +96 -0
- package/src/camera/CaptureOrientationPill.tsx +57 -0
- package/src/camera/CaptureStitchStatsToast.tsx +155 -0
- package/src/camera/PanoramaSettings.ts +605 -0
- package/src/camera/PanoramaSettingsBridge.ts +238 -0
- package/src/camera/PanoramaSettingsModal.tsx +296 -988
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
- package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
- package/src/camera/buildPanoramaInitialSettings.ts +139 -0
- package/src/camera/lowMemDevice.ts +71 -0
- package/src/index.ts +61 -3
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
|
@@ -215,6 +215,12 @@ struct FinalizePayload {
|
|
|
215
215
|
let batchBlenderType: String
|
|
216
216
|
let batchSeamFinderType: String
|
|
217
217
|
let batchEnableInscribedRectCrop: Bool
|
|
218
|
+
/// 2026-05-22 (audit F2) — resolved stitcher mode for this finalize
|
|
219
|
+
/// pass. String form ("panorama" / "scans") matches Android's
|
|
220
|
+
/// JNI string and the Obj-C++ method signature. 'auto' is
|
|
221
|
+
/// resolved upstream by `resolveStitchModeAuto` before this snapshot
|
|
222
|
+
/// is captured; this field never carries 'auto'.
|
|
223
|
+
let batchStitchModeResolved: String
|
|
218
224
|
let keyframeExifOrientation: Int
|
|
219
225
|
/// AR-STITCHING-TWO-MODES (memory/ar-stitching-two-modes.md):
|
|
220
226
|
/// capture-time hold orientation for the bake-rotation pass.
|
|
@@ -409,6 +415,27 @@ public final class IncrementalStitcher: NSObject {
|
|
|
409
415
|
// the inscribed-rect + morph-close + col-projection pipeline
|
|
410
416
|
// runs.
|
|
411
417
|
private var batchEnableInscribedRectCrop: Bool = false
|
|
418
|
+
/// 2026-05-22 (audit F2) — cv::Stitcher pipeline mode for the
|
|
419
|
+
/// batch finalize. Mirrors Android's `batchStitchMode` (kt:187).
|
|
420
|
+
/// Valid values: 'auto' / 'panorama' / 'scans'. 'auto' is
|
|
421
|
+
/// resolved at finalize time via [resolveStitchModeAuto] using
|
|
422
|
+
/// the translation/rotation magnitudes between first + last
|
|
423
|
+
/// accepted keyframe poses. Default 'auto'.
|
|
424
|
+
private var batchStitchMode: String = "auto"
|
|
425
|
+
/// 2026-05-22 (audit F2) — first + last accepted keyframe poses
|
|
426
|
+
/// in the current batch capture. 7 doubles each: [tx, ty, tz,
|
|
427
|
+
/// qx, qy, qz, qw]. Both nil until at least one keyframe has
|
|
428
|
+
/// been accepted. Reset on every start(). Used only by the
|
|
429
|
+
/// auto-resolver; the keyframe-gate's own pose tracking lives
|
|
430
|
+
/// separately in cpp/keyframe_gate.cpp.
|
|
431
|
+
private var batchFirstAcceptedPose: [Double]? = nil
|
|
432
|
+
private var batchLastAcceptedPose: [Double]? = nil
|
|
433
|
+
/// 2026-05-22 (audit F2b) — JS-measured cumulative IMU translation
|
|
434
|
+
/// magnitude in METRES, set by the bridge at finalize time. Used
|
|
435
|
+
/// by the auto-resolver as a fallback translation signal in non-
|
|
436
|
+
/// AR mode (where pose-derived tx/ty/tz is always 0). Set to 0
|
|
437
|
+
/// at start() and overwritten at finalize() entry.
|
|
438
|
+
private var batchImuTranslationMetres: Double = 0.0
|
|
412
439
|
/// AR-STITCHING-TWO-MODES — see memory/ar-stitching-two-modes.md
|
|
413
440
|
///
|
|
414
441
|
/// Physical phone orientation at start() time, sourced from the
|
|
@@ -452,6 +479,18 @@ public final class IncrementalStitcher: NSObject {
|
|
|
452
479
|
prev, orientation)
|
|
453
480
|
}
|
|
454
481
|
|
|
482
|
+
/// 2026-05-22 (audit F2b) — JS calls this at finalize() entry to
|
|
483
|
+
/// hand over the cumulative IMU translation magnitude in METRES.
|
|
484
|
+
/// Stored in batchImuTranslationMetres for the auto-resolver to
|
|
485
|
+
/// consume in non-AR mode (where pose-derived translation is 0).
|
|
486
|
+
/// 0.0 is the back-compat default (resolver falls back to pose
|
|
487
|
+
/// translation; PANORAMA when both are 0).
|
|
488
|
+
@objc public func updateImuTranslationMetres(_ metres: Double) {
|
|
489
|
+
stateLock.lock()
|
|
490
|
+
self.batchImuTranslationMetres = max(0.0, metres)
|
|
491
|
+
stateLock.unlock()
|
|
492
|
+
}
|
|
493
|
+
|
|
455
494
|
/// 2026-05-18 (Iss 3) — return the current capture's keyframe
|
|
456
495
|
/// session directory, or nil if no capture is in flight / engine
|
|
457
496
|
/// isn't using a per-session keyframe collector.
|
|
@@ -823,6 +862,23 @@ public final class IncrementalStitcher: NSObject {
|
|
|
823
862
|
// to FALSE if not provided by JS.
|
|
824
863
|
self.batchEnableInscribedRectCrop =
|
|
825
864
|
(configOverrides["enableMaxInscribedRectCrop"] as? Bool) ?? false
|
|
865
|
+
// 2026-05-22 (audit F2) — read stitchMode from JS. Pre-
|
|
866
|
+
// audit, iOS hardcoded Panorama at OpenCVStitcher.mm:436
|
|
867
|
+
// regardless of the JS setting. Now mirrors Android's
|
|
868
|
+
// batchStitchMode + auto-resolver heuristic. Validate
|
|
869
|
+
// against the closed set; unknown values fall back to 'auto'.
|
|
870
|
+
let rawMode = (configOverrides["stitchMode"] as? String) ?? "auto"
|
|
871
|
+
self.batchStitchMode =
|
|
872
|
+
(["auto", "panorama", "scans"].contains(rawMode))
|
|
873
|
+
? rawMode : "auto"
|
|
874
|
+
// Reset accumulated-pose state for the new capture so
|
|
875
|
+
// finalize() picks a fresh mode.
|
|
876
|
+
self.batchFirstAcceptedPose = nil
|
|
877
|
+
self.batchLastAcceptedPose = nil
|
|
878
|
+
// 2026-05-22 (audit F2b) — reset IMU translation snapshot
|
|
879
|
+
// too. Updated at finalize() entry from JS-supplied
|
|
880
|
+
// option value.
|
|
881
|
+
self.batchImuTranslationMetres = 0.0
|
|
826
882
|
self.batchKeyframeMode = true
|
|
827
883
|
self.hybridEngine = nil
|
|
828
884
|
self.firstwinsEngine = nil
|
|
@@ -887,6 +943,21 @@ public final class IncrementalStitcher: NSObject {
|
|
|
887
943
|
(frameMode == "pose-based" || frameMode == "flow-based")
|
|
888
944
|
self.keyframeGate.strategy =
|
|
889
945
|
(frameMode == "flow-based") ? .flow : .pose
|
|
946
|
+
// 2026-05-22 (audit F1b) — non-AR-mode opt-out for the angular-
|
|
947
|
+
// delta fallback. iOS parity with Android IncrementalStitcher.kt:461.
|
|
948
|
+
// captureSource = 'non-ar' means the host is using vision-camera
|
|
949
|
+
// (no ARKit pose) — disable the gate's angular fallback so it
|
|
950
|
+
// doesn't compute on gyro-drift-driven garbage pose. Without
|
|
951
|
+
// this opt-out, gyro drift accumulates past the overlap
|
|
952
|
+
// threshold even on a stationary camera → near-identical
|
|
953
|
+
// frames get accepted → cv::Stitcher's camera-param estimator
|
|
954
|
+
// goes degenerate → "warpRoi too large (9581×12332) — estimator
|
|
955
|
+
// produced degenerate camera params" crash on finalize.
|
|
956
|
+
// Pre-audit iOS had this bug because the Swift facade had no
|
|
957
|
+
// disableAngularFallback property at all — the C++ setter
|
|
958
|
+
// existed but nothing on iOS ever called it.
|
|
959
|
+
let captureSource = (configOverrides["captureSource"] as? String) ?? "ar"
|
|
960
|
+
self.keyframeGate.disableAngularFallback = (captureSource == "non-ar")
|
|
890
961
|
if let v = configOverrides["keyframeOverlapThreshold"] as? Double {
|
|
891
962
|
self.keyframeGate.overlapThreshold = max(0.10, min(0.80, v))
|
|
892
963
|
} else {
|
|
@@ -1216,6 +1287,27 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1216
1287
|
? String(outputPath.dropFirst(7))
|
|
1217
1288
|
: outputPath
|
|
1218
1289
|
let q = max(1, min(100, jpegQuality))
|
|
1290
|
+
// 2026-05-22 (audit F2) — resolve 'auto' stitchMode now, while
|
|
1291
|
+
// we still have access to first/last pose ivars. The resolver
|
|
1292
|
+
// mirrors Android's resolveStitchModeAuto (IncrementalStitcher.kt:1727):
|
|
1293
|
+
// translation/rotation magnitude ratio between first + last
|
|
1294
|
+
// accepted keyframe poses → SCANS (translation-heavy) or
|
|
1295
|
+
// PANORAMA (rotation-heavy). Non-auto values pass through.
|
|
1296
|
+
let stitchModeResolved: String
|
|
1297
|
+
switch batchStitchMode {
|
|
1298
|
+
case "panorama": stitchModeResolved = "panorama"
|
|
1299
|
+
case "scans": stitchModeResolved = "scans"
|
|
1300
|
+
default: stitchModeResolved = resolveStitchModeAuto(
|
|
1301
|
+
first: batchFirstAcceptedPose,
|
|
1302
|
+
last: batchLastAcceptedPose,
|
|
1303
|
+
imuTranslationMetres: batchImuTranslationMetres
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
os_log(.fault, log: Self.diagLog,
|
|
1307
|
+
"[V16-batch-keyframe.stitchMode] configured=%{public}@ resolved=%{public}@ paths=%d imuT=%.3fm",
|
|
1308
|
+
batchStitchMode, stitchModeResolved, Int32(paths.count),
|
|
1309
|
+
batchImuTranslationMetres)
|
|
1310
|
+
|
|
1219
1311
|
let payload = FinalizePayload(
|
|
1220
1312
|
cleaned: cleaned,
|
|
1221
1313
|
q: q,
|
|
@@ -1228,6 +1320,7 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1228
1320
|
batchBlenderType: batchBlenderType,
|
|
1229
1321
|
batchSeamFinderType: batchSeamFinderType,
|
|
1230
1322
|
batchEnableInscribedRectCrop: batchEnableInscribedRectCrop,
|
|
1323
|
+
batchStitchModeResolved: stitchModeResolved,
|
|
1231
1324
|
keyframeExifOrientation: keyframeExifOrientation,
|
|
1232
1325
|
captureOrientation: captureOrientation,
|
|
1233
1326
|
drops: drops,
|
|
@@ -1492,7 +1585,8 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1492
1585
|
blenderType: payload.batchBlenderType,
|
|
1493
1586
|
seamFinderType: payload.batchSeamFinderType,
|
|
1494
1587
|
captureOrientation: payload.captureOrientation,
|
|
1495
|
-
useInscribedRectCrop: payload.batchEnableInscribedRectCrop
|
|
1588
|
+
useInscribedRectCrop: payload.batchEnableInscribedRectCrop,
|
|
1589
|
+
stitchMode: payload.batchStitchModeResolved
|
|
1496
1590
|
)
|
|
1497
1591
|
// V16 fix-attempt 9 (verified on device,
|
|
1498
1592
|
// 2026-05-13) — sentinel-result detection.
|
|
@@ -1569,6 +1663,14 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1569
1663
|
if r.finalConfidenceThresh >= 0 {
|
|
1570
1664
|
batchDict["finalConfidenceThresh"] = r.finalConfidenceThresh
|
|
1571
1665
|
}
|
|
1666
|
+
// 2026-05-22 (audit F2g) — surface the
|
|
1667
|
+
// auto-resolver's choice (or the operator's
|
|
1668
|
+
// explicit setting) so JS can show "scans"/
|
|
1669
|
+
// "panorama" on the output preview + debug
|
|
1670
|
+
// toast. Always set on the batch path —
|
|
1671
|
+
// helps the operator understand why the
|
|
1672
|
+
// panorama looks the way it does.
|
|
1673
|
+
batchDict["stitchModeResolved"] = payload.batchStitchModeResolved
|
|
1572
1674
|
completion(batchDict, nil)
|
|
1573
1675
|
} catch let stitchErr as NSError {
|
|
1574
1676
|
completion(nil, stitchErr)
|
|
@@ -1727,6 +1829,17 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1727
1829
|
let seam = (config["seamFinderType"] as? String) ?? "graphcut"
|
|
1728
1830
|
let orientation = (config["captureOrientation"] as? String) ?? "portrait"
|
|
1729
1831
|
let useInscribed = (config["useInscribedRectCrop"] as? Bool) ?? false
|
|
1832
|
+
// 2026-05-22 (audit F2) — refine path reads stitchMode too.
|
|
1833
|
+
// The refine flow doesn't have access to first/last accepted
|
|
1834
|
+
// pose ivars (this is a separate JS-driven entry point that
|
|
1835
|
+
// may be called against a saved keyframe set), so we accept
|
|
1836
|
+
// an explicit 'panorama' / 'scans' value here. Default
|
|
1837
|
+
// 'scans' for the refine path since it's typically called on
|
|
1838
|
+
// shelf-scan captures (the slow-path quality bake where SCANS'
|
|
1839
|
+
// translation tolerance gives best results — see docstring
|
|
1840
|
+
// at line 738 of src/stitching/incremental.ts). JS callers
|
|
1841
|
+
// can override by passing config["stitchMode"].
|
|
1842
|
+
let refineStitchMode = (config["stitchMode"] as? String) ?? "scans"
|
|
1730
1843
|
let quality = max(1, min(100, (config["jpegQuality"] as? Int) ?? 90))
|
|
1731
1844
|
let cleanedOutput = outputPath.hasPrefix("file://")
|
|
1732
1845
|
? String(outputPath.dropFirst(7))
|
|
@@ -1753,7 +1866,8 @@ public final class IncrementalStitcher: NSObject {
|
|
|
1753
1866
|
blenderType: blender,
|
|
1754
1867
|
seamFinderType: seam,
|
|
1755
1868
|
captureOrientation: orientation,
|
|
1756
|
-
useInscribedRectCrop: useInscribed
|
|
1869
|
+
useInscribedRectCrop: useInscribed,
|
|
1870
|
+
stitchMode: refineStitchMode
|
|
1757
1871
|
)
|
|
1758
1872
|
// fix-9 sentinel detection — see the finalize() path
|
|
1759
1873
|
// for the full rationale. A 0×0 result means
|
|
@@ -2026,11 +2140,37 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2026
2140
|
}
|
|
2027
2141
|
stateLock.unlock()
|
|
2028
2142
|
|
|
2029
|
-
//
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
)
|
|
2143
|
+
// 2026-05-21 (v0.3) — pixel-aware Flow-strategy evaluation.
|
|
2144
|
+
// Pre-0.3 this was `evaluate(pose:latchedPlane:)` with no pixel
|
|
2145
|
+
// buffer, which forced the C++ gate to silently fall back to
|
|
2146
|
+
// Pose strategy (same bug as Android non-AR; both fixed in
|
|
2147
|
+
// v0.3). We now decode the JPEG snapshot at `path` to a
|
|
2148
|
+
// single-channel grayscale CVPixelBuffer and pass it through,
|
|
2149
|
+
// so the gate's Flow strategy actually runs sparse-flow
|
|
2150
|
+
// novelty on real image content.
|
|
2151
|
+
//
|
|
2152
|
+
// CGImageSource → CGContext into a OneComponent8 CVPixelBuffer.
|
|
2153
|
+
// ~10-20 ms per snapshot on iPhone 13/16 Pro; well under the
|
|
2154
|
+
// ~250 ms non-AR snapshot interval (~4 FPS cadence). v0.4
|
|
2155
|
+
// will replace this path entirely by moving non-AR capture to
|
|
2156
|
+
// vision-camera's Frame Processor API (tracked at issue #11).
|
|
2157
|
+
let decision: KeyframeGateDecision
|
|
2158
|
+
if let grayBuffer = Self.decodeJpegToGrayscalePixelBuffer(path: path) {
|
|
2159
|
+
decision = self.keyframeGate.evaluate(
|
|
2160
|
+
pose: pose,
|
|
2161
|
+
latchedPlane: nil,
|
|
2162
|
+
pixelBuffer: grayBuffer
|
|
2163
|
+
)
|
|
2164
|
+
} else {
|
|
2165
|
+
// JPEG decode failed (corrupt file, OOM, etc.). Fall back
|
|
2166
|
+
// to pose-only so the capture doesn't lock up — matches
|
|
2167
|
+
// the C++ side's defensive grayData==nullptr handling
|
|
2168
|
+
// inside evaluateWithFrame.
|
|
2169
|
+
decision = self.keyframeGate.evaluate(
|
|
2170
|
+
pose: pose,
|
|
2171
|
+
latchedPlane: nil
|
|
2172
|
+
)
|
|
2173
|
+
}
|
|
2034
2174
|
if !decision.accept {
|
|
2035
2175
|
self.emitKeyframeRejectState(decision: decision)
|
|
2036
2176
|
return false
|
|
@@ -2041,6 +2181,13 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2041
2181
|
stateLock.lock()
|
|
2042
2182
|
self.keyframePaths.append(path)
|
|
2043
2183
|
self.keyframePoses.append(pose.asDictionary())
|
|
2184
|
+
// 2026-05-22 (audit F2) — track first + last pose for the
|
|
2185
|
+
// stitchMode auto-resolver. iOS parity: Android records
|
|
2186
|
+
// these in IncrementalStitcher.kt at the same accept points.
|
|
2187
|
+
let poseArr = [pose.tx, pose.ty, pose.tz,
|
|
2188
|
+
pose.qx, pose.qy, pose.qz, pose.qw]
|
|
2189
|
+
if self.batchFirstAcceptedPose == nil { self.batchFirstAcceptedPose = poseArr }
|
|
2190
|
+
self.batchLastAcceptedPose = poseArr
|
|
2044
2191
|
let count = self.keyframePaths.count
|
|
2045
2192
|
stateLock.unlock()
|
|
2046
2193
|
os_log(.fault, log: Self.diagLog,
|
|
@@ -2056,6 +2203,76 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2056
2203
|
return true
|
|
2057
2204
|
}
|
|
2058
2205
|
|
|
2206
|
+
/// 2026-05-21 (v0.3) — decode a JPEG file at the given path into a
|
|
2207
|
+
/// single-channel grayscale CVPixelBuffer (`kCVPixelFormatType_-
|
|
2208
|
+
/// OneComponent8`) suitable for feeding into the C++ KeyframeGate's
|
|
2209
|
+
/// Flow-strategy evaluate path. The bridge's `evaluatePixelBuffer:`
|
|
2210
|
+
/// has explicit OneComponent8 handling (added in v0.3) that reads
|
|
2211
|
+
/// the base address as the Y plane directly, so no extra conversion
|
|
2212
|
+
/// happens on the C++ side.
|
|
2213
|
+
///
|
|
2214
|
+
/// Used by `addBatchKeyframePath` (the JS-driver non-AR path) so the
|
|
2215
|
+
/// Flow strategy actually runs on real pixel data — pre-0.3 this
|
|
2216
|
+
/// path called `evaluate(pose:latchedPlane:)` with no buffer and
|
|
2217
|
+
/// the C++ side silently fell back to Pose strategy.
|
|
2218
|
+
///
|
|
2219
|
+
/// Performance: ~10-20 ms for a 1920×1080 JPEG on iPhone 13/16 Pro.
|
|
2220
|
+
/// Well under the ~250 ms non-AR snapshot interval (~4 FPS).
|
|
2221
|
+
/// v0.4 will replace this path entirely via Frame Processor — see
|
|
2222
|
+
/// issue #11.
|
|
2223
|
+
///
|
|
2224
|
+
/// Returns nil on any failure (file missing, corrupt JPEG, OOM
|
|
2225
|
+
/// on the CVPixelBufferCreate). Callers fall back to the
|
|
2226
|
+
/// pose-only evaluate so the capture doesn't lock up.
|
|
2227
|
+
private static func decodeJpegToGrayscalePixelBuffer(
|
|
2228
|
+
path: String
|
|
2229
|
+
) -> CVPixelBuffer? {
|
|
2230
|
+
let url = URL(fileURLWithPath: path)
|
|
2231
|
+
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
2232
|
+
let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
|
|
2233
|
+
else {
|
|
2234
|
+
return nil
|
|
2235
|
+
}
|
|
2236
|
+
let width = cgImage.width
|
|
2237
|
+
let height = cgImage.height
|
|
2238
|
+
|
|
2239
|
+
var pixelBuffer: CVPixelBuffer?
|
|
2240
|
+
let attrs: NSDictionary = [
|
|
2241
|
+
kCVPixelBufferIOSurfacePropertiesKey: NSDictionary(),
|
|
2242
|
+
]
|
|
2243
|
+
let status = CVPixelBufferCreate(
|
|
2244
|
+
kCFAllocatorDefault,
|
|
2245
|
+
width, height,
|
|
2246
|
+
kCVPixelFormatType_OneComponent8,
|
|
2247
|
+
attrs,
|
|
2248
|
+
&pixelBuffer
|
|
2249
|
+
)
|
|
2250
|
+
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
|
|
2251
|
+
return nil
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
CVPixelBufferLockBaseAddress(buffer, [])
|
|
2255
|
+
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
|
|
2256
|
+
guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
|
|
2257
|
+
return nil
|
|
2258
|
+
}
|
|
2259
|
+
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
|
|
2260
|
+
let colorSpace = CGColorSpaceCreateDeviceGray()
|
|
2261
|
+
guard let context = CGContext(
|
|
2262
|
+
data: baseAddress,
|
|
2263
|
+
width: width,
|
|
2264
|
+
height: height,
|
|
2265
|
+
bitsPerComponent: 8,
|
|
2266
|
+
bytesPerRow: bytesPerRow,
|
|
2267
|
+
space: colorSpace,
|
|
2268
|
+
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
2269
|
+
) else {
|
|
2270
|
+
return nil
|
|
2271
|
+
}
|
|
2272
|
+
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
2273
|
+
return buffer
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2059
2276
|
/// V16 Phase 1 — emit a state event when a batch-keyframe is
|
|
2060
2277
|
/// saved. Carries the on-disk thumbnail path so JS can render it
|
|
2061
2278
|
/// in LiveFrameStrip + advance the "Keyframes: N/M" pill.
|
|
@@ -2522,6 +2739,13 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2522
2739
|
self.stateLock.lock()
|
|
2523
2740
|
self.keyframePaths.append(record.path)
|
|
2524
2741
|
self.keyframePoses.append(pose.asDictionary())
|
|
2742
|
+
// 2026-05-22 (audit F2) — track first + last pose
|
|
2743
|
+
// for the stitchMode auto-resolver. Same as the
|
|
2744
|
+
// non-AR JS-driver site above.
|
|
2745
|
+
let poseArr = [pose.tx, pose.ty, pose.tz,
|
|
2746
|
+
pose.qx, pose.qy, pose.qz, pose.qw]
|
|
2747
|
+
if self.batchFirstAcceptedPose == nil { self.batchFirstAcceptedPose = poseArr }
|
|
2748
|
+
self.batchLastAcceptedPose = poseArr
|
|
2525
2749
|
let count = self.keyframePaths.count
|
|
2526
2750
|
self.stateLock.unlock()
|
|
2527
2751
|
os_log(.fault, log: Self.diagLog,
|
|
@@ -2708,6 +2932,96 @@ public final class IncrementalStitcher: NSObject {
|
|
|
2708
2932
|
let pitch = Double(asin(forward.y))
|
|
2709
2933
|
return (yaw, pitch)
|
|
2710
2934
|
}
|
|
2935
|
+
|
|
2936
|
+
/// 2026-05-22 (audit F2) — stitchMode auto-resolver. Port of
|
|
2937
|
+
/// Android's `resolveStitchModeAuto` (IncrementalStitcher.kt:1727).
|
|
2938
|
+
/// Picks PANORAMA vs SCANS based on the magnitude ratio of
|
|
2939
|
+
/// translation to rotation between first and last accepted
|
|
2940
|
+
/// keyframe poses:
|
|
2941
|
+
///
|
|
2942
|
+
/// translation_score = ‖t_last - t_first‖ / 0.10 (10cm ≈ 1.0)
|
|
2943
|
+
/// rotation_score = angle(fwd_last, fwd_first) / 1.00 (1 rad ≈ 1.0)
|
|
2944
|
+
/// ratio = translation_score / (translation_score + rotation_score)
|
|
2945
|
+
/// ratio ≥ 0.55 → "scans" (biased toward SCANS for safety)
|
|
2946
|
+
/// ratio < 0.55 → "panorama"
|
|
2947
|
+
///
|
|
2948
|
+
/// 2026-05-22 (audit F2b) — non-AR mode has no pose-derived
|
|
2949
|
+
/// translation (JS driver only sends yaw/pitch/quaternion), so
|
|
2950
|
+
/// the pose-only resolver always picked `panorama` even for
|
|
2951
|
+
/// shelf scans. Fold the JS-measured IMU translation magnitude
|
|
2952
|
+
/// into the resolver: use the LARGER of pose-translation and
|
|
2953
|
+
/// IMU-translation as `tMeters`. In AR mode pose-translation
|
|
2954
|
+
/// will dominate (accurate); in non-AR mode pose-translation is
|
|
2955
|
+
/// 0 and IMU translation provides the signal.
|
|
2956
|
+
///
|
|
2957
|
+
/// Returns "panorama" or "scans" — never "auto". Degenerate
|
|
2958
|
+
/// inputs (nil poses, no motion either source) default to
|
|
2959
|
+
/// "panorama" (safer for pure-rotation captures; SCANS on a
|
|
2960
|
+
/// translation-free input produces unbounded canvas growth).
|
|
2961
|
+
private func resolveStitchModeAuto(
|
|
2962
|
+
first: [Double]?,
|
|
2963
|
+
last: [Double]?,
|
|
2964
|
+
imuTranslationMetres: Double
|
|
2965
|
+
) -> String {
|
|
2966
|
+
guard let firstPose = first, firstPose.count == 7,
|
|
2967
|
+
let lastPose = last, lastPose.count == 7 else {
|
|
2968
|
+
// No pose data at all — fall back on whichever signal we
|
|
2969
|
+
// do have. imuTranslationMetres > 0 hints "scans"; 0
|
|
2970
|
+
// hints "panorama".
|
|
2971
|
+
return imuTranslationMetres > 0.05 ? "scans" : "panorama"
|
|
2972
|
+
}
|
|
2973
|
+
// Translation magnitude (Euclidean, in metres).
|
|
2974
|
+
let dtx = lastPose[0] - firstPose[0]
|
|
2975
|
+
let dty = lastPose[1] - firstPose[1]
|
|
2976
|
+
let dtz = lastPose[2] - firstPose[2]
|
|
2977
|
+
let tPose = (dtx*dtx + dty*dty + dtz*dtz).squareRoot()
|
|
2978
|
+
// Take the larger of pose-derived and IMU-measured. In AR
|
|
2979
|
+
// mode pose is accurate; in non-AR mode pose is 0 and IMU is
|
|
2980
|
+
// the only signal we have.
|
|
2981
|
+
let tMeters = max(tPose, imuTranslationMetres)
|
|
2982
|
+
// Rotation magnitude — angle between camera-forward vectors.
|
|
2983
|
+
// Camera-forward in body frame is (0, 0, -1) for ARKit/ARCore.
|
|
2984
|
+
let fwdFirst = qrotForwardZneg(
|
|
2985
|
+
firstPose[3], firstPose[4], firstPose[5], firstPose[6])
|
|
2986
|
+
let fwdLast = qrotForwardZneg(
|
|
2987
|
+
lastPose[3], lastPose[4], lastPose[5], lastPose[6])
|
|
2988
|
+
let dot = max(-1.0, min(1.0,
|
|
2989
|
+
fwdFirst.0 * fwdLast.0 + fwdFirst.1 * fwdLast.1 + fwdFirst.2 * fwdLast.2))
|
|
2990
|
+
let rRadians = acos(dot)
|
|
2991
|
+
// Normalisation: 10 cm of translation ≈ 1 rad of rotation as
|
|
2992
|
+
// "equivalent magnitude" for the ratio. Shelf scans cover
|
|
2993
|
+
// ~30 cm translation with ~10° (0.17 rad) rotation:
|
|
2994
|
+
// ratio = (0.30/0.10) / (3.0 + 0.17) = 0.95 → SCANS.
|
|
2995
|
+
// Pure 90° rotation panorama: 0 translation, 1.57 rad rotation:
|
|
2996
|
+
// ratio = 0 / (0 + 1.57) = 0.0 → PANORAMA.
|
|
2997
|
+
let tScore = tMeters / 0.10
|
|
2998
|
+
let rScore = rRadians / 1.00
|
|
2999
|
+
let denom = tScore + rScore
|
|
3000
|
+
if denom <= 1e-9 { return "panorama" } // no motion either way
|
|
3001
|
+
let ratio = tScore / denom
|
|
3002
|
+
os_log(.fault, log: Self.diagLog,
|
|
3003
|
+
"[stitchMode.auto] tPose=%.3fm tImu=%.3fm r=%.3frad ratio=%.3f → %{public}@",
|
|
3004
|
+
tPose, imuTranslationMetres, rRadians, ratio,
|
|
3005
|
+
ratio >= 0.55 ? "scans" : "panorama")
|
|
3006
|
+
return ratio >= 0.55 ? "scans" : "panorama"
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/// Closed-form q · (0,0,-1) · q⁻¹ — rotates the camera-forward
|
|
3010
|
+
/// unit vector by a unit quaternion (qx, qy, qz, qw). Same
|
|
3011
|
+
/// convention as `qrot` in cpp/keyframe_gate.cpp and
|
|
3012
|
+
/// `qrotForward` in IncrementalStitcher.kt.
|
|
3013
|
+
private func qrotForwardZneg(
|
|
3014
|
+
_ qx: Double, _ qy: Double, _ qz: Double, _ qw: Double
|
|
3015
|
+
) -> (Double, Double, Double) {
|
|
3016
|
+
// v = (0, 0, -1). Expansion:
|
|
3017
|
+
// v' = (-2(qx*qz + qw*qy),
|
|
3018
|
+
// -2(qy*qz - qw*qx),
|
|
3019
|
+
// -1 + 2(qx*qx + qy*qy))
|
|
3020
|
+
let x = -2.0 * (qx * qz + qw * qy)
|
|
3021
|
+
let y = -2.0 * (qy * qz - qw * qx)
|
|
3022
|
+
let z = -1.0 + 2.0 * (qx * qx + qy * qy)
|
|
3023
|
+
return (x, y, z)
|
|
3024
|
+
}
|
|
2711
3025
|
}
|
|
2712
3026
|
|
|
2713
3027
|
// ── Bridge contract for ARSession ───────────────────────────────────
|
|
@@ -202,6 +202,14 @@ public final class IncrementalStitcherBridge: RCTEventEmitter {
|
|
|
202
202
|
freshOrientation
|
|
203
203
|
)
|
|
204
204
|
}
|
|
205
|
+
// 2026-05-22 (audit F2b) — JS may pass cumulative IMU
|
|
206
|
+
// translation in METRES so the stitchMode auto-resolver has a
|
|
207
|
+
// translation signal in non-AR mode (where the JS-driver path
|
|
208
|
+
// doesn't carry pose tx/ty/tz). Always ≥ 0; defaults to 0 if
|
|
209
|
+
// unset (back-compat — auto-resolver falls back to pose data
|
|
210
|
+
// and to PANORAMA when both are 0).
|
|
211
|
+
let imuT = (options["imuTranslationMetres"] as? Double) ?? 0.0
|
|
212
|
+
IncrementalStitcher.shared.updateImuTranslationMetres(imuT)
|
|
205
213
|
IncrementalStitcher.shared.finalize(
|
|
206
214
|
toPath: outputPath,
|
|
207
215
|
jpegQuality: quality
|
|
@@ -167,6 +167,18 @@ final class KeyframeGate {
|
|
|
167
167
|
/// invoking `evaluate(...)`.
|
|
168
168
|
var flowEvalEveryNFrames: Int = 1
|
|
169
169
|
|
|
170
|
+
/// 2026-05-22 (audit F1b) — non-AR-mode opt-out for the angular-
|
|
171
|
+
/// delta fallback. See the bridge doc for the full rationale.
|
|
172
|
+
/// In short: set this to `true` in non-AR mode (captureSource =
|
|
173
|
+
/// 'non-ar') where there's no AR pose — the gate's angular-delta
|
|
174
|
+
/// computation would otherwise run on gyro-integrated drift and
|
|
175
|
+
/// accept near-identical frames → cv::Stitcher degenerate-param
|
|
176
|
+
/// crash. Default `false` (back-compat — AR mode uses the
|
|
177
|
+
/// fallback). Write-only; no read accessor on the C++ side.
|
|
178
|
+
var disableAngularFallback: Bool = false {
|
|
179
|
+
didSet { bridge.setDisableAngularFallback(disableAngularFallback) }
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
|
|
171
183
|
/// One-shot flag: when set to `true`, the very next evaluate()
|
|
172
184
|
/// accepts unconditionally and the flag self-resets. Set by JS
|
|
@@ -76,6 +76,19 @@ NS_SWIFT_NAME(KeyframeGateBridge)
|
|
|
76
76
|
/// See KeyframeGate.swift for the operator-facing description.
|
|
77
77
|
- (void)setFlowNoveltyPercentile:(double)percentile;
|
|
78
78
|
|
|
79
|
+
/// 2026-05-22 (audit F1b) — non-AR-mode opt-out for the angular-
|
|
80
|
+
/// delta fallback path. In non-AR captures there is no
|
|
81
|
+
/// ARKit/ARCore pose, so the gate's angular-delta computation runs
|
|
82
|
+
/// on gyro-integrated yaw/pitch which drifts ~1–2°/min. Drift
|
|
83
|
+
/// accumulates past the overlap threshold even when the camera
|
|
84
|
+
/// hasn't moved → near-identical frames get accepted → cv::Stitcher
|
|
85
|
+
/// camera-param estimator goes degenerate → "warpRoi too large"
|
|
86
|
+
/// crash on finalize. Set this to `true` in non-AR mode to disable
|
|
87
|
+
/// the angular-delta fallback entirely (the Flow strategy still
|
|
88
|
+
/// works when pixel data is supplied). Default `false`
|
|
89
|
+
/// (back-compat — AR mode uses the fallback).
|
|
90
|
+
- (void)setDisableAngularFallback:(BOOL)disabled;
|
|
91
|
+
|
|
79
92
|
// ── Read-only state ─────────────────────────────────────────────
|
|
80
93
|
- (BOOL)isEnabled;
|
|
81
94
|
- (NSInteger)acceptedCount;
|
|
@@ -126,6 +126,11 @@ static NSString *kReasonStringFor(retailens::KeyframeGateDecisionReason r) {
|
|
|
126
126
|
_gate.setFlowNoveltyPercentile(percentile);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
- (void)setDisableAngularFallback:(BOOL)disabled {
|
|
130
|
+
// 2026-05-22 (audit F1b) — see header doc for rationale.
|
|
131
|
+
_gate.setDisableAngularFallback(disabled ? true : false);
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
- (KGBDecision *)evaluateWithTx:(float)tx ty:(float)ty tz:(float)tz
|
|
130
135
|
qx:(float)qx qy:(float)qy qz:(float)qz qw:(float)qw
|
|
131
136
|
fx:(float)fx fy:(float)fy cx:(float)cx cy:(float)cy
|
|
@@ -254,6 +259,16 @@ static NSString *kReasonStringFor(retailens::KeyframeGateDecisionReason r) {
|
|
|
254
259
|
grayWidth = static_cast<int32_t>(bgraToGrayHolder.cols);
|
|
255
260
|
grayHeight = static_cast<int32_t>(bgraToGrayHolder.rows);
|
|
256
261
|
grayStride = static_cast<int32_t>(bgraToGrayHolder.step);
|
|
262
|
+
} else if (format == kCVPixelFormatType_OneComponent8) {
|
|
263
|
+
// Single-channel grayscale — used by the non-AR
|
|
264
|
+
// batch-keyframe path (v0.3+) which decodes the JPEG snapshot
|
|
265
|
+
// directly to grayscale before evaluating. Base address IS
|
|
266
|
+
// the Y plane; no conversion cost.
|
|
267
|
+
grayData = static_cast<const uint8_t *>(
|
|
268
|
+
CVPixelBufferGetBaseAddress(pixelBuffer));
|
|
269
|
+
grayWidth = static_cast<int32_t>(CVPixelBufferGetWidth(pixelBuffer));
|
|
270
|
+
grayHeight = static_cast<int32_t>(CVPixelBufferGetHeight(pixelBuffer));
|
|
271
|
+
grayStride = static_cast<int32_t>(CVPixelBufferGetBytesPerRow(pixelBuffer));
|
|
257
272
|
}
|
|
258
273
|
// else: grayData stays nullptr. The C++ gate detects this and
|
|
259
274
|
// falls back to the pose-only path inside evaluateWithFrame —
|
|
@@ -116,6 +116,7 @@ extern NSString *const RNImageStitcherErrorDomain;
|
|
|
116
116
|
seamFinderType:(nullable NSString *)seamFinderType
|
|
117
117
|
captureOrientation:(nullable NSString *)captureOrientation
|
|
118
118
|
useInscribedRectCrop:(BOOL)useInscribedRectCrop
|
|
119
|
+
stitchMode:(nullable NSString *)stitchMode
|
|
119
120
|
error:(NSError **)error;
|
|
120
121
|
|
|
121
122
|
/// Extract `maxFrames` evenly-spaced frames from the video at
|
|
@@ -388,6 +388,7 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
388
388
|
seamFinderType:(NSString *)seamFinderType
|
|
389
389
|
captureOrientation:(NSString *)captureOrientation
|
|
390
390
|
useInscribedRectCrop:(BOOL)useInscribedRectCrop
|
|
391
|
+
stitchMode:(NSString *)stitchMode
|
|
391
392
|
error:(NSError **)error {
|
|
392
393
|
// ── Phase 2 (2026-05-16): delegated to shared C++ ───────────────────
|
|
393
394
|
//
|
|
@@ -430,10 +431,19 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
430
431
|
cfg.captureOrientation = captureOrientation.UTF8String;
|
|
431
432
|
cfg.useInscribedRectCrop = (useInscribedRectCrop != NO);
|
|
432
433
|
cfg.jpegQuality = (int)quality;
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
434
|
+
// 2026-05-22 (audit F2) — stitchMode is now wired through. Caller
|
|
435
|
+
// (IncrementalStitcher.swift) reads the JS setting and, when set
|
|
436
|
+
// to 'auto', resolves to 'panorama' or 'scans' based on accumulated
|
|
437
|
+
// translation/rotation ratio (mirroring Android's
|
|
438
|
+
// resolveStitchModeAuto at IncrementalStitcher.kt:1727).
|
|
439
|
+
// Unknown / nil values fall through to Panorama (the historical
|
|
440
|
+
// hardcoded default — preserves behaviour for callers that haven't
|
|
441
|
+
// updated yet).
|
|
442
|
+
if ([stitchMode isEqualToString:@"scans"]) {
|
|
443
|
+
cfg.stitchMode = retailens::StitchMode::Scans;
|
|
444
|
+
} else {
|
|
445
|
+
cfg.stitchMode = retailens::StitchMode::Panorama;
|
|
446
|
+
}
|
|
437
447
|
// Pre-stitch memory-abort threshold inside the manual pipeline keys
|
|
438
448
|
// off this value. Plumb the device's physical RAM through so the
|
|
439
449
|
// heuristic scales correctly across the iPhone fleet (~2 GB legacy
|
|
@@ -813,6 +823,8 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
813
823
|
// "portrait" → no bake-rotation. Callers wanting rotation should
|
|
814
824
|
// use the keyframe-driven Swift path which carries the orientation
|
|
815
825
|
// from the JS accelerometer hook through IncrementalStitcher.
|
|
826
|
+
// 2026-05-22 (audit F2) — legacy video path passes nil stitchMode,
|
|
827
|
+
// which falls through to Panorama (preserves prior behaviour).
|
|
816
828
|
RNStitchResult *result =
|
|
817
829
|
[self stitchFramePaths:frames
|
|
818
830
|
outputPath:outputPath
|
|
@@ -822,6 +834,7 @@ cv::detail::CameraParams cameraParamsFromPose(NSDictionary *pose) {
|
|
|
822
834
|
seamFinderType:seamFinderType
|
|
823
835
|
captureOrientation:nil
|
|
824
836
|
useInscribedRectCrop:NO
|
|
837
|
+
stitchMode:nil
|
|
825
838
|
error:&stitchErr];
|
|
826
839
|
|
|
827
840
|
// Always tear down the tmp dir, success or fail — leaving
|
|
@@ -156,7 +156,12 @@ public enum Stitcher {
|
|
|
156
156
|
// in the panorama settings modal). Callers that want
|
|
157
157
|
// inscribed-rect can use IncrementalStitcher with
|
|
158
158
|
// the toggle on.
|
|
159
|
-
useInscribedRectCrop: false
|
|
159
|
+
useInscribedRectCrop: false,
|
|
160
|
+
// 2026-05-22 (audit F2) — legacy video-stitch API doesn't
|
|
161
|
+
// expose stitchMode in its options dict yet. nil falls
|
|
162
|
+
// through to Panorama in OpenCVStitcher.mm (preserves
|
|
163
|
+
// historical behaviour).
|
|
164
|
+
stitchMode: nil
|
|
160
165
|
)
|
|
161
166
|
return StitchResult(
|
|
162
167
|
outputPath: result.outputPath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"CHANGELOG.md"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
28
|
+
"build": "tsc -p tsconfig.build.json",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "jest",
|
|
30
31
|
"clean": "rm -rf dist",
|
|
31
32
|
"postinstall": "node scripts/postinstall-fetch-binaries.js"
|
|
32
33
|
},
|
|
@@ -52,13 +53,16 @@
|
|
|
52
53
|
},
|
|
53
54
|
"homepage": "https://github.com/bhargavkanda/react-native-image-stitcher#readme",
|
|
54
55
|
"devDependencies": {
|
|
56
|
+
"@types/jest": "^29.5.0",
|
|
55
57
|
"@types/react": "^19.0.0",
|
|
58
|
+
"jest": "^29.7.0",
|
|
56
59
|
"react": "^19.0.0",
|
|
57
60
|
"react-native": "^0.84.0",
|
|
58
61
|
"react-native-safe-area-context": "^4.0.0",
|
|
59
62
|
"react-native-sensors": "^7.0.0",
|
|
60
63
|
"react-native-vision-camera": "^4.0.0",
|
|
61
64
|
"rxjs": "^7.0.0",
|
|
65
|
+
"ts-jest": "^29.1.0",
|
|
62
66
|
"typescript": "^5.5.0"
|
|
63
67
|
},
|
|
64
68
|
"peerDependencies": {
|