react-native-image-stitcher 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +181 -0
- package/README.md +33 -17
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +33 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +73 -1
- package/dist/camera/Camera.d.ts +226 -0
- package/dist/camera/Camera.js +208 -20
- package/dist/camera/CameraView.d.ts +6 -0
- package/dist/camera/CameraView.js +2 -2
- package/dist/camera/CaptureHeader.js +39 -16
- package/dist/camera/CapturePreview.js +13 -1
- package/dist/camera/CaptureThumbnailStrip.d.ts +25 -1
- package/dist/camera/CaptureThumbnailStrip.js +17 -4
- package/dist/camera/PanoramaBandOverlay.d.ts +76 -0
- package/dist/camera/PanoramaBandOverlay.js +90 -33
- package/dist/camera/PanoramaConfirmModal.js +11 -1
- package/dist/camera/selectCaptureDevice.d.ts +93 -0
- package/dist/camera/selectCaptureDevice.js +131 -0
- package/dist/camera/useCapture.d.ts +40 -0
- package/dist/camera/useCapture.js +50 -12
- package/dist/camera/useContentRotation.d.ts +99 -0
- package/dist/camera/useContentRotation.js +124 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +6 -5
- package/package.json +1 -1
- package/src/camera/Camera.tsx +546 -32
- package/src/camera/CameraView.tsx +9 -0
- package/src/camera/CaptureHeader.tsx +39 -16
- package/src/camera/CapturePreview.tsx +12 -0
- package/src/camera/CaptureThumbnailStrip.tsx +44 -4
- package/src/camera/PanoramaBandOverlay.tsx +97 -35
- package/src/camera/PanoramaConfirmModal.tsx +10 -0
- package/src/camera/__tests__/bandThumbRotation.test.ts +120 -0
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +116 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +177 -0
- package/src/camera/__tests__/useContentRotation.test.ts +89 -0
- package/src/camera/selectCaptureDevice.ts +187 -0
- package/src/camera/useCapture.ts +99 -11
- package/src/camera/useContentRotation.ts +149 -0
- package/src/index.ts +6 -2
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* selectCaptureDevice — capability-aware back-camera selection.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the single-physical-device request that caused two
|
|
7
|
+
* user-visible bugs (see docs/plans/2026-06-01-v0.13.2-multilens-
|
|
8
|
+
* device-selection.md):
|
|
9
|
+
*
|
|
10
|
+
* 1. 0.5× silently showed the wide-angle FOV on phones where the
|
|
11
|
+
* ultra-wide is only exposed inside a multi-cam logical device —
|
|
12
|
+
* vision-camera's single-lens filter mis-scored and fell back to
|
|
13
|
+
* a plain wide-angle device.
|
|
14
|
+
* 2. flash threw `flash-not-available` on 0.5× because the standalone
|
|
15
|
+
* ultra-wide device has no torch unit.
|
|
16
|
+
*
|
|
17
|
+
* Both stem from mounting ONE standalone physical device per lens. The
|
|
18
|
+
* fix: prefer a MULTI-CAM device that carries the ultra-wide (so a
|
|
19
|
+
* single mounted device spans both FOVs via zoom AND carries the torch
|
|
20
|
+
* through its wide-angle member). Fall back to standalone devices for
|
|
21
|
+
* phones — common on Android — where the ultra-wide has no multi-cam
|
|
22
|
+
* grouping, so we don't regress those.
|
|
23
|
+
*
|
|
24
|
+
* Pure + synchronous: takes a plain device list (the structural subset
|
|
25
|
+
* of vision-camera's `CameraDevice` we need) and returns the choice.
|
|
26
|
+
* No React, no vision-camera hooks — unit-tested directly.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.selectCaptureDevice = selectCaptureDevice;
|
|
30
|
+
exports.zoomForLens = zoomForLens;
|
|
31
|
+
const hasLens = (d, lens) => d.physicalDevices.includes(lens);
|
|
32
|
+
/**
|
|
33
|
+
* Choose the back-camera device(s) for capture.
|
|
34
|
+
*
|
|
35
|
+
* Priority:
|
|
36
|
+
* 1. multicam — a multi-cam device containing BOTH wide + ultra-wide
|
|
37
|
+
* (best: one device, zoom-switch, torch via the wide member).
|
|
38
|
+
* 2. standalone-uw — a standalone wide AND a standalone ultra-wide
|
|
39
|
+
* exist as separate devices (device-swap on lens change; flash
|
|
40
|
+
* hidden on the torchless ultra-wide).
|
|
41
|
+
* 3. wide-only — no ultra-wide reachable; wide-angle only.
|
|
42
|
+
*
|
|
43
|
+
* @param devices All enumerated camera devices (any position).
|
|
44
|
+
*/
|
|
45
|
+
function selectCaptureDevice(devices) {
|
|
46
|
+
const back = devices.filter((d) => d.position === 'back');
|
|
47
|
+
if (back.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
device: null,
|
|
50
|
+
ultraWideDevice: null,
|
|
51
|
+
mode: 'wide-only',
|
|
52
|
+
has0_5x: false,
|
|
53
|
+
hasTorch: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ── 1. Prefer a multi-cam device that carries BOTH wide + ultra-wide.
|
|
57
|
+
// Among candidates, prefer the one that ALSO has a torch (so flash
|
|
58
|
+
// works on every lens), then the one spanning the widest zoom range
|
|
59
|
+
// (more lenses → more reach), as a stable tiebreak.
|
|
60
|
+
const multicamCandidates = back.filter((d) => d.isMultiCam &&
|
|
61
|
+
hasLens(d, 'wide-angle-camera') &&
|
|
62
|
+
hasLens(d, 'ultra-wide-angle-camera'));
|
|
63
|
+
if (multicamCandidates.length > 0) {
|
|
64
|
+
const device = multicamCandidates.reduce((best, d) => {
|
|
65
|
+
// torch-bearing wins; then wider zoom span; then more lenses.
|
|
66
|
+
if (d.hasTorch !== best.hasTorch)
|
|
67
|
+
return d.hasTorch ? d : best;
|
|
68
|
+
const span = d.maxZoom - d.minZoom;
|
|
69
|
+
const bestSpan = best.maxZoom - best.minZoom;
|
|
70
|
+
if (span !== bestSpan)
|
|
71
|
+
return span > bestSpan ? d : best;
|
|
72
|
+
return d.physicalDevices.length > best.physicalDevices.length ? d : best;
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
device,
|
|
76
|
+
ultraWideDevice: null,
|
|
77
|
+
mode: 'multicam',
|
|
78
|
+
has0_5x: true,
|
|
79
|
+
hasTorch: device.hasTorch,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ── 2. Standalone ultra-wide + standalone wide as separate devices.
|
|
83
|
+
// CRITICAL: this fallback is what keeps phones (esp. Android) where
|
|
84
|
+
// the ultra-wide has NO multi-cam grouping working — without it,
|
|
85
|
+
// restricting to multicam would REINTRODUCE the "0.5× shows wide" bug
|
|
86
|
+
// for that device population.
|
|
87
|
+
//
|
|
88
|
+
// Prefer a torch-bearing wide-angle device as the `1×`/primary mount.
|
|
89
|
+
const wideDevices = back.filter((d) => hasLens(d, 'wide-angle-camera'));
|
|
90
|
+
const ultraWide = back.find((d) => !d.isMultiCam && hasLens(d, 'ultra-wide-angle-camera')) ??
|
|
91
|
+
back.find((d) => hasLens(d, 'ultra-wide-angle-camera')) ??
|
|
92
|
+
null;
|
|
93
|
+
if (wideDevices.length > 0 && ultraWide != null) {
|
|
94
|
+
// Prefer the simplest wide device (fewest extra lenses) with a torch
|
|
95
|
+
// as the 1× mount, so 1× flash works. Falls back to any wide device.
|
|
96
|
+
const primary = wideDevices.find((d) => d.hasTorch) ?? wideDevices[0];
|
|
97
|
+
return {
|
|
98
|
+
device: primary,
|
|
99
|
+
ultraWideDevice: ultraWide,
|
|
100
|
+
mode: 'standalone-uw',
|
|
101
|
+
has0_5x: true,
|
|
102
|
+
hasTorch: primary.hasTorch,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── 3. Wide-angle only (no ultra-wide reachable on this device).
|
|
106
|
+
const wideOnly = wideDevices.find((d) => d.hasTorch) ?? wideDevices[0] ?? back[0];
|
|
107
|
+
return {
|
|
108
|
+
device: wideOnly,
|
|
109
|
+
ultraWideDevice: null,
|
|
110
|
+
mode: 'wide-only',
|
|
111
|
+
has0_5x: false,
|
|
112
|
+
hasTorch: wideOnly.hasTorch,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Map a UI lens label to a vision-camera `zoom` value for the
|
|
117
|
+
* `multicam` mode (where lens switching is zoom, not device swap).
|
|
118
|
+
*
|
|
119
|
+
* - `1×` → the device's `neutralZoom` (wide-angle baseline; vision-
|
|
120
|
+
* camera docs: "where the camera is in wide-angle mode and hasn't
|
|
121
|
+
* switched to ultra-wide or telephoto yet").
|
|
122
|
+
* - `0.5×` → `minZoom` (the ultra-wide end of the zoom range).
|
|
123
|
+
*
|
|
124
|
+
* Returns `neutralZoom` for any non-0.5× label as a safe default.
|
|
125
|
+
* Only meaningful in `multicam` mode; the standalone path swaps devices
|
|
126
|
+
* and ignores this.
|
|
127
|
+
*/
|
|
128
|
+
function zoomForLens(device, lens) {
|
|
129
|
+
return lens === '0.5x' ? device.minZoom : device.neutralZoom;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=selectCaptureDevice.js.map
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* still use the SDK's quality + stitching modules.
|
|
25
25
|
*/
|
|
26
26
|
import { Camera, useCameraDevice, type PhysicalCameraDeviceType, type TakePhotoOptions } from 'react-native-vision-camera';
|
|
27
|
+
import { type CaptureDeviceMode } from './selectCaptureDevice';
|
|
27
28
|
import type { CaptureResult, QualityThresholds } from '../types';
|
|
28
29
|
/**
|
|
29
30
|
* Hook input. Everything optional; sensible defaults are applied
|
|
@@ -66,8 +67,22 @@ export interface UseCaptureOptions {
|
|
|
66
67
|
* behaves as if `preferredPhysicalDevice` was undefined). The
|
|
67
68
|
* returned `availablePhysicalDevices` exposes what the device
|
|
68
69
|
* actually offers so the host can render an appropriate switcher.
|
|
70
|
+
*
|
|
71
|
+
* v0.13.2 — superseded by `lens` for `<Camera>`'s own use (see
|
|
72
|
+
* `selectCaptureDevice`). Still honoured for direct Layer-2 hosts.
|
|
69
73
|
*/
|
|
70
74
|
preferredPhysicalDevice?: PhysicalCameraDeviceType;
|
|
75
|
+
/**
|
|
76
|
+
* v0.13.2 — the active UI lens (`1×` / `0.5×`). When supplied, the
|
|
77
|
+
* hook uses capability-aware selection (`selectCaptureDevice`): it
|
|
78
|
+
* prefers a multi-cam device spanning both FOVs (lens switched via
|
|
79
|
+
* `zoom`, torch available on every lens), and falls back to a
|
|
80
|
+
* standalone ultra-wide device-swap only where no such multi-cam
|
|
81
|
+
* device exists. Fixes the "0.5× shows wide-angle on some phones"
|
|
82
|
+
* and "flash unavailable on 0.5×" bugs. When omitted, the legacy
|
|
83
|
+
* `preferredPhysicalDevice` path is used (backwards-compatible).
|
|
84
|
+
*/
|
|
85
|
+
lens?: '1x' | '0.5x';
|
|
71
86
|
}
|
|
72
87
|
/**
|
|
73
88
|
* Per-call options for `takePhoto`. Separate from `UseCaptureOptions`
|
|
@@ -136,6 +151,31 @@ export interface UseCaptureReturn {
|
|
|
136
151
|
* load). Always populated by the time the camera is mountable.
|
|
137
152
|
*/
|
|
138
153
|
availablePhysicalDevices: PhysicalCameraDeviceType[];
|
|
154
|
+
/**
|
|
155
|
+
* v0.13.2 — how lenses are switched for the mounted device:
|
|
156
|
+
* 'multicam' — one device spans both FOVs; switch via `deviceZoom`.
|
|
157
|
+
* 'standalone-uw' — separate ultra-wide device; switch by remounting.
|
|
158
|
+
* 'wide-only' — no ultra-wide; no 0.5× chooser.
|
|
159
|
+
*/
|
|
160
|
+
captureMode: CaptureDeviceMode;
|
|
161
|
+
/**
|
|
162
|
+
* v0.13.2 — whether the device can offer a 0.5× ultra-wide lens AT ALL
|
|
163
|
+
* (real capability, replacing the old hardcoded assumption). Drives
|
|
164
|
+
* whether `<Camera>` renders the lens chooser.
|
|
165
|
+
*/
|
|
166
|
+
has0_5x: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* v0.13.2 — whether the currently-MOUNTED device has a torch. Drives
|
|
169
|
+
* the flash control's availability (the standalone ultra-wide has none).
|
|
170
|
+
*/
|
|
171
|
+
deviceHasTorch: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* v0.13.2 — the `zoom` value to apply for the active lens in
|
|
174
|
+
* `multicam` mode (0.5× → ultra-wide end, 1× → wide baseline).
|
|
175
|
+
* `undefined` in standalone/wide-only modes (lens = device identity,
|
|
176
|
+
* no zoom needed). Pass to `<CameraView zoom>`.
|
|
177
|
+
*/
|
|
178
|
+
deviceZoom: number | undefined;
|
|
139
179
|
}
|
|
140
180
|
export declare function useCapture(options?: UseCaptureOptions): UseCaptureReturn;
|
|
141
181
|
//# sourceMappingURL=useCapture.d.ts.map
|
|
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.useCapture = useCapture;
|
|
30
30
|
const react_1 = require("react");
|
|
31
31
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
32
|
+
const selectCaptureDevice_1 = require("./selectCaptureDevice");
|
|
32
33
|
const runQualityCheck_1 = require("../quality/runQualityCheck");
|
|
33
34
|
const normaliseOrientation_1 = require("../quality/normaliseOrientation");
|
|
34
35
|
const paths_1 = require("../utils/paths");
|
|
@@ -61,28 +62,61 @@ function makeCaptureResult(photo, qualityReport) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
function useCapture(options = {}) {
|
|
64
|
-
const { cameraPosition = 'back', enableQualityChecks = false, qualityThresholds, takePhotoOptions, preferredPhysicalDevice, } = options;
|
|
65
|
+
const { cameraPosition = 'back', enableQualityChecks = false, qualityThresholds, takePhotoOptions, preferredPhysicalDevice, lens, } = options;
|
|
65
66
|
const cameraRef = (0, react_1.useRef)(null);
|
|
66
|
-
|
|
67
|
+
const allDevices = (0, react_native_vision_camera_1.useCameraDevices)();
|
|
68
|
+
// v0.13.2 — capability-aware selection (`lens` supplied) vs legacy
|
|
69
|
+
// per-lens physical-device swap (`preferredPhysicalDevice`).
|
|
67
70
|
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
71
|
+
// Capability-aware: `selectCaptureDevice` prefers a multi-cam device
|
|
72
|
+
// that spans wide + ultra-wide (so 0.5× is reached via `zoom` and the
|
|
73
|
+
// torch works on every lens), falling back to a standalone ultra-wide
|
|
74
|
+
// device-swap only where the platform has no such multi-cam grouping.
|
|
75
|
+
// This fixes (a) 0.5× silently showing the wide-angle FOV on phones
|
|
76
|
+
// where the ultra-wide is only inside a multi-cam device, and (b)
|
|
77
|
+
// flash being unavailable on the torchless standalone ultra-wide.
|
|
78
|
+
const selection = (0, react_1.useMemo)(() => (0, selectCaptureDevice_1.selectCaptureDevice)(allDevices), [allDevices]);
|
|
79
|
+
// Legacy path (no `lens`): preserve the pre-v0.13.2 per-physical-lens
|
|
80
|
+
// request so direct Layer-2 hosts that pass `preferredPhysicalDevice`
|
|
81
|
+
// are unaffected.
|
|
82
|
+
const legacyDevice = (0, react_native_vision_camera_1.useCameraDevice)(cameraPosition, {
|
|
74
83
|
physicalDevices: preferredPhysicalDevice ? [preferredPhysicalDevice] : undefined,
|
|
75
84
|
});
|
|
76
|
-
const
|
|
77
|
-
|
|
85
|
+
const legacyFallback = (0, react_native_vision_camera_1.useCameraDevice)(cameraPosition);
|
|
86
|
+
// The mounted device:
|
|
87
|
+
// - lens supplied + multicam → the single multi-cam device
|
|
88
|
+
// (lens switched via `zoom`, computed below).
|
|
89
|
+
// - lens supplied + standalone-uw→ swap to the ultra-wide device on
|
|
90
|
+
// 0.5×, else the wide primary (matches the legacy swap, but with
|
|
91
|
+
// correct device identity from `selectCaptureDevice`).
|
|
92
|
+
// - lens supplied + wide-only → the wide device (0.5× hidden).
|
|
93
|
+
// - no lens → legacy behaviour.
|
|
94
|
+
let device;
|
|
95
|
+
let activeZoom;
|
|
96
|
+
if (lens != null) {
|
|
97
|
+
if (selection.mode === 'standalone-uw' && lens === '0.5x') {
|
|
98
|
+
device = selection.ultraWideDevice ?? selection.device ?? legacyFallback;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
device = selection.device ?? legacyFallback;
|
|
102
|
+
}
|
|
103
|
+
activeZoom =
|
|
104
|
+
selection.mode === 'multicam' && selection.device
|
|
105
|
+
? (0, selectCaptureDevice_1.zoomForLens)(selection.device, lens)
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
device = legacyDevice ?? legacyFallback;
|
|
110
|
+
activeZoom = undefined;
|
|
111
|
+
}
|
|
78
112
|
// Enumerate ALL physical lens types available on the chosen
|
|
79
113
|
// position so the host can decide whether to render a switcher.
|
|
80
114
|
// Vision-camera's `useCameraDevices()` returns CameraDevice[]; each
|
|
81
115
|
// has `physicalDevices: PhysicalCameraDeviceType[]`. We dedupe the
|
|
82
116
|
// union across all devices at `position` so the host sees the full
|
|
83
117
|
// set the platform exposes (some phones expose ultra-wide only via
|
|
84
|
-
// a separate logical camera, not the main one).
|
|
85
|
-
|
|
118
|
+
// a separate logical camera, not the main one). `allDevices` is
|
|
119
|
+
// computed once above (shared with `selectCaptureDevice`).
|
|
86
120
|
const availablePhysicalDevices = (0, react_1.useMemo)(() => {
|
|
87
121
|
const seen = new Set();
|
|
88
122
|
for (const d of allDevices) {
|
|
@@ -176,6 +210,10 @@ function useCapture(options = {}) {
|
|
|
176
210
|
isCapturing,
|
|
177
211
|
takePhoto,
|
|
178
212
|
availablePhysicalDevices,
|
|
213
|
+
captureMode: selection.mode,
|
|
214
|
+
has0_5x: selection.has0_5x,
|
|
215
|
+
deviceHasTorch: device?.hasTorch ?? false,
|
|
216
|
+
deviceZoom: activeZoom,
|
|
179
217
|
};
|
|
180
218
|
}
|
|
181
219
|
//# sourceMappingURL=useCapture.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useContentRotation — returns a CSS transform that rotates control
|
|
3
|
+
* content so labels stay upright relative to device gravity,
|
|
4
|
+
* regardless of whether the OS rotated the framebuffer.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* v0.12 anchored `<Camera>`'s bottom controls to the home-indicator
|
|
9
|
+
* edge so they stay in thumb reach on phones in landscape on
|
|
10
|
+
* non-locked iOS hosts. The anchoring works because the OS rotates
|
|
11
|
+
* the framebuffer to match the device, so a JS-bottom view in
|
|
12
|
+
* landscape is the device's actual landscape-bottom edge.
|
|
13
|
+
*
|
|
14
|
+
* On locked-portrait hosts (the most common production
|
|
15
|
+
* configuration) the OS does NOT rotate the framebuffer when the
|
|
16
|
+
* device tilts to landscape. v0.12 still anchored controls to
|
|
17
|
+
* "JS-bottom" — which is now the device's side edge — so the
|
|
18
|
+
* shutter sits where the thumb expects, BUT the labels inside
|
|
19
|
+
* each control (`AR`, `1×`, `0.5×`, the lens chip pills, the gear)
|
|
20
|
+
* render at their JS-portrait baseline, so the user holding the
|
|
21
|
+
* device sideways reads them at 90°.
|
|
22
|
+
*
|
|
23
|
+
* This hook fixes that by applying a `transform: rotate(±90°)` to
|
|
24
|
+
* the control's *content* so it appears upright relative to actual
|
|
25
|
+
* gravity, while the control container itself stays in place.
|
|
26
|
+
*
|
|
27
|
+
* ## How the rotation is computed
|
|
28
|
+
*
|
|
29
|
+
* Two signals:
|
|
30
|
+
* - **Framebuffer rotation** — what rotation has the OS already
|
|
31
|
+
* applied to the JS layout? Read from
|
|
32
|
+
* `useWindowDimensions().width > height` — non-locked +
|
|
33
|
+
* device-landscape is the only case where the OS rotates,
|
|
34
|
+
* and that's exactly when `jsLandscape === true`.
|
|
35
|
+
* - **Device-physical rotation** — what rotation does the device
|
|
36
|
+
* have relative to gravity? Read from `useDeviceOrientation()`
|
|
37
|
+
* (accelerometer-derived).
|
|
38
|
+
*
|
|
39
|
+
* The content rotation we apply is the *difference* between
|
|
40
|
+
* device-physical and framebuffer rotation, so the net rotation
|
|
41
|
+
* (content × framebuffer) equals device-physical → labels are
|
|
42
|
+
* upright in the world.
|
|
43
|
+
*
|
|
44
|
+
* ## Truth table
|
|
45
|
+
*
|
|
46
|
+
* | Host config | Device | jsLandscape | Net rot |
|
|
47
|
+
* |--- |--- |--- |--- |
|
|
48
|
+
* | Locked-portrait | portrait | false | 0° |
|
|
49
|
+
* | Locked-portrait | landscape-left | false | 90° |
|
|
50
|
+
* | Locked-portrait | landscape-right | false | -90° |
|
|
51
|
+
* | Locked-portrait | upside-down | false | 180° |
|
|
52
|
+
* | Non-locked | portrait | false | 0° |
|
|
53
|
+
* | Non-locked | landscape-left | true | 0° |
|
|
54
|
+
* | Non-locked | landscape-right | true | 0° |
|
|
55
|
+
*
|
|
56
|
+
* The 0° case is the common one (locked-portrait + device-portrait
|
|
57
|
+
* OR non-locked + framebuffer-already-rotated); we return an empty
|
|
58
|
+
* style object so React skips the layout work.
|
|
59
|
+
*
|
|
60
|
+
* ## Caveats
|
|
61
|
+
*
|
|
62
|
+
* - Rotation transforms preserve hit-testing in RN 0.84 (verified
|
|
63
|
+
* on iOS + Android), but historical RN versions had bugs in this
|
|
64
|
+
* area. If support for older RN is added, retest pressables.
|
|
65
|
+
* - Containers whose sized layouts depend on un-rotated content
|
|
66
|
+
* (e.g. a 100px-wide pill containing text that's now rotated 90°)
|
|
67
|
+
* may overflow. Fixed-size pills (the lens chip, AR toggle,
|
|
68
|
+
* flash button) are fine; the header title's `flex: 1 + textAlign:
|
|
69
|
+
* center` may need tuning when rotated — see `CaptureHeader`'s
|
|
70
|
+
* own rotation handling.
|
|
71
|
+
*/
|
|
72
|
+
import { type ViewStyle } from 'react-native';
|
|
73
|
+
import { type DeviceOrientation } from './useDeviceOrientation';
|
|
74
|
+
export type ContentRotationDeg = 0 | 90 | -90 | 180;
|
|
75
|
+
/**
|
|
76
|
+
* Return type for `useContentRotation`. Typed structurally on just
|
|
77
|
+
* the `transform` property so it spreads cleanly into ViewStyle,
|
|
78
|
+
* TextStyle, AND ImageStyle — all three accept identical transform
|
|
79
|
+
* shapes in RN 0.84. Returning the more specific `ViewStyle` would
|
|
80
|
+
* collide with ImageStyle's stricter `overflow` enum at <Image>
|
|
81
|
+
* call sites.
|
|
82
|
+
*/
|
|
83
|
+
export type ContentRotationStyle = {
|
|
84
|
+
transform?: ViewStyle['transform'];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Pure rotation computation. Exported so tests can exercise the
|
|
88
|
+
* full truth table without booting a React render.
|
|
89
|
+
*/
|
|
90
|
+
export declare function contentRotationDeg(jsLandscape: boolean, deviceOrient: DeviceOrientation): ContentRotationDeg;
|
|
91
|
+
/**
|
|
92
|
+
* Returns the rotation as a ready-to-spread style object. Empty
|
|
93
|
+
* object in the common 0° case so React skips the layout work.
|
|
94
|
+
* Type `ContentRotationStyle` is structurally just `{ transform? }`
|
|
95
|
+
* so call sites can spread it into ViewStyle, TextStyle, or
|
|
96
|
+
* ImageStyle interchangeably.
|
|
97
|
+
*/
|
|
98
|
+
export declare function useContentRotation(): ContentRotationStyle;
|
|
99
|
+
//# sourceMappingURL=useContentRotation.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useContentRotation — returns a CSS transform that rotates control
|
|
5
|
+
* content so labels stay upright relative to device gravity,
|
|
6
|
+
* regardless of whether the OS rotated the framebuffer.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this exists
|
|
9
|
+
*
|
|
10
|
+
* v0.12 anchored `<Camera>`'s bottom controls to the home-indicator
|
|
11
|
+
* edge so they stay in thumb reach on phones in landscape on
|
|
12
|
+
* non-locked iOS hosts. The anchoring works because the OS rotates
|
|
13
|
+
* the framebuffer to match the device, so a JS-bottom view in
|
|
14
|
+
* landscape is the device's actual landscape-bottom edge.
|
|
15
|
+
*
|
|
16
|
+
* On locked-portrait hosts (the most common production
|
|
17
|
+
* configuration) the OS does NOT rotate the framebuffer when the
|
|
18
|
+
* device tilts to landscape. v0.12 still anchored controls to
|
|
19
|
+
* "JS-bottom" — which is now the device's side edge — so the
|
|
20
|
+
* shutter sits where the thumb expects, BUT the labels inside
|
|
21
|
+
* each control (`AR`, `1×`, `0.5×`, the lens chip pills, the gear)
|
|
22
|
+
* render at their JS-portrait baseline, so the user holding the
|
|
23
|
+
* device sideways reads them at 90°.
|
|
24
|
+
*
|
|
25
|
+
* This hook fixes that by applying a `transform: rotate(±90°)` to
|
|
26
|
+
* the control's *content* so it appears upright relative to actual
|
|
27
|
+
* gravity, while the control container itself stays in place.
|
|
28
|
+
*
|
|
29
|
+
* ## How the rotation is computed
|
|
30
|
+
*
|
|
31
|
+
* Two signals:
|
|
32
|
+
* - **Framebuffer rotation** — what rotation has the OS already
|
|
33
|
+
* applied to the JS layout? Read from
|
|
34
|
+
* `useWindowDimensions().width > height` — non-locked +
|
|
35
|
+
* device-landscape is the only case where the OS rotates,
|
|
36
|
+
* and that's exactly when `jsLandscape === true`.
|
|
37
|
+
* - **Device-physical rotation** — what rotation does the device
|
|
38
|
+
* have relative to gravity? Read from `useDeviceOrientation()`
|
|
39
|
+
* (accelerometer-derived).
|
|
40
|
+
*
|
|
41
|
+
* The content rotation we apply is the *difference* between
|
|
42
|
+
* device-physical and framebuffer rotation, so the net rotation
|
|
43
|
+
* (content × framebuffer) equals device-physical → labels are
|
|
44
|
+
* upright in the world.
|
|
45
|
+
*
|
|
46
|
+
* ## Truth table
|
|
47
|
+
*
|
|
48
|
+
* | Host config | Device | jsLandscape | Net rot |
|
|
49
|
+
* |--- |--- |--- |--- |
|
|
50
|
+
* | Locked-portrait | portrait | false | 0° |
|
|
51
|
+
* | Locked-portrait | landscape-left | false | 90° |
|
|
52
|
+
* | Locked-portrait | landscape-right | false | -90° |
|
|
53
|
+
* | Locked-portrait | upside-down | false | 180° |
|
|
54
|
+
* | Non-locked | portrait | false | 0° |
|
|
55
|
+
* | Non-locked | landscape-left | true | 0° |
|
|
56
|
+
* | Non-locked | landscape-right | true | 0° |
|
|
57
|
+
*
|
|
58
|
+
* The 0° case is the common one (locked-portrait + device-portrait
|
|
59
|
+
* OR non-locked + framebuffer-already-rotated); we return an empty
|
|
60
|
+
* style object so React skips the layout work.
|
|
61
|
+
*
|
|
62
|
+
* ## Caveats
|
|
63
|
+
*
|
|
64
|
+
* - Rotation transforms preserve hit-testing in RN 0.84 (verified
|
|
65
|
+
* on iOS + Android), but historical RN versions had bugs in this
|
|
66
|
+
* area. If support for older RN is added, retest pressables.
|
|
67
|
+
* - Containers whose sized layouts depend on un-rotated content
|
|
68
|
+
* (e.g. a 100px-wide pill containing text that's now rotated 90°)
|
|
69
|
+
* may overflow. Fixed-size pills (the lens chip, AR toggle,
|
|
70
|
+
* flash button) are fine; the header title's `flex: 1 + textAlign:
|
|
71
|
+
* center` may need tuning when rotated — see `CaptureHeader`'s
|
|
72
|
+
* own rotation handling.
|
|
73
|
+
*/
|
|
74
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75
|
+
exports.contentRotationDeg = contentRotationDeg;
|
|
76
|
+
exports.useContentRotation = useContentRotation;
|
|
77
|
+
const react_native_1 = require("react-native");
|
|
78
|
+
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
79
|
+
/**
|
|
80
|
+
* Pure rotation computation. Exported so tests can exercise the
|
|
81
|
+
* full truth table without booting a React render.
|
|
82
|
+
*/
|
|
83
|
+
function contentRotationDeg(jsLandscape, deviceOrient) {
|
|
84
|
+
// Framebuffer rotation relative to device-physical. Only the
|
|
85
|
+
// non-locked + device-landscape cases see a rotated framebuffer.
|
|
86
|
+
// jsLandscape can briefly be true mid-rotation on devices that
|
|
87
|
+
// aren't a clean landscape orientation; the device-orientation
|
|
88
|
+
// check below catches those and falls through to 0.
|
|
89
|
+
const fbRot = !jsLandscape ? 0
|
|
90
|
+
: deviceOrient === 'landscape-left' ? 90
|
|
91
|
+
: deviceOrient === 'landscape-right' ? -90
|
|
92
|
+
: 0;
|
|
93
|
+
// Device-physical rotation relative to gravity.
|
|
94
|
+
const deviceRot = deviceOrient === 'portrait' ? 0
|
|
95
|
+
: deviceOrient === 'landscape-left' ? 90
|
|
96
|
+
: deviceOrient === 'landscape-right' ? -90
|
|
97
|
+
: 180;
|
|
98
|
+
// Net rotation we need to apply to content so that
|
|
99
|
+
// content + framebuffer = device-physical (upright in the world).
|
|
100
|
+
// Normalise to [-180, 180] so transform values stay canonical.
|
|
101
|
+
let net = deviceRot - fbRot;
|
|
102
|
+
if (net > 180)
|
|
103
|
+
net -= 360;
|
|
104
|
+
if (net < -180)
|
|
105
|
+
net += 360;
|
|
106
|
+
return net;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns the rotation as a ready-to-spread style object. Empty
|
|
110
|
+
* object in the common 0° case so React skips the layout work.
|
|
111
|
+
* Type `ContentRotationStyle` is structurally just `{ transform? }`
|
|
112
|
+
* so call sites can spread it into ViewStyle, TextStyle, or
|
|
113
|
+
* ImageStyle interchangeably.
|
|
114
|
+
*/
|
|
115
|
+
function useContentRotation() {
|
|
116
|
+
const orient = (0, useDeviceOrientation_1.useDeviceOrientation)();
|
|
117
|
+
const { width, height } = (0, react_native_1.useWindowDimensions)();
|
|
118
|
+
const jsLandscape = width > height;
|
|
119
|
+
const deg = contentRotationDeg(jsLandscape, orient);
|
|
120
|
+
return deg === 0
|
|
121
|
+
? {}
|
|
122
|
+
: { transform: [{ rotate: `${deg}deg` }] };
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=useContentRotation.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* adds RetaiLens-specific features on top.
|
|
21
21
|
*/
|
|
22
22
|
export { Camera, CameraError } from './camera/Camera';
|
|
23
|
-
export type { CameraProps, CameraCaptureResult, CameraErrorCode, CaptureSource, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
|
|
23
|
+
export type { CameraProps, CameraCaptureResult, CameraErrorCode, CaptureSource, CaptureSourcesMode, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
|
|
24
24
|
export { useARSession, ARTrackingState } from './ar/useARSession';
|
|
25
25
|
export type { UseARSessionReturn, FramePose, } from './ar/useARSession';
|
|
26
26
|
export { useIMUTranslationGate } from './sensors/useIMUTranslationGate';
|
|
@@ -47,9 +47,7 @@ export { CaptureStitchStatsToast, useStitchStatsToast, } from './camera/CaptureS
|
|
|
47
47
|
export type { CaptureStitchStatsToastProps, UseStitchStatsToastReturn, } from './camera/CaptureStitchStatsToast';
|
|
48
48
|
export { CaptureThumbnailStrip } from './camera/CaptureThumbnailStrip';
|
|
49
49
|
export type { CaptureThumbnailItem } from './camera/CaptureThumbnailStrip';
|
|
50
|
-
export { IncrementalPanGuide } from './camera/IncrementalPanGuide';
|
|
51
50
|
export { PanoramaBandOverlay } from './camera/PanoramaBandOverlay';
|
|
52
|
-
export { PanoramaGuidance } from './camera/PanoramaGuidance';
|
|
53
51
|
export { PanoramaSettingsModal } from './camera/PanoramaSettingsModal';
|
|
54
52
|
export type { PanoramaSettingsModalProps } from './camera/PanoramaSettingsModal';
|
|
55
53
|
export { DEFAULT_PANORAMA_SETTINGS, DEFAULT_FLOW_GATE_SETTINGS, DEFAULT_SLITSCAN_SETTINGS, DEFAULT_HYBRID_SETTINGS, } from './camera/PanoramaSettings';
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* adds RetaiLens-specific features on top.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.stitchVideo = exports.useStitcherWorklet = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.OrientationDriftModal = exports.useOrientationDrift = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.
|
|
25
|
+
exports.stitchVideo = exports.useStitcherWorklet = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.OrientationDriftModal = exports.useOrientationDrift = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaBandOverlay = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────
|
|
27
27
|
// Layer 1 — the high-level <Camera> component
|
|
28
28
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -85,12 +85,13 @@ Object.defineProperty(exports, "CaptureStitchStatsToast", { enumerable: true, ge
|
|
|
85
85
|
Object.defineProperty(exports, "useStitchStatsToast", { enumerable: true, get: function () { return CaptureStitchStatsToast_1.useStitchStatsToast; } });
|
|
86
86
|
var CaptureThumbnailStrip_1 = require("./camera/CaptureThumbnailStrip");
|
|
87
87
|
Object.defineProperty(exports, "CaptureThumbnailStrip", { enumerable: true, get: function () { return CaptureThumbnailStrip_1.CaptureThumbnailStrip; } });
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// v0.13.1 — IncrementalPanGuide (drift marker) and PanoramaGuidance
|
|
89
|
+
// (pan-speed pill) are no longer part of the public API. They remain
|
|
90
|
+
// in the tree as internal-only components but are not exported and not
|
|
91
|
+
// rendered by <Camera> (the `panGuide` / `panoramaGuidance` props were
|
|
92
|
+
// removed). Re-introduce here if a host need resurfaces.
|
|
90
93
|
var PanoramaBandOverlay_1 = require("./camera/PanoramaBandOverlay");
|
|
91
94
|
Object.defineProperty(exports, "PanoramaBandOverlay", { enumerable: true, get: function () { return PanoramaBandOverlay_1.PanoramaBandOverlay; } });
|
|
92
|
-
var PanoramaGuidance_1 = require("./camera/PanoramaGuidance");
|
|
93
|
-
Object.defineProperty(exports, "PanoramaGuidance", { enumerable: true, get: function () { return PanoramaGuidance_1.PanoramaGuidance; } });
|
|
94
95
|
// Settings modal — the modal is in `PanoramaSettingsModal.tsx`, but
|
|
95
96
|
// the type tree + defaults + JS↔native bridge live in dedicated
|
|
96
97
|
// files since v0.4 (F10). The modal is now a thin presentational
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|