react-native-image-stitcher 0.14.0 → 0.14.1

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 (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +211 -51
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -16,6 +16,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.14.1] — 2026-06-01
20
+
21
+ ### Docs
22
+
23
+ - Refresh the npm README for the v0.14 API: full `<Camera>` prop
24
+ reference (incl. `captureSources`), a complete capture-screen sample,
25
+ the portrait recommendation, and a 0.13.x → 0.14 migration note. (The
26
+ 0.14.0 tarball shipped before this refresh landed; no code change.)
27
+ - Add a Docusaurus docs site (published to GitHub Pages).
28
+
19
29
  ## [0.14.0] — 2026-06-01
20
30
 
21
31
  ### Fixed — Android AR single-photo orientation (landscape was sideways)
package/README.md CHANGED
@@ -8,10 +8,10 @@ AR-backed and IMU-fallback capture paths.
8
8
 
9
9
  | Feature | Behaviour |
10
10
  |---|---|
11
- | **Tap shutter** | Single photo via vision-camera's `takePhoto` (non-AR) or `ARFrame.capturedImage` (AR). |
11
+ | **Tap shutter** | Single photo via vision-camera's `takePhoto` (non-AR) or ARCore/ARKit `capturedImage` (AR). |
12
12
  | **Hold shutter** | Panorama capture — pan and release. Engine accumulates keyframes; stitches via `cv::Stitcher::PANORAMA` (or `SCANS` if the pose suggests a flat-translation scan). |
