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,80 @@
1
+ /**
2
+ * CameraShutter — dual-mode shutter button for the SDK's panorama UX.
3
+ *
4
+ * ┌──────────────────────────────────────────────────┐
5
+ * │ TAP → take a single photo (existing flow) │
6
+ * │ HOLD → start recording video │
7
+ * │ RELEASE → stop recording → return video file │
8
+ * └──────────────────────────────────────────────────┘
9
+ *
10
+ * The button is "pure" UI — it owns gesture detection + visual
11
+ * feedback (idle / pressing / recording / processing rings) but
12
+ * NOT the recording / stitching pipeline itself. Host apps wire
13
+ * the resulting `onTap` / `onHoldComplete` callbacks to whatever
14
+ * `useCapture` / `useVideoCapture` instance they've configured.
15
+ *
16
+ * Why expose just the button (not the full surface)?
17
+ * Different audit screens want different layouts (thumbnails,
18
+ * quality badges, mode chips); pinning them all to one
19
+ * "PanoramaCaptureSurface" would overfit one customer's UX. The
20
+ * button is the only piece every screen needs identical, so it
21
+ * ships in the SDK. The orchestration helpers ship as
22
+ * `<PanoramaCaptureSurface>` next door — host apps that want full
23
+ * plug-and-play use that; the rest stitch this button into their
24
+ * own layout.
25
+ *
26
+ * Gesture detection
27
+ * onPressIn fires immediately on touch down; we start a
28
+ * ``HOLD_THRESHOLD_MS`` timer. Two outcomes:
29
+ *
30
+ * - onPressOut fires before the timer → it was a tap.
31
+ * - timer fires before onPressOut → transition to recording
32
+ * state, fire ``onHoldStart``. When onPressOut eventually
33
+ * fires we call ``onHoldComplete``.
34
+ *
35
+ * We deliberately do NOT use react-native-gesture-handler — the
36
+ * Pressable + setTimeout pattern stays in the SDK's existing dep
37
+ * surface (RN core only). Adds zero new peer deps for host apps.
38
+ */
39
+ import React from 'react';
40
+ import { type ViewStyle } from 'react-native';
41
+ export interface CameraShutterProps {
42
+ /** Called when the user taps (press-and-release before the threshold). */
43
+ onTap: () => void;
44
+ /** Called when the press crosses the threshold and recording should start. */
45
+ onHoldStart: () => void;
46
+ /** Called on release while in the hold state — recording should stop. */
47
+ onHoldComplete: () => void;
48
+ /**
49
+ * Maximum hold duration in milliseconds. When the timer fires
50
+ * we auto-fire `onHoldComplete` — same behaviour as the user
51
+ * releasing the button. Default 8000 ms; keeps recording
52
+ * within the stitcher's adjacent-frame-overlap budget
53
+ * (16 frames × 2 fps = 8 s upper bound). Pass 0 / undefined
54
+ * to disable the auto-stop.
55
+ *
56
+ * Pair with `<CaptureStatusOverlay countdownMs>` so the user
57
+ * sees how much hold time is left.
58
+ */
59
+ maxHoldMs?: number;
60
+ /**
61
+ * Optional state-driven visual override. When the host has its own
62
+ * processing indicator (e.g. "Stitching... 70%") set this to true to
63
+ * paint the button in the disabled-while-processing visual.
64
+ */
65
+ isProcessing?: boolean;
66
+ /** Disable the whole button (e.g. while permissions are loading). */
67
+ disabled?: boolean;
68
+ /** Optional style applied to the outer touch target. */
69
+ style?: ViewStyle;
70
+ }
71
+ /**
72
+ * Imperative handle so a parent can force-release (e.g. on unmount
73
+ * during a long press). Exposed via forwardRef.
74
+ */
75
+ export interface CameraShutterHandle {
76
+ /** Cancel any in-flight hold without calling onHoldComplete. */
77
+ cancelHold: () => void;
78
+ }
79
+ export declare const CameraShutter: React.ForwardRefExoticComponent<CameraShutterProps & React.RefAttributes<CameraShutterHandle>>;
80
+ //# sourceMappingURL=CameraShutter.d.ts.map
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * CameraShutter — dual-mode shutter button for the SDK's panorama UX.
5
+ *
6
+ * ┌──────────────────────────────────────────────────┐
7
+ * │ TAP → take a single photo (existing flow) │
8
+ * │ HOLD → start recording video │
9
+ * │ RELEASE → stop recording → return video file │
10
+ * └──────────────────────────────────────────────────┘
11
+ *
12
+ * The button is "pure" UI — it owns gesture detection + visual
13
+ * feedback (idle / pressing / recording / processing rings) but
14
+ * NOT the recording / stitching pipeline itself. Host apps wire
15
+ * the resulting `onTap` / `onHoldComplete` callbacks to whatever
16
+ * `useCapture` / `useVideoCapture` instance they've configured.
17
+ *
18
+ * Why expose just the button (not the full surface)?
19
+ * Different audit screens want different layouts (thumbnails,
20
+ * quality badges, mode chips); pinning them all to one
21
+ * "PanoramaCaptureSurface" would overfit one customer's UX. The
22
+ * button is the only piece every screen needs identical, so it
23
+ * ships in the SDK. The orchestration helpers ship as
24
+ * `<PanoramaCaptureSurface>` next door — host apps that want full
25
+ * plug-and-play use that; the rest stitch this button into their
26
+ * own layout.
27
+ *
28
+ * Gesture detection
29
+ * onPressIn fires immediately on touch down; we start a
30
+ * ``HOLD_THRESHOLD_MS`` timer. Two outcomes:
31
+ *
32
+ * - onPressOut fires before the timer → it was a tap.
33
+ * - timer fires before onPressOut → transition to recording
34
+ * state, fire ``onHoldStart``. When onPressOut eventually
35
+ * fires we call ``onHoldComplete``.
36
+ *
37
+ * We deliberately do NOT use react-native-gesture-handler — the
38
+ * Pressable + setTimeout pattern stays in the SDK's existing dep
39
+ * surface (RN core only). Adds zero new peer deps for host apps.
40
+ */
41
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
42
+ if (k2 === undefined) k2 = k;
43
+ var desc = Object.getOwnPropertyDescriptor(m, k);
44
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
45
+ desc = { enumerable: true, get: function() { return m[k]; } };
46
+ }
47
+ Object.defineProperty(o, k2, desc);
48
+ }) : (function(o, m, k, k2) {
49
+ if (k2 === undefined) k2 = k;
50
+ o[k2] = m[k];
51
+ }));
52
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
53
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
54
+ }) : function(o, v) {
55
+ o["default"] = v;
56
+ });
57
+ var __importStar = (this && this.__importStar) || (function () {
58
+ var ownKeys = function(o) {
59
+ ownKeys = Object.getOwnPropertyNames || function (o) {
60
+ var ar = [];
61
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
62
+ return ar;
63
+ };
64
+ return ownKeys(o);
65
+ };
66
+ return function (mod) {
67
+ if (mod && mod.__esModule) return mod;
68
+ var result = {};
69
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
70
+ __setModuleDefault(result, mod);
71
+ return result;
72
+ };
73
+ })();
74
+ Object.defineProperty(exports, "__esModule", { value: true });
75
+ exports.CameraShutter = void 0;
76
+ const react_1 = __importStar(require("react"));
77
+ const react_native_1 = require("react-native");
78
+ /// Time the user must hold before tap → hold mode flip. 250 ms is
79
+ /// the iOS native "long press" default; matches user muscle memory
80
+ /// for distinguishing "snap" from "stay".
81
+ const HOLD_THRESHOLD_MS = 250;
82
+ exports.CameraShutter = (0, react_1.forwardRef)(function CameraShutter({ onTap, onHoldStart, onHoldComplete, maxHoldMs, isProcessing = false, disabled = false, style, }, ref) {
83
+ // Phase machine. We use a state value for re-render-driven
84
+ // visuals AND a ref so onPressOut can read the up-to-date phase
85
+ // without waiting on React's render cycle (otherwise the
86
+ // tap-vs-hold decision can race the timer).
87
+ const [phase, setPhase] = (0, react_1.useState)('idle');
88
+ const phaseRef = (0, react_1.useRef)('idle');
89
+ const holdTimerRef = (0, react_1.useRef)(null);
90
+ // Separate timer for the auto-stop (max hold). Distinct from
91
+ // the tap-vs-hold detection timer so each can fire independently.
92
+ const maxHoldTimerRef = (0, react_1.useRef)(null);
93
+ const setPhaseBoth = (0, react_1.useCallback)((next) => {
94
+ phaseRef.current = next;
95
+ setPhase(next);
96
+ }, []);
97
+ const clearHoldTimer = (0, react_1.useCallback)(() => {
98
+ if (holdTimerRef.current !== null) {
99
+ clearTimeout(holdTimerRef.current);
100
+ holdTimerRef.current = null;
101
+ }
102
+ }, []);
103
+ const clearMaxHoldTimer = (0, react_1.useCallback)(() => {
104
+ if (maxHoldTimerRef.current !== null) {
105
+ clearTimeout(maxHoldTimerRef.current);
106
+ maxHoldTimerRef.current = null;
107
+ }
108
+ }, []);
109
+ const cancelHold = (0, react_1.useCallback)(() => {
110
+ clearHoldTimer();
111
+ clearMaxHoldTimer();
112
+ setPhaseBoth('idle');
113
+ }, [clearHoldTimer, clearMaxHoldTimer, setPhaseBoth]);
114
+ (0, react_1.useImperativeHandle)(ref, () => ({ cancelHold }), [cancelHold]);
115
+ // Belt-and-suspenders: clean both timers on unmount so a
116
+ // fast navigation away from the camera doesn't leave one
117
+ // firing into a stale closure.
118
+ (0, react_1.useEffect)(() => () => {
119
+ clearHoldTimer();
120
+ clearMaxHoldTimer();
121
+ }, [clearHoldTimer, clearMaxHoldTimer]);
122
+ const handlePressIn = (0, react_1.useCallback)(() => {
123
+ if (disabled || isProcessing)
124
+ return;
125
+ setPhaseBoth('pressing');
126
+ holdTimerRef.current = setTimeout(() => {
127
+ // Threshold elapsed → enter hold mode + notify.
128
+ if (phaseRef.current === 'pressing') {
129
+ setPhaseBoth('holding');
130
+ onHoldStart();
131
+ // Schedule the auto-stop if maxHoldMs is set. Same
132
+ // outcome as the user releasing the button manually —
133
+ // fires onHoldComplete + drops back to idle.
134
+ if (maxHoldMs && maxHoldMs > 0) {
135
+ maxHoldTimerRef.current = setTimeout(() => {
136
+ // Auto-stop unconditionally after maxHoldMs. Earlier
137
+ // versions gated this on `phase === 'holding'`, which
138
+ // skipped the fire when iOS' gesture recogniser had
139
+ // already flipped the phase to 'idle' due to finger
140
+ // drift from camera motion — leaving the engine running
141
+ // for hundreds of frames after the user thought they
142
+ // released. An extra onHoldComplete call when nothing
143
+ // is recording is a safe no-op (`!incremental.isRunning`
144
+ // early-returns).
145
+ if (phaseRef.current === 'holding')
146
+ setPhaseBoth('idle');
147
+ onHoldComplete();
148
+ }, maxHoldMs);
149
+ }
150
+ }
151
+ }, HOLD_THRESHOLD_MS);
152
+ }, [disabled, isProcessing, onHoldStart, onHoldComplete, maxHoldMs, setPhaseBoth]);
153
+ const handlePressOut = (0, react_1.useCallback)(() => {
154
+ // CRITICAL: release ALWAYS stops the recording, regardless of
155
+ // disabled/isProcessing state. The previous version returned
156
+ // early when `isProcessing === true`, silently swallowing the
157
+ // release. When that happened mid-recording, `onHoldComplete`
158
+ // never fired, the engine kept ingesting AR frames forever
159
+ // (hundreds of frames stacked up before the user even noticed),
160
+ // and the final stitch ran on data the user never intended.
161
+ //
162
+ // The release event is the user's primary signal that they
163
+ // want this capture to end. No internal state is allowed to
164
+ // block it. `onHoldComplete` itself is idempotent (it
165
+ // early-returns when there's nothing running), so an extra
166
+ // call when the engine is already finishing is a safe no-op.
167
+ const wasHolding = phaseRef.current === 'holding';
168
+ clearHoldTimer();
169
+ clearMaxHoldTimer();
170
+ setPhaseBoth('idle');
171
+ if (wasHolding) {
172
+ onHoldComplete();
173
+ }
174
+ else if (!disabled && !isProcessing) {
175
+ // It was a tap (released before the threshold). Suppress
176
+ // the tap when the camera is busy — taps trigger photos and
177
+ // we don't want to fire-and-forget into a busy pipeline.
178
+ onTap();
179
+ }
180
+ }, [disabled, isProcessing, onTap, onHoldComplete, clearHoldTimer, clearMaxHoldTimer, setPhaseBoth]);
181
+ // Visuals. Three layered circles so the inner colour can swap
182
+ // without animating the outer ring (smoother on lower-end phones).
183
+ const innerStyle = isProcessing
184
+ ? styles.innerProcessing
185
+ : phase === 'holding'
186
+ ? styles.innerRecording
187
+ : phase === 'pressing'
188
+ ? styles.innerPressing
189
+ : styles.innerIdle;
190
+ return (react_1.default.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: phase === 'holding'
191
+ ? 'Recording — release to stitch panorama'
192
+ : 'Tap for photo, hold for panorama', accessibilityState: { disabled: disabled || isProcessing }, disabled: disabled || isProcessing, onPressIn: handlePressIn, onPressOut: handlePressOut, style: [styles.outer, disabled && styles.disabled, style] },
193
+ react_1.default.createElement(react_native_1.View, { style: styles.ring }),
194
+ react_1.default.createElement(react_native_1.View, { style: [styles.inner, innerStyle] })));
195
+ });
196
+ const styles = react_native_1.StyleSheet.create({
197
+ outer: {
198
+ width: 76,
199
+ height: 76,
200
+ alignItems: 'center',
201
+ justifyContent: 'center',
202
+ },
203
+ disabled: {
204
+ opacity: 0.4,
205
+ },
206
+ ring: {
207
+ position: 'absolute',
208
+ width: 76,
209
+ height: 76,
210
+ borderRadius: 38,
211
+ borderWidth: 4,
212
+ borderColor: '#ffffff',
213
+ },
214
+ inner: {
215
+ width: 60,
216
+ height: 60,
217
+ borderRadius: 30,
218
+ },
219
+ innerIdle: {
220
+ backgroundColor: '#ffffff',
221
+ },
222
+ innerPressing: {
223
+ // Subtle shrink-effect via colour shift; opacity dims confirm
224
+ // touch landed without committing to a mode yet.
225
+ backgroundColor: 'rgba(255,255,255,0.7)',
226
+ },
227
+ innerRecording: {
228
+ // Apple-native panorama / shutter recording uses red.
229
+ backgroundColor: '#FF3B30',
230
+ },
231
+ innerProcessing: {
232
+ // Greyed mid-tone with reduced contrast — clearly "busy, can't
233
+ // press me" without being alarming.
234
+ backgroundColor: '#9aa0a6',
235
+ },
236
+ });
237
+ //# sourceMappingURL=CameraShutter.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CameraView — the SDK's drop-in replacement for the raw
3
+ * vision-camera ``<Camera />``.
4
+ *
5
+ * Why wrap it?
6
+ * 1. **Default props** — always ``isActive={true}``, ``photo={true}``,
7
+ * and honouring the hook's flash state. Every call-site in the
8
+ * mobile app repeated the same tuple; the SDK canonicalises it.
9
+ * 2. **Branded guidance overlay** — optional ``guidance`` prop renders
10
+ * a themed banner over the preview without the host app having to
11
+ * know about positioning / contrast.
12
+ * 3. **Forward ref** — so ``useCapture``'s ref attaches cleanly.
13
+ *
14
+ * The component is intentionally thin — anything more elaborate goes
15
+ * into a separate screen (e.g. AuditCaptureSurface that combines this
16
+ * view with thumbnails and a shutter button). Keeping CameraView at
17
+ * the vision-camera layer means host apps that want a highly-custom
18
+ * UI can still use it as their building block.
19
+ */
20
+ import React from 'react';
21
+ import { type ViewStyle } from 'react-native';
22
+ import { Camera, type CameraDevice, type CameraProps } from 'react-native-vision-camera';
23
+ export interface CameraViewProps {
24
+ /** Output of ``useCapture().device``. If null, a placeholder is shown. */
25
+ device: CameraDevice | null | undefined;
26
+ /** Flash / torch state from ``useCapture().flash``. */
27
+ flash?: 'off' | 'on';
28
+ /** Whether the preview is actively rendering. Defaults to true. */
29
+ isActive?: boolean;
30
+ /**
31
+ * Enable video recording on the underlying camera. Required for
32
+ * `useVideoCapture().startRecording()` — vision-camera throws
33
+ * `capture/video-not-enabled` if you call startRecording without
34
+ * this flag set. Defaults to `false` so apps that only take photos
35
+ * don't pay the video-pipeline allocation cost.
36
+ *
37
+ * Photo capture remains enabled regardless of this flag, so a
38
+ * single `<CameraView video />` can do both tap (photo) and
39
+ * hold (video → stitch) flows.
40
+ */
41
+ video?: boolean;
42
+ /** Optional themed guidance banner. Renders over the preview at the top. */
43
+ guidance?: string;
44
+ /** Extra style layer applied on top of the default full-screen layout. */
45
+ style?: ViewStyle;
46
+ /** Pass-through to vision-camera for anything custom. */
47
+ cameraProps?: Partial<CameraProps>;
48
+ /**
49
+ * Called when the user taps the preview. Host apps may use this to
50
+ * drive focus-on-tap, AE/AF lock, etc. Not wired into vision-camera's
51
+ * focus API by this component on purpose — host apps have different
52
+ * preferences (focus-on-tap vs. tap-to-lock).
53
+ */
54
+ onPreviewTap?: (event: {
55
+ x: number;
56
+ y: number;
57
+ }) => void;
58
+ }
59
+ /**
60
+ * A forwardRef'd wrapper that exposes the underlying Camera ref
61
+ * to callers (so ``cameraRef.current.takePhoto()`` keeps working),
62
+ * while presenting a smaller API on the outside.
63
+ */
64
+ export declare const CameraView: React.ForwardRefExoticComponent<CameraViewProps & React.RefAttributes<Camera | null>>;
65
+ //# sourceMappingURL=CameraView.d.ts.map
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * CameraView — the SDK's drop-in replacement for the raw
5
+ * vision-camera ``<Camera />``.
6
+ *
7
+ * Why wrap it?
8
+ * 1. **Default props** — always ``isActive={true}``, ``photo={true}``,
9
+ * and honouring the hook's flash state. Every call-site in the
10
+ * mobile app repeated the same tuple; the SDK canonicalises it.
11
+ * 2. **Branded guidance overlay** — optional ``guidance`` prop renders
12
+ * a themed banner over the preview without the host app having to
13
+ * know about positioning / contrast.
14
+ * 3. **Forward ref** — so ``useCapture``'s ref attaches cleanly.
15
+ *
16
+ * The component is intentionally thin — anything more elaborate goes
17
+ * into a separate screen (e.g. AuditCaptureSurface that combines this
18
+ * view with thumbnails and a shutter button). Keeping CameraView at
19
+ * the vision-camera layer means host apps that want a highly-custom
20
+ * UI can still use it as their building block.
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.CameraView = void 0;
57
+ const react_1 = __importStar(require("react"));
58
+ const react_native_1 = require("react-native");
59
+ const react_native_vision_camera_1 = require("react-native-vision-camera");
60
+ /**
61
+ * A forwardRef'd wrapper that exposes the underlying Camera ref
62
+ * to callers (so ``cameraRef.current.takePhoto()`` keeps working),
63
+ * while presenting a smaller API on the outside.
64
+ */
65
+ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', isActive = true, video = false, guidance, style, cameraProps, }, ref) {
66
+ // Internal ref so we can both attach to <Camera> and forward outward.
67
+ const innerRef = (0, react_1.useRef)(null);
68
+ (0, react_1.useImperativeHandle)(ref, () => innerRef.current);
69
+ if (!device) {
70
+ return (react_1.default.createElement(react_native_1.View, { style: [styles.placeholder, style], accessibilityLabel: "Camera initialising" },
71
+ react_1.default.createElement(react_native_1.Text, { style: styles.placeholderText }, "Initialising camera\u2026")));
72
+ }
73
+ return (react_1.default.createElement(react_native_1.View, { style: [styles.root, style] },
74
+ react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive, photo: true, video: video,
75
+ // Bake the device orientation into the captured pixels.
76
+ // Without this, vision-camera writes the file in the camera
77
+ // sensor's native landscape and relies on EXIF metadata to
78
+ // tell viewers "rotate me" — but RN's <Image> on iOS often
79
+ // ignores EXIF, leading to thumbnails / previews appearing
80
+ // sideways even though the user shot in portrait. Setting
81
+ // `outputOrientation="device"` rotates the pixels to match
82
+ // how the user is holding the phone, so the saved JPEG is
83
+ // "what you see is what was taken".
84
+ outputOrientation: "device", torch: flash === 'on' ? 'on' : 'off', ...cameraProps }),
85
+ guidance ? (react_1.default.createElement(react_native_1.View, { style: styles.guidance, pointerEvents: "none", accessible: true, accessibilityRole: "text" },
86
+ react_1.default.createElement(react_native_1.Text, { style: styles.guidanceText, numberOfLines: 2 }, guidance))) : null));
87
+ });
88
+ const styles = react_native_1.StyleSheet.create({
89
+ root: {
90
+ flex: 1,
91
+ overflow: 'hidden',
92
+ },
93
+ placeholder: {
94
+ flex: 1,
95
+ alignItems: 'center',
96
+ justifyContent: 'center',
97
+ backgroundColor: '#000',
98
+ },
99
+ placeholderText: {
100
+ color: '#ffffff',
101
+ fontSize: 14,
102
+ },
103
+ guidance: {
104
+ position: 'absolute',
105
+ top: 0,
106
+ left: 0,
107
+ right: 0,
108
+ paddingHorizontal: 16,
109
+ paddingVertical: 10,
110
+ backgroundColor: 'rgba(0, 0, 0, 0.55)',
111
+ },
112
+ guidanceText: {
113
+ color: '#ffffff',
114
+ fontSize: 13,
115
+ },
116
+ });
117
+ //# sourceMappingURL=CameraView.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * CaptureControlsBar — bottom-of-screen controls for any capture
3
+ * surface.
4
+ *
5
+ * ┌──────────────────────────────────────────────────────────┐
6
+ * │ [⚡ flash] [● shutter] [ host slot ] │
7
+ * └──────────────────────────────────────────────────────────┘
8
+ *
9
+ * The SDK owns the flash button and the shutter button (which is
10
+ * `<CameraShutter>` under the hood, so tap-vs-hold gesture handling
11
+ * comes "for free" in any host that uses this). The right-side
12
+ * action is a render-prop — host apps put a "Submit", "Done",
13
+ * "Save", "Next" button there as fits their flow.
14
+ *
15
+ * Why a slot for the right-side action?
16
+ * The flash and shutter buttons are universally camera-shaped;
17
+ * every host wants them with the same gesture, the same colors,
18
+ * the same accessibility labels. But the third action varies
19
+ * wildly — submitting an audit, saving a single photo, advancing
20
+ * a wizard step. Slotting keeps the SDK from prescribing host
21
+ * semantics.
22
+ */
23
+ import React from 'react';
24
+ import { type StyleProp, type ViewStyle } from 'react-native';
25
+ export interface CaptureControlsBarProps {
26
+ /** Current flash mode — drives the flash icon's colour. */
27
+ flashMode: 'off' | 'on';
28
+ /** Called when the flash button is pressed. */
29
+ onToggleFlash: () => void;
30
+ /**
31
+ * 2026-05-16 — disable the flash button. Pass `true` when the
32
+ * active camera surface doesn't honour torch state — currently the
33
+ * AR camera (ARKit / ARCore own AVCaptureDevice and don't expose
34
+ * torch control through the JS bridge; toggling would silently
35
+ * no-op). Renders the button at reduced opacity + ignores presses.
36
+ */
37
+ flashDisabled?: boolean;
38
+ /** Tap → take photo. */
39
+ onShutterTap: () => void;
40
+ /** Hold crosses threshold → start video recording. */
41
+ onShutterHoldStart: () => void;
42
+ /** Release after hold → stop recording, stitch. */
43
+ onShutterHoldComplete: () => void;
44
+ /**
45
+ * Disable the shutter (e.g. at-max-photos for the audit, no
46
+ * camera permission, etc). Flash and the right-side action
47
+ * remain interactive.
48
+ */
49
+ shutterDisabled?: boolean;
50
+ /**
51
+ * Show the shutter's "processing" visual. Use this while a
52
+ * stitch is in progress so the operator can't kick off a second
53
+ * recording mid-stitch.
54
+ */
55
+ shutterProcessing?: boolean;
56
+ /**
57
+ * Forwards to <CameraShutter maxHoldMs>. Auto-fires
58
+ * onShutterHoldComplete when the timer elapses, simulating the
59
+ * user releasing. Pair with <CaptureStatusOverlay countdownMs>
60
+ * so the user sees how long they have left.
61
+ */
62
+ shutterMaxHoldMs?: number;
63
+ /**
64
+ * Render-prop slot for the host's right-side action. Typically a
65
+ * Pressable wrapping a "Submit" / "Done" button, but anything is
66
+ * fair game. Receives no arguments — wire your callbacks in the
67
+ * usual way.
68
+ *
69
+ * Pass `null` to render an empty spacer instead (keeps the shutter
70
+ * centred when there's no action to show).
71
+ */
72
+ rightAction?: React.ReactNode;
73
+ /** Override the default colours. */
74
+ colors?: {
75
+ background?: string;
76
+ iconButton?: string;
77
+ iconActive?: string;
78
+ icon?: string;
79
+ iconAccessible?: string;
80
+ };
81
+ /** Bottom inset for safe-area on devices with a home indicator. */
82
+ bottomInset?: number;
83
+ /** Outer style passthrough. */
84
+ style?: StyleProp<ViewStyle>;
85
+ }
86
+ export declare function CaptureControlsBar({ flashMode, onToggleFlash, flashDisabled, onShutterTap, onShutterHoldStart, onShutterHoldComplete, shutterDisabled, shutterProcessing, shutterMaxHoldMs, rightAction, colors, bottomInset, style, }: CaptureControlsBarProps): React.JSX.Element;
87
+ //# sourceMappingURL=CaptureControlsBar.d.ts.map
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * CaptureControlsBar — bottom-of-screen controls for any capture
5
+ * surface.
6
+ *
7
+ * ┌──────────────────────────────────────────────────────────┐
8
+ * │ [⚡ flash] [● shutter] [ host slot ] │
9
+ * └──────────────────────────────────────────────────────────┘
10
+ *
11
+ * The SDK owns the flash button and the shutter button (which is
12
+ * `<CameraShutter>` under the hood, so tap-vs-hold gesture handling
13
+ * comes "for free" in any host that uses this). The right-side
14
+ * action is a render-prop — host apps put a "Submit", "Done",
15
+ * "Save", "Next" button there as fits their flow.
16
+ *
17
+ * Why a slot for the right-side action?
18
+ * The flash and shutter buttons are universally camera-shaped;
19
+ * every host wants them with the same gesture, the same colors,
20
+ * the same accessibility labels. But the third action varies
21
+ * wildly — submitting an audit, saving a single photo, advancing
22
+ * a wizard step. Slotting keeps the SDK from prescribing host
23
+ * semantics.
24
+ */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.CaptureControlsBar = CaptureControlsBar;
30
+ const react_1 = __importDefault(require("react"));
31
+ const react_native_1 = require("react-native");
32
+ const CameraShutter_1 = require("./CameraShutter");
33
+ function CaptureControlsBar({ flashMode, onToggleFlash, flashDisabled = false, onShutterTap, onShutterHoldStart, onShutterHoldComplete, shutterDisabled = false, shutterProcessing = false, shutterMaxHoldMs, rightAction = null, colors, bottomInset = 0, style, }) {
34
+ const bg = colors?.background ?? '#000000';
35
+ const iconButtonBg = colors?.iconButton ?? 'rgba(255,255,255,0.12)';
36
+ const iconActiveBg = colors?.iconActive ?? '#FF9F0A';
37
+ const iconColor = colors?.icon ?? '#ffffff';
38
+ return (react_1.default.createElement(react_native_1.View, { style: [
39
+ styles.bar,
40
+ { backgroundColor: bg, paddingBottom: bottomInset + 16 },
41
+ style,
42
+ ] },
43
+ react_1.default.createElement(react_native_1.Pressable, { onPress: flashDisabled ? undefined : onToggleFlash, accessibilityRole: "button", accessibilityLabel: flashDisabled
44
+ ? 'Flash unavailable in AR mode'
45
+ : `Flash ${flashMode === 'on' ? 'on' : 'off'}`, accessibilityState: {
46
+ selected: flashMode === 'on',
47
+ disabled: flashDisabled,
48
+ }, disabled: flashDisabled, style: [
49
+ styles.iconButton,
50
+ { backgroundColor: flashMode === 'on' ? iconActiveBg : iconButtonBg },
51
+ flashDisabled ? { opacity: 0.35 } : null,
52
+ ], hitSlop: 8 },
53
+ react_1.default.createElement(react_native_1.Text, { style: [styles.icon, { color: iconColor }] }, "\u26A1")),
54
+ react_1.default.createElement(CameraShutter_1.CameraShutter, { onTap: onShutterTap, onHoldStart: onShutterHoldStart, onHoldComplete: onShutterHoldComplete, maxHoldMs: shutterMaxHoldMs, disabled: shutterDisabled, isProcessing: shutterProcessing }),
55
+ react_1.default.createElement(react_native_1.View, { style: styles.rightSlot }, rightAction)));
56
+ }
57
+ const styles = react_native_1.StyleSheet.create({
58
+ bar: {
59
+ flexDirection: 'row',
60
+ alignItems: 'center',
61
+ justifyContent: 'space-between',
62
+ paddingHorizontal: 24,
63
+ paddingTop: 16,
64
+ },
65
+ iconButton: {
66
+ width: 48,
67
+ height: 48,
68
+ borderRadius: 24,
69
+ alignItems: 'center',
70
+ justifyContent: 'center',
71
+ },
72
+ icon: {
73
+ fontSize: 22,
74
+ },
75
+ rightSlot: {
76
+ width: 48,
77
+ height: 48,
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ },
81
+ });
82
+ //# sourceMappingURL=CaptureControlsBar.js.map