react-native-image-stitcher 0.11.1 → 0.12.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 +75 -0
- package/README.md +28 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +3 -2
- package/dist/camera/ARCameraView.d.ts +10 -0
- package/dist/camera/ARCameraView.js +1 -0
- package/dist/camera/Camera.d.ts +20 -0
- package/dist/camera/Camera.js +175 -6
- package/dist/camera/OrientationDriftModal.d.ts +83 -0
- package/dist/camera/OrientationDriftModal.js +159 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +13 -1
- package/dist/camera/PanoramaBandOverlay.js +106 -45
- package/dist/camera/PanoramaSettingsModal.js +15 -1
- package/dist/camera/ViewportCropOverlay.d.ts +35 -31
- package/dist/camera/ViewportCropOverlay.js +39 -30
- package/dist/camera/useDeviceOrientation.d.ts +18 -9
- package/dist/camera/useDeviceOrientation.js +18 -9
- package/dist/camera/useOrientationDrift.d.ts +104 -0
- package/dist/camera/useOrientationDrift.js +120 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -1
- package/dist/stitching/incremental.d.ts +5 -3
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +7 -1
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +4 -3
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +9 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +46 -7
- package/package.json +1 -1
- package/src/camera/ARCameraView.tsx +18 -1
- package/src/camera/Camera.tsx +280 -13
- package/src/camera/OrientationDriftModal.tsx +224 -0
- package/src/camera/PanoramaBandOverlay.tsx +135 -49
- package/src/camera/PanoramaSettingsModal.tsx +14 -0
- package/src/camera/ViewportCropOverlay.tsx +52 -30
- package/src/camera/__tests__/useOrientationDrift.test.ts +169 -0
- package/src/camera/useDeviceOrientation.ts +18 -9
- package/src/camera/useOrientationDrift.ts +172 -0
- package/src/index.ts +13 -0
- package/src/stitching/incremental.ts +5 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useOrientationDrift — detects mid-capture device rotation.
|
|
5
|
+
*
|
|
6
|
+
* Pairs with `useDeviceOrientation()` to surface the case where the
|
|
7
|
+
* user rotates the device *during* an active capture. The
|
|
8
|
+
* incremental stitching engine supports both portrait (Mode B,
|
|
9
|
+
* horizontal pan) and landscape (Mode A, vertical pan) capture
|
|
10
|
+
* modes as first-class — but mixing them mid-capture produces
|
|
11
|
+
* malformed output ("cross-mode capture is best-effort," per
|
|
12
|
+
* `incremental.ts:373-403`). Hosts that want to protect against
|
|
13
|
+
* this use this hook + `OrientationDriftModal` together: the
|
|
14
|
+
* `<Camera>` flagship component auto-abandons capture the instant
|
|
15
|
+
* `drifted === true` (PR-2 wiring); the modal surfaces an
|
|
16
|
+
* explanatory popup to the user.
|
|
17
|
+
*
|
|
18
|
+
* ## API contract
|
|
19
|
+
*
|
|
20
|
+
* Pass `active` true while a capture is in flight, false otherwise.
|
|
21
|
+
* Returns:
|
|
22
|
+
*
|
|
23
|
+
* - `captureOrientation` — the orientation snapshotted at the
|
|
24
|
+
* moment `active` transitioned false → true. `undefined` when
|
|
25
|
+
* `active` is false.
|
|
26
|
+
* - `currentOrientation` — live orientation from
|
|
27
|
+
* `useDeviceOrientation()`. Always defined (defaults to
|
|
28
|
+
* `'portrait'` until the accelerometer's first sample).
|
|
29
|
+
* - `drifted` — `true` IFF `active` is currently true AND
|
|
30
|
+
* `currentOrientation !== captureOrientation` at some point
|
|
31
|
+
* since the snapshot. **Latching** — once true, stays true
|
|
32
|
+
* until `active` flips back to false. This is intentional:
|
|
33
|
+
* after detection, callers should auto-abandon the capture
|
|
34
|
+
* (engine `stop()`); allowing the flag to clear before then
|
|
35
|
+
* would mask the drift if the user rotated back to the
|
|
36
|
+
* original orientation between the detection tick and the
|
|
37
|
+
* callers' abandonment effect.
|
|
38
|
+
*
|
|
39
|
+
* ## Semantics by transition
|
|
40
|
+
*
|
|
41
|
+
* - `active` false → true: snapshot `currentOrientation`;
|
|
42
|
+
* reset `drifted` to false.
|
|
43
|
+
* - `active` true (steady): if `currentOrientation !==
|
|
44
|
+
* captureOrientation` at any point, latch `drifted = true`.
|
|
45
|
+
* - `active` true → false: clear snapshot; reset `drifted`.
|
|
46
|
+
*
|
|
47
|
+
* ## Why a separate hook (rather than inlining in `<Camera>`)
|
|
48
|
+
*
|
|
49
|
+
* Hosts using the Layer-2 building blocks (`CameraView` directly,
|
|
50
|
+
* custom capture UX) can reuse this hook without mounting the
|
|
51
|
+
* full `<Camera>` flagship. Same composition pattern as
|
|
52
|
+
* `useIMUTranslationGate` and `useKeyframeStream`.
|
|
53
|
+
*
|
|
54
|
+
* ## Testing
|
|
55
|
+
*
|
|
56
|
+
* The pure state-transition function `_computeDriftStateForTests`
|
|
57
|
+
* is exported separately so jest can exercise all 5 transition
|
|
58
|
+
* cases without booting a React render. The hook itself is a
|
|
59
|
+
* thin wrapper around it (verified via on-device manual flow in
|
|
60
|
+
* the v0.12 verification checklist).
|
|
61
|
+
*/
|
|
62
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
+
exports._computeDriftStateForTests = _computeDriftStateForTests;
|
|
64
|
+
exports.useOrientationDrift = useOrientationDrift;
|
|
65
|
+
const react_1 = require("react");
|
|
66
|
+
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
67
|
+
const INITIAL_STATE = {
|
|
68
|
+
captureOrientation: undefined,
|
|
69
|
+
drifted: false,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Pure state-transition function for the drift detector. Exported
|
|
73
|
+
* with a `_` prefix to signal "internal — not part of the public
|
|
74
|
+
* API." Jest uses this directly so tests don't need a React
|
|
75
|
+
* renderer (the lib's jest config is pure-data / no RN preset).
|
|
76
|
+
*
|
|
77
|
+
* Given the previous state + the current `active` flag + the
|
|
78
|
+
* current device orientation, returns the new state. Idempotent
|
|
79
|
+
* when nothing changed (returns the same object reference) so
|
|
80
|
+
* downstream `useState(setState)` calls become no-ops.
|
|
81
|
+
*/
|
|
82
|
+
function _computeDriftStateForTests(prev, active, currentOrientation) {
|
|
83
|
+
if (!active) {
|
|
84
|
+
// active is false (or just transitioned to false). Clear the
|
|
85
|
+
// snapshot + drift flag. Idempotent when already cleared.
|
|
86
|
+
if (prev.captureOrientation === undefined && !prev.drifted) {
|
|
87
|
+
return prev;
|
|
88
|
+
}
|
|
89
|
+
return INITIAL_STATE;
|
|
90
|
+
}
|
|
91
|
+
// active is true.
|
|
92
|
+
if (prev.captureOrientation === undefined) {
|
|
93
|
+
// false → true transition. Snapshot the current orientation.
|
|
94
|
+
// drifted starts false because, by definition, the current
|
|
95
|
+
// orientation matches itself.
|
|
96
|
+
return { captureOrientation: currentOrientation, drifted: false };
|
|
97
|
+
}
|
|
98
|
+
// active is steady true. Check for drift. Latching: once
|
|
99
|
+
// drifted is true, never set it back to false until active
|
|
100
|
+
// flips (handled above).
|
|
101
|
+
if (!prev.drifted && currentOrientation !== prev.captureOrientation) {
|
|
102
|
+
return { captureOrientation: prev.captureOrientation, drifted: true };
|
|
103
|
+
}
|
|
104
|
+
// No transition + no new drift. Return prev to avoid an
|
|
105
|
+
// unnecessary state update + re-render.
|
|
106
|
+
return prev;
|
|
107
|
+
}
|
|
108
|
+
function useOrientationDrift(active) {
|
|
109
|
+
const currentOrientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
|
|
110
|
+
const [state, setState] = (0, react_1.useState)(INITIAL_STATE);
|
|
111
|
+
(0, react_1.useEffect)(() => {
|
|
112
|
+
setState((prev) => _computeDriftStateForTests(prev, active, currentOrientation));
|
|
113
|
+
}, [active, currentOrientation]);
|
|
114
|
+
return {
|
|
115
|
+
drifted: state.drifted,
|
|
116
|
+
captureOrientation: state.captureOrientation,
|
|
117
|
+
currentOrientation,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=useOrientationDrift.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export { useCapture } from './camera/useCapture';
|
|
|
61
61
|
export type { TakePhotoCallOptions } from './camera/useCapture';
|
|
62
62
|
export { useVideoCapture } from './camera/useVideoCapture';
|
|
63
63
|
export { useDeviceOrientation } from './camera/useDeviceOrientation';
|
|
64
|
+
export type { DeviceOrientation } from './camera/useDeviceOrientation';
|
|
65
|
+
export { useOrientationDrift } from './camera/useOrientationDrift';
|
|
66
|
+
export type { UseOrientationDriftReturn } from './camera/useOrientationDrift';
|
|
67
|
+
export { OrientationDriftModal } from './camera/OrientationDriftModal';
|
|
68
|
+
export type { OrientationDriftModalProps } from './camera/OrientationDriftModal';
|
|
64
69
|
export { IncrementalOutcome, incrementalStitcherIsAvailable, subscribeIncrementalState, getIncrementalNativeModule, cleanupOldKeyframes, } from './stitching/incremental';
|
|
65
70
|
export type { IncrementalState, AcceptedKeyframe } from './stitching/incremental';
|
|
66
71
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
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.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.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = 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;
|
|
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.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = 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
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -126,6 +126,17 @@ var useVideoCapture_1 = require("./camera/useVideoCapture");
|
|
|
126
126
|
Object.defineProperty(exports, "useVideoCapture", { enumerable: true, get: function () { return useVideoCapture_1.useVideoCapture; } });
|
|
127
127
|
var useDeviceOrientation_1 = require("./camera/useDeviceOrientation");
|
|
128
128
|
Object.defineProperty(exports, "useDeviceOrientation", { enumerable: true, get: function () { return useDeviceOrientation_1.useDeviceOrientation; } });
|
|
129
|
+
// v0.12.0 — orientation-aware Camera (R2-lite). `useOrientationDrift`
|
|
130
|
+
// snapshots the device orientation at capture start and latches a
|
|
131
|
+
// `drifted` flag if the user rotates mid-capture. Pairs with
|
|
132
|
+
// `OrientationDriftModal` for the auto-abandon UX flow. The
|
|
133
|
+
// flagship `<Camera>` component wires both internally (PR-2);
|
|
134
|
+
// Layer-2 hosts using `CameraView` directly can compose the pair
|
|
135
|
+
// manually (see the modal's docstring for the integration pattern).
|
|
136
|
+
var useOrientationDrift_1 = require("./camera/useOrientationDrift");
|
|
137
|
+
Object.defineProperty(exports, "useOrientationDrift", { enumerable: true, get: function () { return useOrientationDrift_1.useOrientationDrift; } });
|
|
138
|
+
var OrientationDriftModal_1 = require("./camera/OrientationDriftModal");
|
|
139
|
+
Object.defineProperty(exports, "OrientationDriftModal", { enumerable: true, get: function () { return OrientationDriftModal_1.OrientationDriftModal; } });
|
|
129
140
|
// ── Incremental stitching engine ──────────────────────────────────────
|
|
130
141
|
// JS bindings around the native `IncrementalStitcher` module. Use
|
|
131
142
|
// these when you need finer control than <Camera>'s built-in
|
|
@@ -127,9 +127,11 @@ export interface IncrementalState {
|
|
|
127
127
|
* at the FIRST-FRAME determination thereafter.
|
|
128
128
|
*
|
|
129
129
|
* **This is the single source of truth for orientation across
|
|
130
|
-
* the SDK + host.**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
130
|
+
* the SDK + host.** Pose-derived detection is preferred over
|
|
131
|
+
* JS-side hooks because it works identically regardless of host
|
|
132
|
+
* configuration — `useWindowDimensions` reports JS-portrait when
|
|
133
|
+
* the host is portrait-locked (even with the device in landscape),
|
|
134
|
+
* while pose data reflects what the camera actually saw. UI
|
|
133
135
|
* components that need to know orientation (band overlay, dim
|
|
134
136
|
* bars, pan guide) MUST consume `state.isLandscape` rather
|
|
135
137
|
* than re-detecting.
|
|
@@ -99,9 +99,15 @@ public final class RNSARSessionBridge: NSObject {
|
|
|
99
99
|
) {
|
|
100
100
|
let path = (options["path"] as? String) ?? ""
|
|
101
101
|
let quality = (options["quality"] as? Int) ?? 90
|
|
102
|
+
// v0.12.0 — host passes the actual device orientation so
|
|
103
|
+
// the saved JPEG matches the user's view. Defaults to
|
|
104
|
+
// "portrait" if absent, preserving pre-v0.12 behavior for
|
|
105
|
+
// any caller that hasn't been updated.
|
|
106
|
+
let orientation = (options["orientation"] as? String) ?? "portrait"
|
|
102
107
|
RNSARSession.shared.takePhoto(
|
|
103
108
|
toPath: path,
|
|
104
|
-
quality: quality
|
|
109
|
+
quality: quality,
|
|
110
|
+
orientation: orientation
|
|
105
111
|
) { result, error in
|
|
106
112
|
if let error = error {
|
|
107
113
|
rejecter("ar-photo-failed", error.localizedDescription, error)
|
|
@@ -100,9 +100,10 @@ typedef NS_ENUM(NSInteger, RLISFrameOutcome) {
|
|
|
100
100
|
/// JS side reads this from `IncrementalState.isLandscape` to drive
|
|
101
101
|
/// orientation-aware UI (band overlay, dim bars). This is the
|
|
102
102
|
/// single source of truth for orientation across the SDK + host —
|
|
103
|
-
///
|
|
104
|
-
///
|
|
105
|
-
///
|
|
103
|
+
/// pose-derived detection (V12.6) is preferred because it works
|
|
104
|
+
/// regardless of host orientation config (portrait-locked hosts
|
|
105
|
+
/// suppress framebuffer rotation, so `useWindowDimensions` lies
|
|
106
|
+
/// about the physical hold; pose data doesn't).
|
|
106
107
|
@property (nonatomic, readonly) BOOL isLandscape;
|
|
107
108
|
|
|
108
109
|
/// V12.14.9 — running max paint position along the pan axis, in
|
|
@@ -305,13 +305,15 @@ constexpr double kRansacReprojThresh = 5.0;
|
|
|
305
305
|
_frameRotationDegrees = frameRotationDegrees;
|
|
306
306
|
// V12.6 Step C: _isLandscape is no longer derived from the
|
|
307
307
|
// JS-passed frameRotationDegrees. V12.5 telemetry proved
|
|
308
|
-
// JS was sending the wrong value
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
308
|
+
// JS was sending the wrong value under portrait-locked
|
|
309
|
+
// hosts (the lock suppresses RN's rotation event so
|
|
310
|
+
// `useWindowDimensions` always reported portrait even in
|
|
311
|
+
// landscape). We now detect at first-frame init from
|
|
312
|
+
// R_panToCam directly — see the cylindricalWarp's
|
|
313
|
+
// first-frame branch. Pose-derived detection works for
|
|
314
|
+
// both portrait-locked and non-locked R2-lite hosts.
|
|
315
|
+
// Default false here is just a safe initialiser; it WILL
|
|
316
|
+
// be overwritten before any warping happens.
|
|
315
317
|
_isLandscape = NO;
|
|
316
318
|
// Default compose dims preserve the 4:3 sensor aspect
|
|
317
319
|
// (1920x1440 → 960x720 at scale 0.5). Always landscape
|
|
@@ -813,6 +813,7 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
813
813
|
@objc public func takePhoto(
|
|
814
814
|
toPath rawPath: String,
|
|
815
815
|
quality: Int,
|
|
816
|
+
orientation: String,
|
|
816
817
|
completion: @escaping ([String: Any]?, NSError?) -> Void
|
|
817
818
|
) {
|
|
818
819
|
let resolvedPath: String
|
|
@@ -835,14 +836,52 @@ public final class RNSARSession: NSObject, ARSessionDelegate {
|
|
|
835
836
|
}
|
|
836
837
|
let pixelBuffer = frame.capturedImage
|
|
837
838
|
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
839
|
+
// v0.12.0 — Pre-v0.12 this method hardcoded `.right` (90° CW)
|
|
840
|
+
// to rotate-to-portrait, assuming the user always held the
|
|
841
|
+
// phone in portrait. Under R2-lite the device can be in
|
|
842
|
+
// any orientation, so we pick the CIImage orientation per
|
|
843
|
+
// the JS-supplied `orientation` arg (from
|
|
844
|
+
// `useDeviceOrientation()`).
|
|
845
|
+
//
|
|
846
|
+
// Empirical mapping (on-device test 2026-05-28):
|
|
847
|
+
// portrait → .right (90° CW — preserved from pre-v0.12)
|
|
848
|
+
// landscape-left → .up (sensor matches device tilt; no rotation)
|
|
849
|
+
// landscape-right → .down (180° — sensor opposite of device tilt)
|
|
850
|
+
// portrait-upside-down → .left (90° CCW)
|
|
851
|
+
//
|
|
852
|
+
// The landscape mapping (landscape-left → .up) was determined
|
|
853
|
+
// empirically and is the opposite of what Apple's ARKit
|
|
854
|
+
// pixel-buffer-orientation docs would imply. Likely because
|
|
855
|
+
// `useDeviceOrientation()` reports `landscape-left` via the
|
|
856
|
+
// `UIDeviceOrientation` convention (home indicator on user-
|
|
857
|
+
// right) while iOS's sensor-native orientation matches that
|
|
858
|
+
// tilt direction directly. Without this fix, AR-mode single
|
|
859
|
+
// photos in landscape come out upside-down.
|
|
860
|
+
// v0.12.0 — Pre-v0.12 this method hardcoded `.right` (90° CW)
|
|
861
|
+
// to rotate-to-portrait, assuming the user always held the
|
|
862
|
+
// phone in portrait. Under R2-lite the device can be in
|
|
863
|
+
// any orientation, so we pick the CIImage orientation per
|
|
864
|
+
// the JS-supplied `orientation` arg (from
|
|
865
|
+
// `useDeviceOrientation()`).
|
|
866
|
+
//
|
|
867
|
+
// Empirical mapping (on-device test 2026-05-28):
|
|
868
|
+
// portrait → .right (90° CW — preserved from pre-v0.12)
|
|
869
|
+
// landscape-left → .up (sensor matches device tilt; no rotation)
|
|
870
|
+
// landscape-right → .down (180° — sensor opposite of device tilt)
|
|
871
|
+
// portrait-upside-down → .left (90° CCW)
|
|
872
|
+
//
|
|
873
|
+
// The landscape mapping (landscape-left → .up) was determined
|
|
874
|
+
// empirically; the user reported AR landscape photos came out
|
|
875
|
+
// upside-down with .down and correctly upright with .up.
|
|
876
|
+
let exifOrientation: CGImagePropertyOrientation
|
|
877
|
+
switch orientation {
|
|
878
|
+
case "landscape-left": exifOrientation = .up
|
|
879
|
+
case "landscape-right": exifOrientation = .down
|
|
880
|
+
case "portrait-upside-down": exifOrientation = .left
|
|
881
|
+
default: exifOrientation = .right // portrait + unknown
|
|
882
|
+
}
|
|
844
883
|
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
|
845
|
-
.oriented(
|
|
884
|
+
.oriented(exifOrientation)
|
|
846
885
|
let context = CIContext(options: nil)
|
|
847
886
|
guard let cgImage = context.createCGImage(
|
|
848
887
|
ciImage,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -84,7 +84,23 @@ export interface ARCameraViewHandle {
|
|
|
84
84
|
* isMirrored, isRawPhoto }`). Native generates a temp path —
|
|
85
85
|
* caller does NOT need to construct one.
|
|
86
86
|
*/
|
|
87
|
-
takePhoto: (options?: {
|
|
87
|
+
takePhoto: (options?: {
|
|
88
|
+
quality?: number;
|
|
89
|
+
/**
|
|
90
|
+
* v0.12.0 — device orientation at capture time, used to bake
|
|
91
|
+
* correct rotation into the saved JPEG. Pass the value from
|
|
92
|
+
* `useDeviceOrientation()`. Defaults to `'portrait'` on the
|
|
93
|
+
* native side if omitted (preserves pre-v0.12 behavior).
|
|
94
|
+
* Without this, AR-mode photos taken in landscape come out
|
|
95
|
+
* sideways because the native side previously hardcoded the
|
|
96
|
+
* rotate-to-portrait assumption.
|
|
97
|
+
*/
|
|
98
|
+
orientation?:
|
|
99
|
+
| 'portrait'
|
|
100
|
+
| 'portrait-upside-down'
|
|
101
|
+
| 'landscape-left'
|
|
102
|
+
| 'landscape-right';
|
|
103
|
+
}) => Promise<{
|
|
88
104
|
path: string;
|
|
89
105
|
width: number;
|
|
90
106
|
height: number;
|
|
@@ -149,6 +165,7 @@ export const ARCameraView = forwardRef<ARCameraViewHandle, ARCameraViewProps>(
|
|
|
149
165
|
return native.takePhoto({
|
|
150
166
|
path: '',
|
|
151
167
|
quality: options.quality ?? 90,
|
|
168
|
+
orientation: options.orientation ?? 'portrait',
|
|
152
169
|
});
|
|
153
170
|
},
|
|
154
171
|
startRecording: (options) => {
|