react-native-image-stitcher 0.15.2 → 0.16.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.
- package/CHANGELOG.md +171 -1
- package/README.md +131 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +1122 -132
- package/cpp/stitcher.hpp +62 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +209 -12
- package/dist/camera/Camera.js +575 -36
- package/dist/camera/CameraView.js +35 -16
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
- package/dist/camera/CaptureMemoryPill.d.ts +24 -8
- package/dist/camera/CaptureMemoryPill.js +37 -12
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +19 -1
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +135 -0
- package/dist/camera/RectCropPreview.js +370 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +74 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +945 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
- package/src/camera/CaptureMemoryPill.tsx +50 -12
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +27 -7
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +638 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +74 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the pure `isBelowMemThreshold` classifier.
|
|
4
|
-
*
|
|
5
|
-
* The other two exports (`getPhysicalMemoryBytes` and `isLowMemDevice`)
|
|
6
|
-
* read the React Native bridge and can only be exercised on a real
|
|
7
|
-
* device. This file covers the threshold logic exhaustively so the
|
|
8
|
-
* classification rule is unit-tested without needing the RN runtime.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
LOW_MEM_THRESHOLD_BYTES,
|
|
13
|
-
isBelowMemThreshold,
|
|
14
|
-
} from '../lowMemDevice';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
describe('isBelowMemThreshold', () => {
|
|
18
|
-
it('returns true for positive byte counts below the threshold', () => {
|
|
19
|
-
expect(isBelowMemThreshold(1)).toBe(true);
|
|
20
|
-
expect(isBelowMemThreshold(1024)).toBe(true);
|
|
21
|
-
expect(isBelowMemThreshold(LOW_MEM_THRESHOLD_BYTES - 1)).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('returns false at exactly the threshold (strict < comparison)', () => {
|
|
25
|
-
expect(isBelowMemThreshold(LOW_MEM_THRESHOLD_BYTES)).toBe(false);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('returns false for byte counts above the threshold', () => {
|
|
29
|
-
expect(isBelowMemThreshold(LOW_MEM_THRESHOLD_BYTES + 1)).toBe(false);
|
|
30
|
-
expect(isBelowMemThreshold(4 * 1024 * 1024 * 1024)).toBe(false);
|
|
31
|
-
expect(isBelowMemThreshold(Number.MAX_SAFE_INTEGER)).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('returns false for zero (unknown — safe default to high-quality combo)', () => {
|
|
35
|
-
expect(isBelowMemThreshold(0)).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns false for negative values (defensive)', () => {
|
|
39
|
-
expect(isBelowMemThreshold(-1)).toBe(false);
|
|
40
|
-
expect(isBelowMemThreshold(-LOW_MEM_THRESHOLD_BYTES)).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('returns false for non-finite values (NaN / Infinity)', () => {
|
|
44
|
-
expect(isBelowMemThreshold(Number.NaN)).toBe(false);
|
|
45
|
-
expect(isBelowMemThreshold(Number.POSITIVE_INFINITY)).toBe(false);
|
|
46
|
-
expect(isBelowMemThreshold(Number.NEGATIVE_INFINITY)).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('threshold is exactly 2 GB', () => {
|
|
50
|
-
expect(LOW_MEM_THRESHOLD_BYTES).toBe(2 * 1024 * 1024 * 1024);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for `selectCaptureDevice` + `zoomForLens` — the pure
|
|
4
|
-
* capability-aware back-camera selection (v0.13.2).
|
|
5
|
-
*
|
|
6
|
-
* Covers the device matrix from the plan
|
|
7
|
-
* (docs/plans/2026-06-01-v0.13.2-multilens-device-selection.md),
|
|
8
|
-
* including the critical edge cases:
|
|
9
|
-
* - ultra-wide ONLY inside a multi-cam device (Symptom 1 fix)
|
|
10
|
-
* - ultra-wide ONLY as a standalone device (Android; must NOT regress)
|
|
11
|
-
* - ultra-wide present BOTH ways (prefer multicam)
|
|
12
|
-
*
|
|
13
|
-
* Pure — no mocks needed; we build synthetic DeviceLike lists.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
selectCaptureDevice,
|
|
18
|
-
zoomForLens,
|
|
19
|
-
type DeviceLike,
|
|
20
|
-
} from '../selectCaptureDevice';
|
|
21
|
-
|
|
22
|
-
// ── Synthetic device builders ───────────────────────────────────────
|
|
23
|
-
let idCounter = 0;
|
|
24
|
-
function dev(partial: Partial<DeviceLike>): DeviceLike {
|
|
25
|
-
idCounter += 1;
|
|
26
|
-
return {
|
|
27
|
-
id: `dev-${idCounter}`,
|
|
28
|
-
position: 'back',
|
|
29
|
-
physicalDevices: ['wide-angle-camera'],
|
|
30
|
-
isMultiCam: false,
|
|
31
|
-
hasTorch: true,
|
|
32
|
-
minZoom: 1,
|
|
33
|
-
neutralZoom: 1,
|
|
34
|
-
maxZoom: 10,
|
|
35
|
-
...partial,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const tripleCam = (p: Partial<DeviceLike> = {}) =>
|
|
40
|
-
dev({
|
|
41
|
-
physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
|
|
42
|
-
isMultiCam: true,
|
|
43
|
-
hasTorch: true,
|
|
44
|
-
minZoom: 0.5,
|
|
45
|
-
neutralZoom: 1,
|
|
46
|
-
maxZoom: 30,
|
|
47
|
-
...p,
|
|
48
|
-
});
|
|
49
|
-
const dualWide = (p: Partial<DeviceLike> = {}) =>
|
|
50
|
-
dev({
|
|
51
|
-
physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera'],
|
|
52
|
-
isMultiCam: true,
|
|
53
|
-
hasTorch: true,
|
|
54
|
-
minZoom: 0.5,
|
|
55
|
-
neutralZoom: 1,
|
|
56
|
-
maxZoom: 6,
|
|
57
|
-
...p,
|
|
58
|
-
});
|
|
59
|
-
const standaloneWide = (p: Partial<DeviceLike> = {}) =>
|
|
60
|
-
dev({ physicalDevices: ['wide-angle-camera'], isMultiCam: false, hasTorch: true, ...p });
|
|
61
|
-
const standaloneUltraWide = (p: Partial<DeviceLike> = {}) =>
|
|
62
|
-
dev({ physicalDevices: ['ultra-wide-angle-camera'], isMultiCam: false, hasTorch: false, ...p });
|
|
63
|
-
|
|
64
|
-
describe('selectCaptureDevice', () => {
|
|
65
|
-
it('picks the MULTICAM device when one spans wide + ultra-wide (triple cam)', () => {
|
|
66
|
-
const triple = tripleCam();
|
|
67
|
-
const sel = selectCaptureDevice([triple, standaloneWide(), standaloneUltraWide()]);
|
|
68
|
-
expect(sel.mode).toBe('multicam');
|
|
69
|
-
expect(sel.device).toBe(triple);
|
|
70
|
-
expect(sel.ultraWideDevice).toBeNull();
|
|
71
|
-
expect(sel.has0_5x).toBe(true);
|
|
72
|
-
expect(sel.hasTorch).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('picks the MULTICAM device for a dual-wide grouping', () => {
|
|
76
|
-
const dual = dualWide();
|
|
77
|
-
const sel = selectCaptureDevice([dual, standaloneWide()]);
|
|
78
|
-
expect(sel.mode).toBe('multicam');
|
|
79
|
-
expect(sel.device).toBe(dual);
|
|
80
|
-
expect(sel.has0_5x).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('SYMPTOM 1 FIX: ultra-wide ONLY in a multi-cam device → multicam (not wide fallback)', () => {
|
|
84
|
-
// The exact bug: a phone where ultra-wide is bundled in a multicam
|
|
85
|
-
// device and there is NO standalone ultra-wide. Old single-lens
|
|
86
|
-
// filter fell back to wide-angle; we must pick the multicam.
|
|
87
|
-
const dual = dualWide();
|
|
88
|
-
const wide = standaloneWide();
|
|
89
|
-
const sel = selectCaptureDevice([wide, dual]);
|
|
90
|
-
expect(sel.mode).toBe('multicam');
|
|
91
|
-
expect(sel.device).toBe(dual);
|
|
92
|
-
expect(sel.has0_5x).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('EDGE: ultra-wide ONLY as a standalone device (Android) → standalone-uw (no regression)', () => {
|
|
96
|
-
// No multicam grouping at all. Must still expose 0.5× via the
|
|
97
|
-
// standalone ultra-wide, mounting the wide-angle as primary.
|
|
98
|
-
const wide = standaloneWide();
|
|
99
|
-
const uw = standaloneUltraWide();
|
|
100
|
-
const sel = selectCaptureDevice([wide, uw]);
|
|
101
|
-
expect(sel.mode).toBe('standalone-uw');
|
|
102
|
-
expect(sel.device).toBe(wide); // primary = torch-bearing wide
|
|
103
|
-
expect(sel.ultraWideDevice).toBe(uw);
|
|
104
|
-
expect(sel.has0_5x).toBe(true);
|
|
105
|
-
expect(sel.hasTorch).toBe(true); // the 1× mount has a torch
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('EDGE: ultra-wide present BOTH standalone AND in multicam → prefer multicam', () => {
|
|
109
|
-
const dual = dualWide();
|
|
110
|
-
const wide = standaloneWide();
|
|
111
|
-
const uw = standaloneUltraWide();
|
|
112
|
-
const sel = selectCaptureDevice([uw, wide, dual]);
|
|
113
|
-
expect(sel.mode).toBe('multicam');
|
|
114
|
-
expect(sel.device).toBe(dual);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('wide-angle ONLY (no ultra-wide anywhere) → wide-only, no 0.5×', () => {
|
|
118
|
-
const wide = standaloneWide();
|
|
119
|
-
const sel = selectCaptureDevice([wide]);
|
|
120
|
-
expect(sel.mode).toBe('wide-only');
|
|
121
|
-
expect(sel.device).toBe(wide);
|
|
122
|
-
expect(sel.has0_5x).toBe(false);
|
|
123
|
-
expect(sel.ultraWideDevice).toBeNull();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('prefers a TORCH-bearing multicam device over a torchless one', () => {
|
|
127
|
-
const noTorch = dualWide({ hasTorch: false });
|
|
128
|
-
const withTorch = tripleCam({ hasTorch: true });
|
|
129
|
-
const sel = selectCaptureDevice([noTorch, withTorch]);
|
|
130
|
-
expect(sel.mode).toBe('multicam');
|
|
131
|
-
expect(sel.device).toBe(withTorch);
|
|
132
|
-
expect(sel.hasTorch).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('ignores front-facing devices', () => {
|
|
136
|
-
const front = dev({ position: 'front', physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera'], isMultiCam: true });
|
|
137
|
-
const backWide = standaloneWide();
|
|
138
|
-
const sel = selectCaptureDevice([front, backWide]);
|
|
139
|
-
expect(sel.mode).toBe('wide-only'); // front multicam doesn't count
|
|
140
|
-
expect(sel.device).toBe(backWide);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('empty device list → null device, wide-only, no 0.5×', () => {
|
|
144
|
-
const sel = selectCaptureDevice([]);
|
|
145
|
-
expect(sel.device).toBeNull();
|
|
146
|
-
expect(sel.mode).toBe('wide-only');
|
|
147
|
-
expect(sel.has0_5x).toBe(false);
|
|
148
|
-
expect(sel.hasTorch).toBe(false);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('standalone-uw: primary prefers a torch-bearing wide when multiple wides exist', () => {
|
|
152
|
-
const wideNoTorch = standaloneWide({ hasTorch: false });
|
|
153
|
-
const wideTorch = standaloneWide({ hasTorch: true });
|
|
154
|
-
const uw = standaloneUltraWide();
|
|
155
|
-
const sel = selectCaptureDevice([wideNoTorch, uw, wideTorch]);
|
|
156
|
-
expect(sel.mode).toBe('standalone-uw');
|
|
157
|
-
expect(sel.device).toBe(wideTorch);
|
|
158
|
-
expect(sel.hasTorch).toBe(true);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('S24: multicam LISTS ultra-wide but zoom cannot reach it (minZoom~1) + standalone uw swaps', () => {
|
|
162
|
-
// Samsung/Camera2: the logical device lists the ultra-wide but its zoom
|
|
163
|
-
// range starts at 1.0, so zoom cannot reach it. A separate ultra-wide id
|
|
164
|
-
// exists -> keep the multicam for 1x (torch) and swap to the standalone
|
|
165
|
-
// ultra-wide on 0.5x.
|
|
166
|
-
const multicamNoReach = dualWide({ minZoom: 1, hasTorch: true });
|
|
167
|
-
const uw = standaloneUltraWide();
|
|
168
|
-
const sel = selectCaptureDevice([multicamNoReach, uw]);
|
|
169
|
-
expect(sel.mode).toBe('standalone-uw');
|
|
170
|
-
expect(sel.device).toBe(multicamNoReach); // 1x primary keeps the torch
|
|
171
|
-
expect(sel.ultraWideDevice).toBe(uw); // 0.5x swaps to the real ultra-wide
|
|
172
|
-
expect(sel.has0_5x).toBe(true);
|
|
173
|
-
expect(sel.hasTorch).toBe(true);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('multicam lists ultra-wide, zoom cannot reach (minZoom~1), NO standalone uw -> hide', () => {
|
|
177
|
-
// The ultra-wide exists ONLY inside a non-zoomable logical device with no
|
|
178
|
-
// separate id to swap to -> undeliverable -> hide the chooser.
|
|
179
|
-
const multicamNoReach = dualWide({ minZoom: 1 });
|
|
180
|
-
const sel = selectCaptureDevice([multicamNoReach]);
|
|
181
|
-
expect(sel.mode).toBe('wide-only');
|
|
182
|
-
expect(sel.has0_5x).toBe(false);
|
|
183
|
-
expect(sel.ultraWideDevice).toBeNull();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('minZoom threshold: <=0.7 zoom-switches, >0.7 falls through to swap', () => {
|
|
187
|
-
const atThreshold = dualWide({ minZoom: 0.7 });
|
|
188
|
-
expect(selectCaptureDevice([atThreshold]).mode).toBe('multicam');
|
|
189
|
-
const aboveThreshold = dualWide({ minZoom: 0.71 });
|
|
190
|
-
const uw = standaloneUltraWide();
|
|
191
|
-
expect(selectCaptureDevice([aboveThreshold, uw]).mode).toBe('standalone-uw');
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe('zoomForLens (multicam lens→zoom mapping)', () => {
|
|
196
|
-
const d = { minZoom: 0.5, neutralZoom: 1 };
|
|
197
|
-
|
|
198
|
-
it('maps 0.5× to the device minZoom (ultra-wide end)', () => {
|
|
199
|
-
expect(zoomForLens(d, '0.5x')).toBe(0.5);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('maps 1× to the device neutralZoom (wide-angle baseline)', () => {
|
|
203
|
-
expect(zoomForLens(d, '1x')).toBe(1);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('handles a device whose neutralZoom differs from 1', () => {
|
|
207
|
-
expect(zoomForLens({ minZoom: 0.6, neutralZoom: 2 }, '1x')).toBe(2);
|
|
208
|
-
expect(zoomForLens({ minZoom: 0.6, neutralZoom: 2 }, '0.5x')).toBe(0.6);
|
|
209
|
-
});
|
|
210
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for `contentRotationDeg` — the pure rotation computation
|
|
4
|
-
* behind `useContentRotation`, which keeps control content (AR toggle,
|
|
5
|
-
* lens/zoom pill, flash, thumbnails) upright relative to gravity
|
|
6
|
-
* regardless of host portrait-lock state.
|
|
7
|
-
*
|
|
8
|
-
* Covers the full truth table from the hook's docstring plus the
|
|
9
|
-
* mid-rotation transients (jsLandscape=true with a non-landscape device
|
|
10
|
-
* reading, which can briefly happen while the OS catches up).
|
|
11
|
-
*
|
|
12
|
-
* Pure-TS test per jest.config.js. `useContentRotation` transitively
|
|
13
|
-
* imports `useDeviceOrientation` → `react-native-sensors` (an ES module
|
|
14
|
-
* the no-RN-preset jest infra can't parse), so stub it before importing
|
|
15
|
-
* the SUT. We only call the pure `contentRotationDeg` export.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
jest.mock('react-native-sensors', () => ({
|
|
19
|
-
accelerometer: { subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) },
|
|
20
|
-
gyroscope: { subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) },
|
|
21
|
-
setUpdateIntervalForType: jest.fn(),
|
|
22
|
-
SensorTypes: { accelerometer: 'accelerometer', gyroscope: 'gyroscope' },
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
import { contentRotationDeg } from '../useContentRotation';
|
|
26
|
-
|
|
27
|
-
describe('contentRotationDeg', () => {
|
|
28
|
-
// Locked-portrait host: jsLandscape is ALWAYS false (window dims stay
|
|
29
|
-
// portrait regardless of device tilt). The OS doesn't rotate the
|
|
30
|
-
// framebuffer, so content rotation must match device-physical for
|
|
31
|
-
// labels to read upright. THIS is the case task #5b targets.
|
|
32
|
-
|
|
33
|
-
it('locked-portrait + device-portrait → 0° (no-op)', () => {
|
|
34
|
-
expect(contentRotationDeg(false, 'portrait')).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('locked-portrait + device-landscape-left → 90° (CW)', () => {
|
|
38
|
-
expect(contentRotationDeg(false, 'landscape-left')).toBe(90);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('locked-portrait + device-landscape-right → -90° (CCW)', () => {
|
|
42
|
-
expect(contentRotationDeg(false, 'landscape-right')).toBe(-90);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('locked-portrait + device-upside-down → 180°', () => {
|
|
46
|
-
expect(contentRotationDeg(false, 'portrait-upside-down')).toBe(180);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Non-locked host + device-landscape: OS rotated the framebuffer for
|
|
50
|
-
// us; we must NOT double-rotate. Net rotation must be 0.
|
|
51
|
-
|
|
52
|
-
it('non-locked + device-landscape-left (jsLandscape=true) → 0°', () => {
|
|
53
|
-
expect(contentRotationDeg(true, 'landscape-left')).toBe(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('non-locked + device-landscape-right (jsLandscape=true) → 0°', () => {
|
|
57
|
-
expect(contentRotationDeg(true, 'landscape-right')).toBe(0);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('non-locked + device-portrait (jsLandscape=false) → 0°', () => {
|
|
61
|
-
expect(contentRotationDeg(false, 'portrait')).toBe(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Mid-rotation transients: jsLandscape=true with a non-landscape
|
|
65
|
-
// device reading. Falls through to 0 framebuffer rotation and
|
|
66
|
-
// applies device rotation directly; settles once the transient clears.
|
|
67
|
-
|
|
68
|
-
it('jsLandscape=true mid-rotation with device-portrait → 0°', () => {
|
|
69
|
-
expect(contentRotationDeg(true, 'portrait')).toBe(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('jsLandscape=true mid-rotation with device-upside-down → 180°', () => {
|
|
73
|
-
expect(contentRotationDeg(true, 'portrait-upside-down')).toBe(180);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('all returned values are in {0, 90, -90, 180} (no off-by-360°)', () => {
|
|
77
|
-
const orientations = [
|
|
78
|
-
'portrait',
|
|
79
|
-
'portrait-upside-down',
|
|
80
|
-
'landscape-left',
|
|
81
|
-
'landscape-right',
|
|
82
|
-
] as const;
|
|
83
|
-
for (const o of orientations) {
|
|
84
|
-
for (const jsl of [true, false]) {
|
|
85
|
-
expect([0, 90, -90, 180]).toContain(contentRotationDeg(jsl, o));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for `useOrientationDrift` — exercises the pure
|
|
4
|
-
* state-transition function `_computeDriftStateForTests` directly.
|
|
5
|
-
*
|
|
6
|
-
* Why not test the hook end-to-end via render: the lib's jest
|
|
7
|
-
* config is `preset: 'ts-jest'` + `testEnvironment: 'node'` — no
|
|
8
|
-
* React Native preset, no `@testing-library/react-native`. See the
|
|
9
|
-
* jest.config.js header comment: "If we ever add component-render
|
|
10
|
-
* tests we'd flip to the RN preset then." The component-render
|
|
11
|
-
* tests for `<OrientationDriftModal>`, `<PanoramaBandOverlay>`,
|
|
12
|
-
* `<ViewportCropOverlay>`, and `<Camera>` composition (all called
|
|
13
|
-
* out in the v0.12 plan) will all need that flip. Setting it up
|
|
14
|
-
* is grouped in Phase 5 of the plan (Tests) rather than scattered
|
|
15
|
-
* across each PR. For PR-1, the pure state-transition function
|
|
16
|
-
* carries the full behavioural contract — same approach
|
|
17
|
-
* `useThrottledFrameProcessor.test.ts` uses for its throttle gate.
|
|
18
|
-
*
|
|
19
|
-
* The 5 cases below cover the full state machine per the plan
|
|
20
|
-
* (lines 119, 277):
|
|
21
|
-
*
|
|
22
|
-
* (a) no change → not drifted
|
|
23
|
-
* (b) orientation changes during active=true → drifted
|
|
24
|
-
* (c) drift state survives further changes (latching)
|
|
25
|
-
* (d) inactive → captureOrientation undefined
|
|
26
|
-
* (e) active resets snapshot (false → true → false → true cycle)
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
// Mock `react-native-sensors` BEFORE importing the SUT. The hook
|
|
30
|
-
// itself transitively pulls in `useDeviceOrientation` which imports
|
|
31
|
-
// `accelerometer` from `react-native-sensors` — an ES module that
|
|
32
|
-
// jest can't parse without the RN preset (which jest.config.js
|
|
33
|
-
// intentionally avoids; see config header comment). We're only
|
|
34
|
-
// testing the pure transition function below, but TS imports are
|
|
35
|
-
// transitive so we still need to silence the chain.
|
|
36
|
-
jest.mock('react-native-sensors', () => ({
|
|
37
|
-
accelerometer: { subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) },
|
|
38
|
-
setUpdateIntervalForType: jest.fn(),
|
|
39
|
-
SensorTypes: { accelerometer: 'accelerometer' },
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
// eslint-disable-next-line import/first
|
|
43
|
-
import { _computeDriftStateForTests } from '../useOrientationDrift';
|
|
44
|
-
|
|
45
|
-
const INITIAL = { captureOrientation: undefined, drifted: false };
|
|
46
|
-
|
|
47
|
-
describe('_computeDriftStateForTests (useOrientationDrift core logic)', () => {
|
|
48
|
-
describe('(a) no change → not drifted', () => {
|
|
49
|
-
it('stays in initial state when active is false from the start', () => {
|
|
50
|
-
const next = _computeDriftStateForTests(INITIAL, false, 'portrait');
|
|
51
|
-
expect(next).toEqual({ captureOrientation: undefined, drifted: false });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('snapshots orientation when active flips true, drifted starts false', () => {
|
|
55
|
-
const next = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
56
|
-
expect(next).toEqual({ captureOrientation: 'portrait', drifted: false });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('stays clean when active=true and orientation does not change', () => {
|
|
60
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
61
|
-
const after2 = _computeDriftStateForTests(after1, true, 'portrait');
|
|
62
|
-
const after3 = _computeDriftStateForTests(after2, true, 'portrait');
|
|
63
|
-
expect(after3).toEqual({ captureOrientation: 'portrait', drifted: false });
|
|
64
|
-
// Reference equality: once steady, returns the prev ref so
|
|
65
|
-
// React's setState becomes a no-op (no re-render).
|
|
66
|
-
expect(after2).toBe(after1);
|
|
67
|
-
expect(after3).toBe(after2);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('(b) orientation changes during active=true → drifted', () => {
|
|
72
|
-
it('latches drifted=true when orientation changes mid-active', () => {
|
|
73
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
74
|
-
const after2 = _computeDriftStateForTests(after1, true, 'landscape-left');
|
|
75
|
-
expect(after2).toEqual({ captureOrientation: 'portrait', drifted: true });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('captures the ORIGINAL orientation in captureOrientation, not the new one', () => {
|
|
79
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
80
|
-
const after2 = _computeDriftStateForTests(after1, true, 'landscape-right');
|
|
81
|
-
// captureOrientation MUST remain the snapshot (portrait), not
|
|
82
|
-
// the current rotation — that's how the drift modal copy
|
|
83
|
-
// ("captured in PORTRAIT, now LANDSCAPE-RIGHT") works.
|
|
84
|
-
expect(after2.captureOrientation).toBe('portrait');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('detects drift to any of the 3 other orientations', () => {
|
|
88
|
-
const cases: Array<['portrait', 'portrait-upside-down' | 'landscape-left' | 'landscape-right']> = [
|
|
89
|
-
['portrait', 'portrait-upside-down'],
|
|
90
|
-
['portrait', 'landscape-left'],
|
|
91
|
-
['portrait', 'landscape-right'],
|
|
92
|
-
];
|
|
93
|
-
for (const [captured, drifted] of cases) {
|
|
94
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, captured);
|
|
95
|
-
const after2 = _computeDriftStateForTests(after1, true, drifted);
|
|
96
|
-
expect(after2.drifted).toBe(true);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('(c) drift state survives further changes (latching)', () => {
|
|
102
|
-
it('stays drifted even if the user rotates back to the captured orientation', () => {
|
|
103
|
-
// User rotates portrait → landscape (drift triggers) → portrait
|
|
104
|
-
// (back to original). The flag MUST stay latched. Rationale:
|
|
105
|
-
// the engine docstring says cross-mode capture is "best-effort,
|
|
106
|
-
// not supported" — a brief rotation pollutes the buffer even
|
|
107
|
-
// if the user rotates back, so the safe action is decisive
|
|
108
|
-
// abandonment regardless of post-detection orientation.
|
|
109
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
110
|
-
const after2 = _computeDriftStateForTests(after1, true, 'landscape-left');
|
|
111
|
-
const after3 = _computeDriftStateForTests(after2, true, 'portrait');
|
|
112
|
-
expect(after3).toEqual({ captureOrientation: 'portrait', drifted: true });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('stays drifted across multiple subsequent orientation changes', () => {
|
|
116
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
117
|
-
const after2 = _computeDriftStateForTests(after1, true, 'landscape-left');
|
|
118
|
-
const after3 = _computeDriftStateForTests(after2, true, 'landscape-right');
|
|
119
|
-
const after4 = _computeDriftStateForTests(after3, true, 'portrait-upside-down');
|
|
120
|
-
expect(after4.drifted).toBe(true);
|
|
121
|
-
expect(after4.captureOrientation).toBe('portrait');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('(d) inactive → captureOrientation undefined', () => {
|
|
126
|
-
it('clears the snapshot when active flips back to false', () => {
|
|
127
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
128
|
-
const after2 = _computeDriftStateForTests(after1, false, 'portrait');
|
|
129
|
-
expect(after2).toEqual({ captureOrientation: undefined, drifted: false });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('clears the drift flag when active flips back to false', () => {
|
|
133
|
-
const after1 = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
134
|
-
const after2 = _computeDriftStateForTests(after1, true, 'landscape-left');
|
|
135
|
-
expect(after2.drifted).toBe(true);
|
|
136
|
-
const after3 = _computeDriftStateForTests(after2, false, 'landscape-left');
|
|
137
|
-
expect(after3).toEqual({ captureOrientation: undefined, drifted: false });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('is idempotent — no state change when inactive and already clear', () => {
|
|
141
|
-
const after1 = _computeDriftStateForTests(INITIAL, false, 'portrait');
|
|
142
|
-
const after2 = _computeDriftStateForTests(after1, false, 'landscape-left');
|
|
143
|
-
// Same ref → setState becomes a no-op.
|
|
144
|
-
expect(after2).toBe(after1);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('(e) active resets snapshot', () => {
|
|
149
|
-
it('re-snapshots on a fresh active cycle (false → true → false → true)', () => {
|
|
150
|
-
// Cycle 1: capture in portrait, drift.
|
|
151
|
-
const c1a = _computeDriftStateForTests(INITIAL, true, 'portrait');
|
|
152
|
-
const c1b = _computeDriftStateForTests(c1a, true, 'landscape-left');
|
|
153
|
-
expect(c1b).toEqual({ captureOrientation: 'portrait', drifted: true });
|
|
154
|
-
|
|
155
|
-
// Stop the capture.
|
|
156
|
-
const cleared = _computeDriftStateForTests(c1b, false, 'landscape-left');
|
|
157
|
-
expect(cleared).toEqual({ captureOrientation: undefined, drifted: false });
|
|
158
|
-
|
|
159
|
-
// Cycle 2: re-capture, now in landscape-left. Snapshot
|
|
160
|
-
// should be landscape-left, NOT carry over the old portrait.
|
|
161
|
-
const c2a = _computeDriftStateForTests(cleared, true, 'landscape-left');
|
|
162
|
-
expect(c2a).toEqual({ captureOrientation: 'landscape-left', drifted: false });
|
|
163
|
-
|
|
164
|
-
// And staying in landscape-left should not drift.
|
|
165
|
-
const c2b = _computeDriftStateForTests(c2a, true, 'landscape-left');
|
|
166
|
-
expect(c2b.drifted).toBe(false);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
});
|