13
- | **Lens chip** | 1× / 0.5× toggle above the shutter. Selecting 0.5× forces non-AR (AR sessions can't switch physical lenses mid-session). |
14
- | **AR toggle** | Bottom-right corner, conditional on the lens. Toggles between AR-pose-driven and IMU-driven capture paths. |
13
+ | **Lens chip** | 1× / 0.5× toggle next to the shutter. Shown only when the device actually has a usable ultra-wide (real capability detection, v0.14). Hidden entirely in AR-only mode (`captureSources="ar"`). |
14
+ | **Flash & AR pills** | Top-right pill stack, under the settings gear. Flash toggles the torch (hidden on torchless lenses, e.g. a standalone ultra-wide). AR pill toggles AR ↔ non-AR — shown only when `captureSources="both"` and the device supports AR. |
15
15
  | **Internal settings panel** | Opt-in gear icon (top-right) via `showSettingsButton` prop. Exposes blender, seam finder, warper, flow-gate tunables — useful for internal testers; hidden from public consumers by default. |
16
16
 
17
17
  ## Installation
@@ -65,6 +65,15 @@ cd android && ./gradlew :app:assembleDebug # Android
65
65
 
66
66
  ## Quick start
67
67
 
68
+ > **Orientation: use portrait.** `<Camera>` is designed and tuned for
69
+ > portrait capture. On Android it self-locks to portrait; on iOS,
70
+ > portrait-only is the recommended host `Info.plist` configuration.
71
+ > See [Orientation support](#orientation-support) for the full story
72
+ > (landscape *is* supported on iOS if you need it).
73
+
74
+ The minimum: resolve camera permission, then mount `<Camera>` with an
75
+ `onCapture` handler.
76
+
68
77
  ```tsx
69
78
  import {
70
79
  Camera,
@@ -81,68 +90,213 @@ export function CaptureScreen() {
81
90
  'Panorama:',
82
91
  result.uri,
83
92
  `${result.framesIncluded}/${result.framesRequested} frames`,
93
+ `stitched as ${result.stitchModeResolved ?? 'n/a'}`,
84
94
  );
85
95
  }
86
96
  };
87
97
 
88
- const handleError = (err: CameraError) => {
89
- console.warn(err.code, err.message);
90
- };
91
-
92
98
  return (
93
99
  <Camera
94
- defaultCaptureSource="ar"
95
- defaultLens="1x"
96
- enablePhotoMode
97
- enablePanoramaMode
98
100
  onCapture={handleCapture}
99
- onError={handleError}
101
+ onError={(err: CameraError) => console.warn(err.code, err.message)}
100
102
  />
101
103
  );
102
104
  }
103
105
  ```
104
106
 
105
- ## `<Camera>` props (summary)
107
+ ### A complete capture screen
106
108
 
107
- > **Full reference:** [`docs/camera-component.md`](docs/camera-component.md)
108
- > covers every prop with purpose, default, behaviour notes, the
109
- > `CameraCaptureResult` / `CameraError` shapes, orientation
110
- > behaviour, and common compositions. This README summary lists
111
- > the highlights only.
109
+ A realistic screen: requests permission up front, shows a capture
110
+ history strip, opens a post-stitch preview modal, and persists the
111
+ output to a directory you control. (The SDK does **not** request camera
112
+ permission for you the host owns that.)
112
113
 
113
- ### Initial values (uncontrolled — read once at mount)
114
+ ```tsx
115
+ import React, { useCallback, useEffect, useState } from 'react';
116
+ import { View, StyleSheet } from 'react-native';
117
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
118
+ import { useCameraPermission } from 'react-native-vision-camera';
119
+ import {
120
+ Camera,
121
+ type CameraCaptureResult,
122
+ type CameraError,
123
+ type CaptureThumbnailItem,
124
+ } from 'react-native-image-stitcher';
114
125
 
115
- | Prop | Default | Notes |
116
- |---|---|---|
117
- | `defaultCaptureSource` | `'ar'` | `'ar'` `'non-ar'` |
118
- | `defaultLens` | `'1x'` | `'1x'` `'0.5x'` |
119
- | `defaultStitchMode` | `'auto'` | `'auto'`, `'panorama'`, `'scans'` |
120
- | `defaultBlender` | `'multiband'` | `'multiband'`, `'feather'` |
121
- | `defaultSeamFinder` | `'graphcut'` | `'graphcut'`, `'skip'` |
122
- | `defaultWarper` | `'plane'` | `'plane'`, `'cylindrical'`, `'spherical'` |
123
- | `defaultFlowNoveltyPercentile` | `0.85` | Range 0.50 – 0.99 |
124
- | `defaultFlowEvalEveryNFrames` | `5` | Range 1 10 |
125
- | `defaultFlowMaxTranslationCm` | `50` | 0 = disabled |
126
- | `defaultKeyframeMaxCount` | `6` | Range 3 – 10 |
127
- | `defaultKeyframeOverlapThreshold` | `0.20` | Range 0.20 0.60 |
126
+ export function CaptureScreen() {
127
+ // 1. Camera permission is a HOST concern — resolve it BEFORE mounting
128
+ // <Camera>. (Android treats unrequested permissions as denied even
129
+ // when declared in the manifest, so the explicit call is required.)
130
+ const { hasPermission, requestPermission } = useCameraPermission();
131
+ useEffect(() => {
132
+ if (!hasPermission) requestPermission().catch(() => undefined);
133
+ }, [hasPermission, requestPermission]);
134
+
135
+ // 2. Capture history (drives the built-in thumbnail strip).
136
+ const [thumbnails, setThumbnails] = useState<CaptureThumbnailItem[]>([]);
137
+
138
+ // 3. Post-stitch preview modal set on capture, cleared on close.
139
+ const [preview, setPreview] = useState<CameraCaptureResult | null>(null);
140
+
141
+ const onCapture = useCallback((result: CameraCaptureResult) => {
142
+ setPreview(result);
143
+ setThumbnails((prev) => [
144
+ ...prev,
145
+ { id: String(Date.now()), uri: result.uri, width: result.width, height: result.height },
146
+ ]);
147
+ }, []);
148
+
149
+ if (!hasPermission) return <View style={styles.fill} />; // or your own "grant access" UI
150
+
151
+ return (
152
+ <SafeAreaProvider>
153
+ <View style={styles.fill}>
154
+ <Camera
155
+ // Capture-mode controls
156
+ defaultCaptureSource="ar" // start in AR mode (pose-driven)
157
+ captureSources="both" // allow AR + non-AR; show the AR toggle
158
+ enablePhotoMode // tap = photo
159
+ enablePanoramaMode // hold + pan = panorama
160
+ // Output
161
+ outputDir={`${/* your app dir */ ''}/captures`}
162
+ // Header chrome (optional)
163
+ headerTitle="Capture"
164
+ headerGuidance="Tap for a photo. Hold + pan + release for a panorama."
165
+ // Capture history strip
166
+ thumbnails={thumbnails}
167
+ // Post-stitch preview modal (controlled — clear it on close)
168
+ capturePreview={preview ? { imageUri: preview.uri } : undefined}
169
+ onCapturePreviewClose={() => setPreview(null)}
170
+ // Events
171
+ onCapture={onCapture}
172
+ onError={(err: CameraError) => console.warn(err.code, err.message)}
173
+ onCaptureAbandoned={(reason) => console.log('abandoned:', reason)}
174
+ />
175
+ </View>
176
+ </SafeAreaProvider>
177
+ );
178
+ }
179
+
180
+ const styles = StyleSheet.create({ fill: { flex: 1, backgroundColor: '#000' } });
181
+ ```
182
+
183
+ ## `<Camera>` props (full reference)
184
+
185
+ Every prop is optional. `<Camera>` works with no props at all (it just
186
+ captures and you wire `onCapture`). Props fall into seven groups.
187
+
188
+ > A deeper companion reference with composition recipes lives in
189
+ > [`docs/camera-component.md`](docs/camera-component.md). The tables
190
+ > below are the authoritative prop list.
191
+
192
+ ### Capture-source & lens (uncontrolled — read once at mount)
193
+
194
+ | Prop | Type | Default | Notes |
195
+ |---|---|---|---|
196
+ | `defaultCaptureSource` | `'ar' \| 'non-ar'` | `'ar'` | Initial capture path. Clamped to `captureSources` (below). |
197
+ | `captureSources` | `'ar' \| 'non-ar' \| 'both'` | `'both'` | **(v0.14)** Which sources are allowed. `'both'` shows the AR toggle. `'ar'` hides the AR toggle **and** the lens chooser (ARKit/ARCore can't use the ultra-wide). `'non-ar'` hides the AR toggle, keeps the lens chooser. A single-source value overrides a conflicting `defaultCaptureSource`. |
198
+ | `defaultLens` | `'1x' \| '0.5x'` | `'1x'` | Initial lens. The 0.5× chooser only appears if the device actually has a usable ultra-wide (real capability detection, v0.14). |
199
+
200
+ ### Panorama / stitcher tunables (uncontrolled — internal-tester knobs)
201
+
202
+ These mirror the in-app settings panel; most apps never set them.
203
+
204
+ | Prop | Type | Default | Notes |
205
+ |---|---|---|---|
206
+ | `defaultStitchMode` | `'auto' \| 'panorama' \| 'scans'` | `'auto'` | `'auto'` picks PANORAMA vs SCANS from the pose at finalize. |
207
+ | `defaultBlender` | `'multiband' \| 'feather'` | `'multiband'` | cv::Stitcher blender. |
208
+ | `defaultSeamFinder` | `'graphcut' \| 'skip'` | `'graphcut'` | Seam finder. |
209
+ | `defaultWarper` | `'plane' \| 'cylindrical' \| 'spherical'` | `'plane'` | Projection surface. |
210
+ | `defaultFlowNoveltyPercentile` | `number` | `0.85` | Keyframe-gate novelty threshold (0.50–0.99). |
211
+ | `defaultFlowEvalEveryNFrames` | `number` | `5` | Flow-gate eval cadence (1–10). |
212
+ | `defaultFlowMaxTranslationCm` | `number` | `50` | Max IMU translation between keyframes; 0 = disabled. |
213
+ | `defaultKeyframeMaxCount` | `number` | `6` | Keyframe cap per capture (3–10). |
214
+ | `defaultKeyframeOverlapThreshold` | `number` | `0.20` | Min overlap to accept a keyframe (0.20–0.60). |
215
+ | `defaultCompositingResolMP` / `defaultRegistrationResolMP` / `defaultSeamEstimationResolMP` | `number` | — | Forward-looking cv::Stitcher resolution knobs (currently no-ops). |
128
216
 
129
217
  ### UI toggles
130
218
 
131
- | Prop | Default | Notes |
219
+ | Prop | Type | Default | Notes |
220
+ |---|---|---|---|
221
+ | `enablePhotoMode` | `boolean` | `true` | Tap = photo. When false, tap is a no-op. |
222
+ | `enablePanoramaMode` | `boolean` | `true` | Hold + pan = panorama. When false, hold is a no-op. |
223
+ | `showSettingsButton` | `boolean` | `false` | Gear icon → internal settings panel. Internal-tester only; leave off for public consumers. |
224
+ | `style` | `StyleProp<ViewStyle>` | — | Outer container style. |
225
+
226
+ ### Flash (controlled or uncontrolled)
227
+
228
+ | Prop | Type | Default | Notes |
229
+ |---|---|---|---|
230
+ | `flash` | `'on' \| 'off'` | — | Controlled torch state. Omit to let `<Camera>` own it internally. |
231
+ | `onFlashChange` | `(next: 'on' \| 'off') => void` | — | Fires on flash-button tap (controlled and uncontrolled). |
232
+ | `showFlashButton` | `boolean` | `true` | Built-in flash pill (top-right). Auto-hidden when the mounted device has no torch (e.g. a standalone ultra-wide) and in AR mode. |
233
+
234
+ ### Header chrome (opt-in)
235
+
236
+ Setting `headerTitle` renders a built-in top header; the settings gear is absorbed into it.
237
+
238
+ | Prop | Type | Notes |
132
239
  |---|---|---|
133
- | `enablePhotoMode` | `true` | Tap-to-photo |
134
- | `enablePanoramaMode` | `true` | Hold-to-pan |
135
- | `showSettingsButton` | `false` | Internal-tester only; OFF for public consumers |
240
+ | `headerTitle` | `string` | Shows the header when set. |
241
+ | `headerGuidance` | `string` | Subtitle / guidance pill under the title. |
242
+ | `onHeaderBack` | `() => void` | Renders a back affordance when provided. |
243
+ | `headerBackLabel` | `string` | Custom back-button label. |
244
+ | `headerColors` | `object` | Override header colours. |
136
245
 
137
- ### Callbacks
246
+ ### Capture history + post-stitch preview
138
247
 
139
- | Prop | Fires when |
140
- |---|---|
141
- | `onCapture(result)` | Photo OR panorama capture completes successfully. `result.type` discriminates. |
142
- | `onCaptureSourceChange(source)` | Effective capture source changes (e.g., user toggles AR, or selecting 0. forces non-AR). |
143
- | `onLensChange(lens)` | User taps the 1×/0.5× chip. |
144
- | `onFramesDropped(info)` | cv::Stitcher's confidence retry loop dropped one or more input frames. |
145
- | `onError(err)` | Classified error. `err.code` from a known taxonomy (`STITCH_NEED_MORE_IMGS`, `STITCH_HOMOGRAPHY_FAIL`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_OOM`, `CAMERA_PERMISSION_DENIED`, etc.). |
248
+ | Prop | Type | Notes |
249
+ |---|---|---|
250
+ | `thumbnails` | `CaptureThumbnailItem[]` | When supplied (even `[]`), renders the built-in thumbnail strip. Hidden during recording. |
251
+ | `thumbnailsMin` / `thumbnailsMax` | `number` | Optional count-line hints (e.g. quota guidance). |
252
+ | `onThumbnailPress` | `(item) => void` | Replaces the strip's built-in tap-to-preview with your handler. |
253
+ | `capturePreview` | `{ imageUri; imageWidth?; imageHeight?; title? }` | When set, renders the built-in preview modal. Controlled — clear it via `onCapturePreviewClose`. |
254
+ | `capturePreviewActions` | `CapturePreviewAction[]` | Action buttons for the preview modal (e.g. Save / Retake). |
255
+ | `onCapturePreviewClose` | `() => void` | Fires when the preview modal is dismissed. |
256
+
257
+ ### Callbacks & advanced
258
+
259
+ | Prop | Type | Fires / purpose |
260
+ |---|---|---|
261
+ | `onCapture` | `(result: CameraCaptureResult) => void` | Photo OR panorama completes. `result.type` discriminates (`'photo'` / `'panorama'`). |
262
+ | `onCaptureSourceChange` | `(source: CaptureSource) => void` | Effective source changes (AR toggle, or 0.5× forcing non-AR). |
263
+ | `onLensChange` | `(lens: CameraLens) => void` | User taps the 1×/0.5× chip. |
264
+ | `onFramesDropped` | `(info: FramesDroppedInfo) => void` | cv::Stitcher's confidence retry dropped input frame(s). |
265
+ | `onCaptureAbandoned` | `(reason: 'orientation-drift') => void` | SDK auto-cancelled an in-flight capture (currently only mid-capture rotation). |
266
+ | `onError` | `(err: CameraError) => void` | Classified error — see codes below. |
267
+ | `outputDir` | `string` | Directory for saved JPEGs. The lib creates it if missing. |
268
+ | `engine` | `'batch-keyframe' \| …` | Stitching engine. Default `'batch-keyframe'`; most apps leave it. |
269
+ | `frameProcessor` | vision-camera frame processor | Host worklet composed with first-party stitching (see [`useStitcherWorklet`](docs/camera-component.md)). Advanced. |
270
+
271
+ ### `CameraCaptureResult`
272
+
273
+ ```ts
274
+ type CameraCaptureResult =
275
+ | { type: 'photo'; uri: string; width: number; height: number }
276
+ | { type: 'panorama'; uri: string; width: number; height: number;
277
+ framesRequested: number; framesIncluded: number; framesDropped: number;
278
+ finalConfidenceThresh: number; durationMs: number;
279
+ stitchModeResolved?: 'panorama' | 'scans' };
280
+ ```
281
+
282
+ ### `CameraError` codes
283
+
284
+ `err.code` is one of a fixed taxonomy so you can branch (toast vs retry vs report):
285
+ `CAMERA_PERMISSION_DENIED`, `CAMERA_DEVICE_UNAVAILABLE`, `PHOTO_CAPTURE_FAILED`,
286
+ `PANORAMA_START_FAILED`, `PANORAMA_FINALIZE_FAILED`, `STITCH_NEED_MORE_IMGS`,
287
+ `STITCH_HOMOGRAPHY_FAIL`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_OOM`,
288
+ `OUTPUT_WRITE_FAILED`, plus `VISION_CAMERA_RUNTIME`.
289
+
290
+ ### Migration from 0.13.x
291
+
292
+ - **Removed:** the `panGuide` and `panoramaGuidance` props (the
293
+ drift-marker overlay + pan-speed pill). They are no longer part of the
294
+ public API and `<Camera>` no longer renders them. Remove these props
295
+ if you were passing them — they're now a no-op type error.
296
+ - **Added:** `captureSources` (above).
297
+ - **Behaviour:** flash + AR controls moved to a top-right pill stack; the
298
+ 0.5× chooser now reflects real device capability; Android self-locks to
299
+ portrait. No code change required for any of these.
146
300
 
147
301
  ## Orientation support
148
302
 
@@ -186,14 +340,20 @@ the modal alone.
186
340
 
187
341
  ## Lens ↔ AR interaction
188
342
 
189
- | Action | `arPreference` | `lens` | UI |
190
- |---|---|---|---|
191
- | Initial mount with defaults | `true` | `1x` | AR toggle ON |
192
- | User switches to 0.5× | unchanged (`true`) | `0.5x` | AR toggle HIDDEN, forced non-AR |
193
- | User switches back to 1× | unchanged (`true`) | `1x` | AR toggle visible at its previous state |
194
- | User taps AR toggle off (on 1×) | `false` | `1x` | AR toggle OFF |
343
+ The lens chooser and AR toggle interact, because ARKit/ARCore sessions
344
+ can't switch to the ultra-wide. With `captureSources="both"` (default):
195
345
 
196
- The component owns the runtime state; the parent persists across launches via the `on*Change` callbacks if desired.
346
+ | Action | AR preference | Lens | UI |
347
+ |---|---|---|---|
348
+ | Initial mount (defaults) | on | `1×` | AR pill ON |
349
+ | Switch to 0.5× | unchanged | `0.5×` | AR pill HIDDEN; capture forced non-AR |
350
+ | Switch back to 1× | unchanged | `1×` | AR pill visible at its previous state |
351
+ | Tap AR pill off (on 1×) | off | `1×` | AR pill OFF |
352
+
353
+ When `captureSources` is `'ar'` or `'non-ar'`, the AR pill never shows
354
+ (nothing to toggle), and `'ar'` additionally hides the lens chooser. The
355
+ component owns this runtime state; persist across launches via the
356
+ `on*Change` callbacks if desired.
197
357
 
198
358
  ## Architecture notes
199
359
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-image-stitcher",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
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",