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,46 @@
1
+ /**
2
+ * ViewportCropOverlay — V12.12.
3
+ *
4
+ * Translucent dim bars on the camera preview's PAN-AXIS edges
5
+ * showing where the panorama engine's source-crop is. Earlier
6
+ * versions (V12.11 Step B) put the bars on JS-top/bottom because
7
+ * the engine clipped the long sensor axis (perpendicular to pan
8
+ * in landscape, along pan in portrait) — that produced visible
9
+ * bars on the user-LEFT/RIGHT in landscape, which is the WRONG
10
+ * place: those edges aren't what the engine clips.
11
+ *
12
+ * V12.12: engine now clips ALONG the pan axis. In sensor-native
13
+ * coords:
14
+ * • landscape capture (vertical pan): clip = sensor Y (rows).
15
+ * User perceives this as TOP and BOTTOM of their landscape view.
16
+ * • portrait capture (horizontal pan): clip = sensor X (cols).
17
+ * User perceives this as LEFT and RIGHT of their portrait view.
18
+ *
19
+ * In JS coords (the host app is portrait-locked):
20
+ * • portrait device: user-left/right == JS-left/right. Bars on
21
+ * JS-left/right.
22
+ * • landscape device: user-top/bottom == JS-left/right (because
23
+ * the user's vertical maps to JS-horizontal
24
+ * under portrait-lock). Bars on JS-left/right.
25
+ *
26
+ * So in BOTH device orientations the bars sit at JS-left and JS-right.
27
+ * **No orientation detection needed in this component.** The
28
+ * engine has already arranged for the clip to manifest at the same
29
+ * JS edges regardless of physical device orientation.
30
+ *
31
+ * Bar width = `(1 - panFraction) / 2` of the JS-horizontal extent.
32
+ * For the default `kPanAxisFractionRect = 0.70` engine constant,
33
+ * each bar is 15 % wide — visibly substantial, matching what the
34
+ * engine clips out per frame.
35
+ */
36
+ import React from 'react';
37
+ export interface ViewportCropOverlayProps {
38
+ /**
39
+ * Fraction of the PAN axis the engine keeps per frame, in (0, 1].
40
+ * E.g. 0.70 for the V12.12 rectilinear engine's
41
+ * `kPanAxisFractionRect`. Values ≥ 1 hide the overlay (no clip).
42
+ */
43
+ panFraction: number;
44
+ }
45
+ export declare function ViewportCropOverlay({ panFraction, }: ViewportCropOverlayProps): React.JSX.Element | null;
46
+ //# sourceMappingURL=ViewportCropOverlay.d.ts.map
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * ViewportCropOverlay — V12.12.
5
+ *
6
+ * Translucent dim bars on the camera preview's PAN-AXIS edges
7
+ * showing where the panorama engine's source-crop is. Earlier
8
+ * versions (V12.11 Step B) put the bars on JS-top/bottom because
9
+ * the engine clipped the long sensor axis (perpendicular to pan
10
+ * in landscape, along pan in portrait) — that produced visible
11
+ * bars on the user-LEFT/RIGHT in landscape, which is the WRONG
12
+ * place: those edges aren't what the engine clips.
13
+ *
14
+ * V12.12: engine now clips ALONG the pan axis. In sensor-native
15
+ * coords:
16
+ * • landscape capture (vertical pan): clip = sensor Y (rows).
17
+ * User perceives this as TOP and BOTTOM of their landscape view.
18
+ * • portrait capture (horizontal pan): clip = sensor X (cols).
19
+ * User perceives this as LEFT and RIGHT of their portrait view.
20
+ *
21
+ * In JS coords (the host app is portrait-locked):
22
+ * • portrait device: user-left/right == JS-left/right. Bars on
23
+ * JS-left/right.
24
+ * • landscape device: user-top/bottom == JS-left/right (because
25
+ * the user's vertical maps to JS-horizontal
26
+ * under portrait-lock). Bars on JS-left/right.
27
+ *
28
+ * So in BOTH device orientations the bars sit at JS-left and JS-right.
29
+ * **No orientation detection needed in this component.** The
30
+ * engine has already arranged for the clip to manifest at the same
31
+ * JS edges regardless of physical device orientation.
32
+ *
33
+ * Bar width = `(1 - panFraction) / 2` of the JS-horizontal extent.
34
+ * For the default `kPanAxisFractionRect = 0.70` engine constant,
35
+ * each bar is 15 % wide — visibly substantial, matching what the
36
+ * engine clips out per frame.
37
+ */
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.ViewportCropOverlay = ViewportCropOverlay;
43
+ const react_1 = __importDefault(require("react"));
44
+ const react_native_1 = require("react-native");
45
+ function ViewportCropOverlay({ panFraction, }) {
46
+ if (panFraction >= 1)
47
+ return null;
48
+ // (1 - panFraction) / 2 of the JS-horizontal extent on each side.
49
+ const barPercent = `${((1 - panFraction) / 2) * 100}%`;
50
+ return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: styles.root },
51
+ react_1.default.createElement(react_native_1.View, { style: [styles.bar, { left: 0, top: 0, bottom: 0, width: barPercent }] }),
52
+ react_1.default.createElement(react_native_1.View, { style: [styles.bar, { right: 0, top: 0, bottom: 0, width: barPercent }] })));
53
+ }
54
+ const styles = react_native_1.StyleSheet.create({
55
+ root: {
56
+ ...react_native_1.StyleSheet.absoluteFillObject,
57
+ },
58
+ // Dim bars: translucent black overlay so the underlying camera
59
+ // preview is still visible (the user gets spatial context for
60
+ // what's about to leave the frame), but darkened enough to read
61
+ // as "this is OUTSIDE the capture region."
62
+ bar: {
63
+ position: 'absolute',
64
+ backgroundColor: 'rgba(0, 0, 0, 0.55)',
65
+ },
66
+ });
67
+ //# sourceMappingURL=ViewportCropOverlay.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * useCapture — React hook that encapsulates the camera capture state
3
+ * machine so host apps get a drop-in replacement for the ad-hoc
4
+ * vision-camera wiring they used to have inline on each screen.
5
+ *
6
+ * Responsibilities:
7
+ * - Holds the Camera ref for ``takePhoto``.
8
+ * - Tracks the device permission state and exposes a request helper.
9
+ * - Manages torch / flash state + a toggle helper.
10
+ * - Wraps takePhoto with a single-flight guard so a double-tap on
11
+ * the shutter button doesn't spawn two captures in parallel.
12
+ * - Runs an optional JS-side quality check on the captured image
13
+ * before resolving; the host app sees the QualityReport on the
14
+ * returned CaptureResult.
15
+ *
16
+ * Non-goals:
17
+ * - This hook does NOT persist captures. Host apps hand the
18
+ * returned CaptureResult to their own storage layer (WatermelonDB
19
+ * insert, Redux dispatch, whatever).
20
+ * - Video recording lives in useVideoCapture (TODO).
21
+ *
22
+ * The public API is designed to be minimal and replaceable: host apps
23
+ * that prefer the raw vision-camera API can opt out of this hook and
24
+ * still use the SDK's quality + stitching modules.
25
+ */
26
+ import { Camera, useCameraDevice, type PhysicalCameraDeviceType, type TakePhotoOptions } from 'react-native-vision-camera';
27
+ import type { CaptureResult, QualityThresholds } from '../types';
28
+ /**
29
+ * Hook input. Everything optional; sensible defaults are applied
30
+ * so simple call-sites can write ``useCapture()`` and get a usable
31
+ * back-camera pipeline with ``flash=off`` and no quality checking.
32
+ */
33
+ export interface UseCaptureOptions {
34
+ /** 'back' | 'front' — defaults to 'back' (shelf photos). */
35
+ cameraPosition?: 'back' | 'front';
36
+ /** Quality check toggle + thresholds. */
37
+ enableQualityChecks?: boolean;
38
+ qualityThresholds?: QualityThresholds;
39
+ /**
40
+ * Extra TakePhotoOptions to pass through to vision-camera.
41
+ * The SDK merges these with its defaults; host-supplied values win.
42
+ */
43
+ takePhotoOptions?: TakePhotoOptions;
44
+ /**
45
+ * 2026-05-14 — preferred physical-lens type for the chosen
46
+ * `cameraPosition`. Maps to vision-camera's `physicalDevices`
47
+ * filter on `useCameraDevice`.
48
+ *
49
+ * undefined (default) — use vision-camera's selection algorithm,
50
+ * which picks the device that combines
51
+ * the most lenses (typically the "main"
52
+ * multi-lens virtual camera). Existing
53
+ * behaviour; backwards-compatible.
54
+ * 'wide-angle-camera' — 1× physical lens (the standard rear
55
+ * camera most users think of as "the
56
+ * camera").
57
+ * 'ultra-wide-angle-camera' — 0.5× ultra-wide lens (only on
58
+ * devices with one; Samsung A35 has one;
59
+ * iPhone 11 Pro and later have one).
60
+ * 'telephoto-camera' — 2× / 3× telephoto if the device has
61
+ * one. Rare on field-rep deployments;
62
+ * exposed for symmetry.
63
+ *
64
+ * When the preferred type isn't available on the device, the
65
+ * hook falls back to vision-camera's default selection (i.e.,
66
+ * behaves as if `preferredPhysicalDevice` was undefined). The
67
+ * returned `availablePhysicalDevices` exposes what the device
68
+ * actually offers so the host can render an appropriate switcher.
69
+ */
70
+ preferredPhysicalDevice?: PhysicalCameraDeviceType;
71
+ }
72
+ /**
73
+ * Hook output. Intentionally flat so destructuring a subset is
74
+ * cheap and the API doesn't force callers to drill into nested
75
+ * objects for common concerns.
76
+ */
77
+ export interface UseCaptureReturn {
78
+ /** Pass to <CameraView ref={ref} /> (or the raw Camera directly). */
79
+ cameraRef: React.RefObject<Camera | null>;
80
+ /** The currently selected device — null while vision-camera hasn't picked one. */
81
+ device: ReturnType<typeof useCameraDevice>;
82
+ /** True once the user has granted camera permission. */
83
+ hasPermission: boolean;
84
+ /** Trigger the system permission sheet. Resolves to the new state. */
85
+ requestPermission: () => Promise<boolean>;
86
+ /** Current flash mode — controlled from host code. */
87
+ flash: 'off' | 'on';
88
+ toggleFlash: () => void;
89
+ /** True while takePhoto is in flight. Use to disable the shutter button. */
90
+ isCapturing: boolean;
91
+ /**
92
+ * Take a photo. Single-flight: parallel calls return the in-flight
93
+ * promise. Returns a CaptureResult (with an optional QualityReport
94
+ * when ``enableQualityChecks`` is on).
95
+ */
96
+ takePhoto: () => Promise<CaptureResult>;
97
+ /**
98
+ * 2026-05-14 — physical lens types available on the chosen
99
+ * `cameraPosition`. Computed once at the first vision-camera
100
+ * device-list emission; useful for the host to decide whether to
101
+ * render a 0.5×/1× camera switcher chip (only show if both
102
+ * `wide-angle-camera` AND `ultra-wide-angle-camera` are present).
103
+ *
104
+ * Empty array on platforms that haven't enumerated devices yet
105
+ * (very brief — vision-camera resolves the device list at module
106
+ * load). Always populated by the time the camera is mountable.
107
+ */
108
+ availablePhysicalDevices: PhysicalCameraDeviceType[];
109
+ }
110
+ export declare function useCapture(options?: UseCaptureOptions): UseCaptureReturn;
111
+ //# sourceMappingURL=useCapture.d.ts.map
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * useCapture — React hook that encapsulates the camera capture state
5
+ * machine so host apps get a drop-in replacement for the ad-hoc
6
+ * vision-camera wiring they used to have inline on each screen.
7
+ *
8
+ * Responsibilities:
9
+ * - Holds the Camera ref for ``takePhoto``.
10
+ * - Tracks the device permission state and exposes a request helper.
11
+ * - Manages torch / flash state + a toggle helper.
12
+ * - Wraps takePhoto with a single-flight guard so a double-tap on
13
+ * the shutter button doesn't spawn two captures in parallel.
14
+ * - Runs an optional JS-side quality check on the captured image
15
+ * before resolving; the host app sees the QualityReport on the
16
+ * returned CaptureResult.
17
+ *
18
+ * Non-goals:
19
+ * - This hook does NOT persist captures. Host apps hand the
20
+ * returned CaptureResult to their own storage layer (WatermelonDB
21
+ * insert, Redux dispatch, whatever).
22
+ * - Video recording lives in useVideoCapture (TODO).
23
+ *
24
+ * The public API is designed to be minimal and replaceable: host apps
25
+ * that prefer the raw vision-camera API can opt out of this hook and
26
+ * still use the SDK's quality + stitching modules.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.useCapture = useCapture;
30
+ const react_1 = require("react");
31
+ const react_native_vision_camera_1 = require("react-native-vision-camera");
32
+ const runQualityCheck_1 = require("../quality/runQualityCheck");
33
+ const normaliseOrientation_1 = require("../quality/normaliseOrientation");
34
+ function makeCaptureResult(photo, qualityReport) {
35
+ const capturedAt = new Date().toISOString();
36
+ return {
37
+ // The device UUID the host wants to identify this capture with is
38
+ // app-specific. We synthesise a deterministic ish value so the
39
+ // host gets a placeholder; most hosts will swap it out for a uuid
40
+ // library (react-native-uuid or similar) before persisting.
41
+ deviceUuid: `${capturedAt}-${photo.path.split('/').pop() ?? 'photo'}`,
42
+ compressedUri: `file://${photo.path}`,
43
+ // vision-camera reports width/height post-orientation-correction,
44
+ // matching what `<Image>` renders. Forwarding them lets the
45
+ // SDK's thumbnail strip / preview modal lay out at the correct
46
+ // aspect ratio instead of forcing square crops.
47
+ width: photo.width,
48
+ height: photo.height,
49
+ isStitched: false,
50
+ capturedAt,
51
+ qualityReport,
52
+ deviceMetadata: {
53
+ platform: 'ios',
54
+ osVersion: '',
55
+ deviceModel: '',
56
+ cameraId: '',
57
+ flashEnabled: false,
58
+ },
59
+ };
60
+ }
61
+ function useCapture(options = {}) {
62
+ const { cameraPosition = 'back', enableQualityChecks = false, qualityThresholds, takePhotoOptions, preferredPhysicalDevice, } = options;
63
+ const cameraRef = (0, react_1.useRef)(null);
64
+ // 2026-05-14 — physical-lens-aware device picker.
65
+ //
66
+ // When `preferredPhysicalDevice` is supplied, ask vision-camera
67
+ // for a device that exposes that specific physical lens (e.g.,
68
+ // 'ultra-wide-angle-camera'). Falls back to the position-default
69
+ // when the device doesn't have that lens. When undefined, behaves
70
+ // identically to the pre-2026-05-14 useCameraDevice(position) call.
71
+ const deviceWithPreferred = (0, react_native_vision_camera_1.useCameraDevice)(cameraPosition, {
72
+ physicalDevices: preferredPhysicalDevice ? [preferredPhysicalDevice] : undefined,
73
+ });
74
+ const deviceFallback = (0, react_native_vision_camera_1.useCameraDevice)(cameraPosition);
75
+ const device = deviceWithPreferred ?? deviceFallback;
76
+ // Enumerate ALL physical lens types available on the chosen
77
+ // position so the host can decide whether to render a switcher.
78
+ // Vision-camera's `useCameraDevices()` returns CameraDevice[]; each
79
+ // has `physicalDevices: PhysicalCameraDeviceType[]`. We dedupe the
80
+ // union across all devices at `position` so the host sees the full
81
+ // set the platform exposes (some phones expose ultra-wide only via
82
+ // a separate logical camera, not the main one).
83
+ const allDevices = (0, react_native_vision_camera_1.useCameraDevices)();
84
+ const availablePhysicalDevices = (0, react_1.useMemo)(() => {
85
+ const seen = new Set();
86
+ for (const d of allDevices) {
87
+ if (d.position !== cameraPosition)
88
+ continue;
89
+ for (const pd of d.physicalDevices ?? []) {
90
+ seen.add(pd);
91
+ }
92
+ }
93
+ return Array.from(seen);
94
+ }, [allDevices, cameraPosition]);
95
+ const { hasPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
96
+ const [flash, setFlash] = (0, react_1.useState)('off');
97
+ const [isCapturing, setIsCapturing] = (0, react_1.useState)(false);
98
+ // Holds the in-flight takePhoto promise so we don't kick off a second
99
+ // call while the first is still settling. Cleared in the finally.
100
+ const inFlightRef = (0, react_1.useRef)(null);
101
+ const toggleFlash = (0, react_1.useCallback)(() => {
102
+ setFlash((prev) => (prev === 'off' ? 'on' : 'off'));
103
+ }, []);
104
+ const takePhoto = (0, react_1.useCallback)(async () => {
105
+ if (inFlightRef.current) {
106
+ return inFlightRef.current;
107
+ }
108
+ if (!cameraRef.current) {
109
+ throw new Error('useCapture.takePhoto: cameraRef is not yet attached. '
110
+ + 'Render <CameraView ref={cameraRef} /> or the raw Camera with this ref first.');
111
+ }
112
+ const promise = (async () => {
113
+ setIsCapturing(true);
114
+ try {
115
+ const photo = await cameraRef.current.takePhoto({
116
+ flash,
117
+ ...takePhotoOptions,
118
+ });
119
+ // Bake EXIF rotation into pixels so the file on disk matches
120
+ // what the operator just saw on the preview, regardless of
121
+ // how downstream consumers handle EXIF. Returns the
122
+ // post-rotation dimensions; we override the photo's
123
+ // width/height before constructing the CaptureResult so
124
+ // the SDK contract reports "what's actually saved".
125
+ const normalised = await (0, normaliseOrientation_1.normaliseOrientation)(photo.path, {
126
+ width: photo.width,
127
+ height: photo.height,
128
+ });
129
+ const orientedPhoto = {
130
+ ...photo,
131
+ width: normalised.width || photo.width,
132
+ height: normalised.height || photo.height,
133
+ };
134
+ let report;
135
+ if (enableQualityChecks && qualityThresholds) {
136
+ report = await (0, runQualityCheck_1.runQualityCheck)(orientedPhoto.path, qualityThresholds);
137
+ }
138
+ return makeCaptureResult(orientedPhoto, report);
139
+ }
140
+ finally {
141
+ setIsCapturing(false);
142
+ inFlightRef.current = null;
143
+ }
144
+ })();
145
+ inFlightRef.current = promise;
146
+ return promise;
147
+ }, [flash, enableQualityChecks, qualityThresholds, takePhotoOptions]);
148
+ return {
149
+ cameraRef,
150
+ device,
151
+ hasPermission,
152
+ requestPermission,
153
+ flash,
154
+ toggleFlash,
155
+ isCapturing,
156
+ takePhoto,
157
+ availablePhysicalDevices,
158
+ };
159
+ }
160
+ //# sourceMappingURL=useCapture.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * useDeviceOrientation — physical device orientation hook.
3
+ *
4
+ * The host app is portrait-locked at the iOS app level (so the
5
+ * camera preview, header, controls, and thumbnails stay in their
6
+ * portrait positions even when the user holds the phone sideways
7
+ * for a vertical pan). But text overlays — the REC banner, the
8
+ * pan-speed pill, the live frame strip — need to follow the
9
+ * physical device orientation so they stay readable in the user's
10
+ * hands. RN's `useWindowDimensions` can't help with this when
11
+ * the app is orientation-locked: window dimensions don't change
12
+ * when only the device rotates.
13
+ *
14
+ * 2026-05-18 (Issue #3) — rewritten on top of `expo-sensors`
15
+ * `DeviceMotion` (CoreMotion-fused on iOS, SensorManager on
16
+ * Android). The previous implementation used
17
+ * `react-native-sensors` raw accelerometer with an Android-only
18
+ * sign convention (`y > 0` ⇒ portrait), which silently failed on
19
+ * iOS — Apple's CoreMotion convention is `y < 0` ⇒ portrait
20
+ * because device-Y points from the phone's bottom to the top,
21
+ * and gravity in that frame is `-Y`. Users on iOS saw the hook
22
+ * stuck at its initial value ('portrait') regardless of physical
23
+ * rotation, which cascaded into wrong panorama bake-rotation and
24
+ * a broken landscape band layout.
25
+ *
26
+ * Sign conventions used here (per platform docs):
27
+ *
28
+ * iOS (CMDeviceMotion.accelerationIncludingGravity, reported in
29
+ * m/s² in the device reference frame):
30
+ * portrait → y ≈ -9.8
31
+ * portrait-upside-down → y ≈ +9.8
32
+ * landscape-left (home indicator on user's RIGHT) → x ≈ +9.8
33
+ * landscape-right (home indicator on user's LEFT) → x ≈ -9.8
34
+ *
35
+ * Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention):
36
+ * portrait → y ≈ +9.8 ← opposite sign vs iOS
37
+ * portrait-upside-down → y ≈ -9.8
38
+ * landscape-left → x ≈ -9.8
39
+ * landscape-right → x ≈ +9.8
40
+ *
41
+ * We flip the Android x/y to match the iOS convention before
42
+ * classification so the rest of the logic stays platform-
43
+ * independent. The classification then unambiguously maps to
44
+ * the user-visible `DeviceOrientation` enum.
45
+ */
46
+ export type DeviceOrientation = 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
47
+ export declare function useDeviceOrientation(): DeviceOrientation;
48
+ //# sourceMappingURL=useDeviceOrientation.d.ts.map
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * useDeviceOrientation — physical device orientation hook.
5
+ *
6
+ * The host app is portrait-locked at the iOS app level (so the
7
+ * camera preview, header, controls, and thumbnails stay in their
8
+ * portrait positions even when the user holds the phone sideways
9
+ * for a vertical pan). But text overlays — the REC banner, the
10
+ * pan-speed pill, the live frame strip — need to follow the
11
+ * physical device orientation so they stay readable in the user's
12
+ * hands. RN's `useWindowDimensions` can't help with this when
13
+ * the app is orientation-locked: window dimensions don't change
14
+ * when only the device rotates.
15
+ *
16
+ * 2026-05-18 (Issue #3) — rewritten on top of `expo-sensors`
17
+ * `DeviceMotion` (CoreMotion-fused on iOS, SensorManager on
18
+ * Android). The previous implementation used
19
+ * `react-native-sensors` raw accelerometer with an Android-only
20
+ * sign convention (`y > 0` ⇒ portrait), which silently failed on
21
+ * iOS — Apple's CoreMotion convention is `y < 0` ⇒ portrait
22
+ * because device-Y points from the phone's bottom to the top,
23
+ * and gravity in that frame is `-Y`. Users on iOS saw the hook
24
+ * stuck at its initial value ('portrait') regardless of physical
25
+ * rotation, which cascaded into wrong panorama bake-rotation and
26
+ * a broken landscape band layout.
27
+ *
28
+ * Sign conventions used here (per platform docs):
29
+ *
30
+ * iOS (CMDeviceMotion.accelerationIncludingGravity, reported in
31
+ * m/s² in the device reference frame):
32
+ * portrait → y ≈ -9.8
33
+ * portrait-upside-down → y ≈ +9.8
34
+ * landscape-left (home indicator on user's RIGHT) → x ≈ +9.8
35
+ * landscape-right (home indicator on user's LEFT) → x ≈ -9.8
36
+ *
37
+ * Android (Sensor.TYPE_ACCELEROMETER, reaction-force convention):
38
+ * portrait → y ≈ +9.8 ← opposite sign vs iOS
39
+ * portrait-upside-down → y ≈ -9.8
40
+ * landscape-left → x ≈ -9.8
41
+ * landscape-right → x ≈ +9.8
42
+ *
43
+ * We flip the Android x/y to match the iOS convention before
44
+ * classification so the rest of the logic stays platform-
45
+ * independent. The classification then unambiguously maps to
46
+ * the user-visible `DeviceOrientation` enum.
47
+ */
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.useDeviceOrientation = useDeviceOrientation;
50
+ const react_1 = require("react");
51
+ const expo_sensors_1 = require("expo-sensors");
52
+ /// Threshold (m/s²) above which gravity dominance is considered
53
+ /// conclusive. 5 m/s² out of ~9.8 means the phone is at least ~30°
54
+ /// tilted toward that axis — comfortable for stable orientation
55
+ /// classification without flipping on minor wobbles.
56
+ const DOMINANT_AXIS_THRESHOLD = 5.0;
57
+ /// Sample at ~10 Hz — plenty for orientation detection (phones
58
+ /// don't physically flip faster than this).
59
+ const SAMPLE_INTERVAL_MS = 100;
60
+ function classify(x, y) {
61
+ // 2026-05-18 (Issue #3 round 2) — re-derived sign convention.
62
+ //
63
+ // Through expo-sensors, BOTH platforms normalize to the iOS
64
+ // CoreMotion gravity-vector convention: stationary phone reports
65
+ // the gravity vector itself in the device frame. Device axes:
66
+ // +X points from phone-left to phone-right; +Y from phone-bottom
67
+ // to phone-top; +Z out of the screen toward the viewer.
68
+ //
69
+ // Per-orientation gravity-vector signs in the device frame:
70
+ //
71
+ // portrait (upright) → y ≈ -9.8
72
+ // Phone-Y points up in world; gravity is along device -Y.
73
+ //
74
+ // portrait-upside-down → y ≈ +9.8
75
+ // Phone-Y points down in world; gravity is along device +Y.
76
+ //
77
+ // landscape-left (Apple: home indicator on user's RIGHT;
78
+ // phone rotated 90° CCW from portrait):
79
+ // phone-X axis points from user-bottom to user-top in this
80
+ // orientation, so gravity (world-down) is along device -X.
81
+ // → x ≈ -9.8
82
+ //
83
+ // landscape-right (Apple: home indicator on user's LEFT;
84
+ // phone rotated 90° CW from portrait):
85
+ // phone-X axis points from user-top to user-bottom, so
86
+ // gravity is along device +X.
87
+ // → x ≈ +9.8
88
+ //
89
+ // The earlier implementation had an Android-specific axis flip
90
+ // baked in. Removed — expo-sensors normalizes Android signs to
91
+ // match iOS, and the platform branch was producing wrong values
92
+ // (Android portrait → reported as portrait-upside-down; iOS
93
+ // landscape-left → reported as landscape-right).
94
+ if (Math.abs(y) > Math.abs(x)) {
95
+ if (y < -DOMINANT_AXIS_THRESHOLD)
96
+ return 'portrait';
97
+ if (y > DOMINANT_AXIS_THRESHOLD)
98
+ return 'portrait-upside-down';
99
+ }
100
+ else {
101
+ if (x < -DOMINANT_AXIS_THRESHOLD)
102
+ return 'landscape-left';
103
+ if (x > DOMINANT_AXIS_THRESHOLD)
104
+ return 'landscape-right';
105
+ }
106
+ // Phone face-up or face-down (z dominates): keep the previous
107
+ // orientation rather than flicker.
108
+ return null;
109
+ }
110
+ function useDeviceOrientation() {
111
+ const [orientation, setOrientation] = (0, react_1.useState)('portrait');
112
+ (0, react_1.useEffect)(() => {
113
+ expo_sensors_1.DeviceMotion.setUpdateInterval(SAMPLE_INTERVAL_MS);
114
+ let last = 'portrait';
115
+ const sub = expo_sensors_1.DeviceMotion.addListener((m) => {
116
+ const g = m.accelerationIncludingGravity;
117
+ // First emissions can be null on cold start while CoreMotion
118
+ // warms up; skip until data arrives.
119
+ if (!g)
120
+ return;
121
+ const next = classify(g.x, g.y);
122
+ if (next && next !== last) {
123
+ last = next;
124
+ setOrientation(next);
125
+ }
126
+ });
127
+ return () => sub.remove();
128
+ }, []);
129
+ return orientation;
130
+ }
131
+ //# sourceMappingURL=useDeviceOrientation.js.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * useVideoCapture — video recording + frame extraction API.
3
+ *
4
+ * Sibling of ``useCapture`` for the "sweep a shelf with a video" flow
5
+ * that feeds the stitcher. The hook's state machine is:
6
+ *
7
+ * idle → recording → stopping → idle (success)
8
+ * idle → recording → idle (recording errored)
9
+ * idle → recording → stopping → idle (user cancelled)
10
+ *
11
+ * API shape — `startRecording` returns a `Promise<VideoFile>` that:
12
+ * - resolves with the recorded file when the user releases the
13
+ * shutter and `stopRecording()` is called (vision-camera fires
14
+ * `onRecordingFinished`),
15
+ * - rejects if vision-camera fires `onRecordingError` at any point
16
+ * (e.g. `<Camera>` was rendered without `video={true}`, disk
17
+ * full mid-recording, permission revoked).
18
+ *
19
+ * Why a single future instead of separate finished/error callbacks?
20
+ * Lets host code use the natural async/await pattern. Earlier we
21
+ * used a callback + a parked resolver in the host screen; that
22
+ * masked start-time errors (the resolver hung forever) and produced
23
+ * confusing cascade errors when `stopRecording` ran against a
24
+ * non-recording camera.
25
+ *
26
+ * Frame extraction lives behind the `extractFrames` method but is
27
+ * deferred to the higher-level `stitchVideo()` SDK API now — host
28
+ * apps shouldn't have to manage their own tmp dir. This shim is
29
+ * kept for parity / future use; it currently throws.
30
+ */
31
+ import { Camera, type VideoFile } from 'react-native-vision-camera';
32
+ export type VideoCaptureState = 'idle' | 'recording' | 'stopping' | 'extracting';
33
+ export interface UseVideoCaptureReturn {
34
+ state: VideoCaptureState;
35
+ /**
36
+ * Begin recording on the given camera ref. Returns a Promise that
37
+ * resolves with the resulting `VideoFile` once `stopRecording()`
38
+ * has been called and vision-camera writes the file to disk, or
39
+ * rejects on any recording error.
40
+ *
41
+ * Callers who only need fire-and-forget can ignore the returned
42
+ * promise; the hook still tracks state internally.
43
+ */
44
+ startRecording: (cameraRef: React.RefObject<Camera | null>) => Promise<VideoFile>;
45
+ /**
46
+ * Tell vision-camera to stop the active recording. If no recording
47
+ * is in progress (e.g. it errored at start) this is a safe no-op.
48
+ * The recorded file flows back through the promise returned by
49
+ * `startRecording`, NOT through this method's return.
50
+ */
51
+ stopRecording: (cameraRef: React.RefObject<Camera | null>) => Promise<void>;
52
+ /**
53
+ * Cancel the active recording without delivering the file. The
54
+ * promise from `startRecording` will reject with a cancellation
55
+ * error so awaiters unblock instead of receiving a stale file.
56
+ */
57
+ cancelRecording: (cameraRef: React.RefObject<Camera | null>) => Promise<void>;
58
+ /**
59
+ * Decode an mp4 into N evenly-spaced still frames, written to
60
+ * ``outputDir``. Throws NOT_IMPLEMENTED — call `stitchVideo` from
61
+ * the SDK index instead, which combines extract + stitch in one
62
+ * native bridge call so the JS thread never has to round-trip
63
+ * frame paths.
64
+ */
65
+ extractFrames: (opts: ExtractFramesOptions) => Promise<ExtractFramesResult>;
66
+ }
67
+ export interface ExtractFramesOptions {
68
+ videoPath: string;
69
+ outputDir: string;
70
+ frameCount: number;
71
+ /** JPEG quality [0-100]. Defaults to 85. */
72
+ quality?: number;
73
+ }
74
+ export interface ExtractFramesResult {
75
+ framePaths: string[];
76
+ durationMs: number;
77
+ }
78
+ export declare function useVideoCapture(): UseVideoCaptureReturn;
79
+ //# sourceMappingURL=useVideoCapture.d.ts.map