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,276 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit + integration coverage for the v0.10.0 PR B refine-progress
|
|
4
|
-
* lifecycle on the `IncrementalStateUpdate` channel.
|
|
5
|
-
*
|
|
6
|
-
* What this test is for:
|
|
7
|
-
*
|
|
8
|
-
* - Contract: native emits a 4-stage sequence
|
|
9
|
-
* `validating → stitching → writing → done` (and the failure
|
|
10
|
-
* variant `validating → error`) with `refineStage` /
|
|
11
|
-
* `refineProgress` / `refineFrames` / `refineError` keys.
|
|
12
|
-
* - Regression: catches a future renamer of any of those keys
|
|
13
|
-
* (subscribeIncrementalState would silently deliver `undefined`
|
|
14
|
-
* for the missing fields, and the host's progress pill would
|
|
15
|
-
* stop rendering — exactly the bug class we hit on iOS in PR B
|
|
16
|
-
* before the bridgeless-interop fix).
|
|
17
|
-
*
|
|
18
|
-
* What this test is NOT for:
|
|
19
|
-
*
|
|
20
|
-
* - Exercising the real native bridge — RCTEventEmitter under RN
|
|
21
|
-
* bridgeless interop can only be tested on-device. The bug we
|
|
22
|
-
* fixed in PR B (sendEvent silently no-ops for certain body
|
|
23
|
-
* shapes) is verified via the manual smoke test recorded in
|
|
24
|
-
* CHANGELOG.md "Fixed — v0.10.0 PR B (iOS)". This file pins
|
|
25
|
-
* the JS-side contract that bridge fix has to satisfy.
|
|
26
|
-
*
|
|
27
|
-
* Mock surface: per-test `jest.mock('react-native', ...)` so the
|
|
28
|
-
* shared `jest.mocks/react-native.js` stays minimal (per the comment
|
|
29
|
-
* in that file). We stub `NativeModules.IncrementalStitcher` and
|
|
30
|
-
* `NativeEventEmitter` together because `subscribeIncrementalState`
|
|
31
|
-
* wires them together internally.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import type { IncrementalState } from '../incremental';
|
|
35
|
-
|
|
36
|
-
// Hand-rolled event-emitter fake we can drive synchronously from
|
|
37
|
-
// tests. Modelled on RN's NativeEventEmitter shape: addListener
|
|
38
|
-
// returns an object with a `.remove()` method.
|
|
39
|
-
type Listener = (state: IncrementalState) => void;
|
|
40
|
-
|
|
41
|
-
class FakeNativeEventEmitter {
|
|
42
|
-
private listeners: Map<string, Set<Listener>> = new Map();
|
|
43
|
-
|
|
44
|
-
constructor(_nativeModule: unknown) {
|
|
45
|
-
// No-op: real RN reads addListener/removeListeners off the
|
|
46
|
-
// native module for the listener-count contract; we don't.
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
addListener(eventType: string, listener: Listener) {
|
|
50
|
-
let set = this.listeners.get(eventType);
|
|
51
|
-
if (!set) {
|
|
52
|
-
set = new Set();
|
|
53
|
-
this.listeners.set(eventType, set);
|
|
54
|
-
}
|
|
55
|
-
set.add(listener);
|
|
56
|
-
return {
|
|
57
|
-
remove: () => {
|
|
58
|
-
set!.delete(listener);
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Test-only helper: drive an event into all subscribers.
|
|
64
|
-
_emit(eventType: string, state: IncrementalState) {
|
|
65
|
-
const set = this.listeners.get(eventType);
|
|
66
|
-
if (!set) return;
|
|
67
|
-
for (const listener of set) {
|
|
68
|
-
listener(state);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Shared emitter handle the per-test setup writes its asserts
|
|
74
|
-
// against. The mock factory below has to construct via `new`, so we
|
|
75
|
-
// stash the latest instance here for the test to drive.
|
|
76
|
-
let lastEmitter: FakeNativeEventEmitter | null = null;
|
|
77
|
-
|
|
78
|
-
jest.mock('react-native', () => ({
|
|
79
|
-
NativeModules: {
|
|
80
|
-
IncrementalStitcher: {
|
|
81
|
-
// RCTEventEmitter / NativeEventEmitter contract — RN's runtime
|
|
82
|
-
// calls these when JS subscribes / unsubscribes so the native
|
|
83
|
-
// side can track listener count. We just stub them.
|
|
84
|
-
addListener: jest.fn(),
|
|
85
|
-
removeListeners: jest.fn(),
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
NativeEventEmitter: jest.fn().mockImplementation((nativeModule: unknown) => {
|
|
89
|
-
lastEmitter = new FakeNativeEventEmitter(nativeModule);
|
|
90
|
-
return lastEmitter;
|
|
91
|
-
}),
|
|
92
|
-
Platform: { OS: 'ios', select: (spec: { ios?: unknown; default?: unknown }) => spec.ios ?? spec.default },
|
|
93
|
-
}));
|
|
94
|
-
|
|
95
|
-
// Import AFTER jest.mock so the SUT picks up the mocked module.
|
|
96
|
-
import { subscribeIncrementalState } from '../incremental';
|
|
97
|
-
|
|
98
|
-
// Build the base state shape the native side emits — matches the
|
|
99
|
-
// fields IncrementalStateObject.asDictionary() includes on iOS and
|
|
100
|
-
// Arguments.createMap() includes on Android.
|
|
101
|
-
function makeBaseState(): IncrementalState {
|
|
102
|
-
return {
|
|
103
|
-
width: 1920,
|
|
104
|
-
height: 1080,
|
|
105
|
-
acceptedCount: 3,
|
|
106
|
-
outcome: 8, // acceptedHigh
|
|
107
|
-
confidence: 0.92,
|
|
108
|
-
overlapPercent: 18.5,
|
|
109
|
-
processingMs: 0,
|
|
110
|
-
isLandscape: false,
|
|
111
|
-
paintedExtent: 1920,
|
|
112
|
-
panExtent: 1920,
|
|
113
|
-
keyframeMax: 0,
|
|
114
|
-
} as IncrementalState;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
describe('subscribeIncrementalState — refine progress lifecycle (v0.10.0 PR B)', () => {
|
|
118
|
-
beforeEach(() => {
|
|
119
|
-
lastEmitter = null;
|
|
120
|
-
jest.clearAllMocks();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('returns null when the native IncrementalStitcher module is missing', () => {
|
|
124
|
-
// Temporarily blank the module.
|
|
125
|
-
const RN = jest.requireMock('react-native') as { NativeModules: Record<string, unknown> };
|
|
126
|
-
const saved = RN.NativeModules.IncrementalStitcher;
|
|
127
|
-
RN.NativeModules.IncrementalStitcher = undefined;
|
|
128
|
-
try {
|
|
129
|
-
expect(subscribeIncrementalState(() => {})).toBeNull();
|
|
130
|
-
} finally {
|
|
131
|
-
RN.NativeModules.IncrementalStitcher = saved;
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('returns an EmitterSubscription when subscribed; remove() stops delivery', () => {
|
|
136
|
-
const events: IncrementalState[] = [];
|
|
137
|
-
const sub = subscribeIncrementalState((s) => events.push(s));
|
|
138
|
-
expect(sub).not.toBeNull();
|
|
139
|
-
expect(lastEmitter).not.toBeNull();
|
|
140
|
-
|
|
141
|
-
lastEmitter!._emit('IncrementalStateUpdate', {
|
|
142
|
-
...makeBaseState(),
|
|
143
|
-
refineStage: 'validating',
|
|
144
|
-
refineProgress: 0.05,
|
|
145
|
-
refineFrames: 3,
|
|
146
|
-
} as IncrementalState);
|
|
147
|
-
expect(events).toHaveLength(1);
|
|
148
|
-
|
|
149
|
-
sub!.remove();
|
|
150
|
-
lastEmitter!._emit('IncrementalStateUpdate', {
|
|
151
|
-
...makeBaseState(),
|
|
152
|
-
refineStage: 'done',
|
|
153
|
-
refineProgress: 1.0,
|
|
154
|
-
refineFrames: 3,
|
|
155
|
-
} as IncrementalState);
|
|
156
|
-
expect(events).toHaveLength(1); // unchanged after remove()
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('happy-path: delivers validating → stitching → writing → done in order with correct refineStage', () => {
|
|
160
|
-
const stages: Array<{ stage: string | undefined; progress: number | undefined }> = [];
|
|
161
|
-
subscribeIncrementalState((s) => {
|
|
162
|
-
stages.push({ stage: s.refineStage, progress: s.refineProgress });
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const sequence: Array<Pick<IncrementalState, 'refineStage' | 'refineProgress' | 'refineFrames'>> = [
|
|
166
|
-
{ refineStage: 'validating', refineProgress: 0.05, refineFrames: 3 },
|
|
167
|
-
{ refineStage: 'stitching', refineProgress: 0.10, refineFrames: 3 },
|
|
168
|
-
{ refineStage: 'writing', refineProgress: 0.90, refineFrames: 3 },
|
|
169
|
-
{ refineStage: 'done', refineProgress: 1.00, refineFrames: 3 },
|
|
170
|
-
];
|
|
171
|
-
for (const ev of sequence) {
|
|
172
|
-
lastEmitter!._emit('IncrementalStateUpdate', { ...makeBaseState(), ...ev } as IncrementalState);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
expect(stages).toEqual([
|
|
176
|
-
{ stage: 'validating', progress: 0.05 },
|
|
177
|
-
{ stage: 'stitching', progress: 0.10 },
|
|
178
|
-
{ stage: 'writing', progress: 0.90 },
|
|
179
|
-
{ stage: 'done', progress: 1.00 },
|
|
180
|
-
]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('refineProgress is non-decreasing across the happy-path sequence (monotonicity contract)', () => {
|
|
184
|
-
const progresses: number[] = [];
|
|
185
|
-
subscribeIncrementalState((s) => {
|
|
186
|
-
if (s.refineProgress !== undefined) progresses.push(s.refineProgress);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
for (const p of [0.05, 0.10, 0.90, 1.00]) {
|
|
190
|
-
lastEmitter!._emit('IncrementalStateUpdate', {
|
|
191
|
-
...makeBaseState(),
|
|
192
|
-
refineStage: 'stitching', // stage is irrelevant for this assertion
|
|
193
|
-
refineProgress: p,
|
|
194
|
-
refineFrames: 3,
|
|
195
|
-
} as IncrementalState);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
expect(progresses).toEqual([0.05, 0.10, 0.90, 1.00]);
|
|
199
|
-
for (let i = 1; i < progresses.length; i++) {
|
|
200
|
-
expect(progresses[i]).toBeGreaterThanOrEqual(progresses[i - 1]);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('failure-path: validating → error carries refineError; no further stages emitted', () => {
|
|
205
|
-
const events: IncrementalState[] = [];
|
|
206
|
-
subscribeIncrementalState((s) => events.push(s));
|
|
207
|
-
|
|
208
|
-
lastEmitter!._emit('IncrementalStateUpdate', {
|
|
209
|
-
...makeBaseState(),
|
|
210
|
-
refineStage: 'validating',
|
|
211
|
-
refineProgress: 0.05,
|
|
212
|
-
refineFrames: 3,
|
|
213
|
-
} as IncrementalState);
|
|
214
|
-
lastEmitter!._emit('IncrementalStateUpdate', {
|
|
215
|
-
...makeBaseState(),
|
|
216
|
-
refineStage: 'error',
|
|
217
|
-
refineProgress: 1.0,
|
|
218
|
-
refineFrames: 3,
|
|
219
|
-
refineError: 'INVALID_FRAMES: missing JPEG at index 1',
|
|
220
|
-
} as IncrementalState);
|
|
221
|
-
|
|
222
|
-
expect(events).toHaveLength(2);
|
|
223
|
-
expect(events[0].refineStage).toBe('validating');
|
|
224
|
-
expect(events[1].refineStage).toBe('error');
|
|
225
|
-
expect(events[1].refineError).toBe('INVALID_FRAMES: missing JPEG at index 1');
|
|
226
|
-
// refineError is absent on the validating event.
|
|
227
|
-
expect(events[0].refineError).toBeUndefined();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('refineFrames passes through unchanged (regression guard for key rename)', () => {
|
|
231
|
-
const seen: Array<number | undefined> = [];
|
|
232
|
-
subscribeIncrementalState((s) => seen.push(s.refineFrames));
|
|
233
|
-
|
|
234
|
-
for (const n of [3, 5, 8]) {
|
|
235
|
-
lastEmitter!._emit('IncrementalStateUpdate', {
|
|
236
|
-
...makeBaseState(),
|
|
237
|
-
refineStage: 'stitching',
|
|
238
|
-
refineProgress: 0.5,
|
|
239
|
-
refineFrames: n,
|
|
240
|
-
} as IncrementalState);
|
|
241
|
-
}
|
|
242
|
-
expect(seen).toEqual([3, 5, 8]);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('live (non-refine) state events leave refine fields undefined', () => {
|
|
246
|
-
// Asserts that the contract is "refine fields are only populated
|
|
247
|
-
// during a refine call" — so the example app's `if
|
|
248
|
-
// (s.refineStage === undefined) return;` short-circuit is sound.
|
|
249
|
-
const events: IncrementalState[] = [];
|
|
250
|
-
subscribeIncrementalState((s) => events.push(s));
|
|
251
|
-
|
|
252
|
-
lastEmitter!._emit('IncrementalStateUpdate', makeBaseState());
|
|
253
|
-
expect(events).toHaveLength(1);
|
|
254
|
-
expect(events[0].refineStage).toBeUndefined();
|
|
255
|
-
expect(events[0].refineProgress).toBeUndefined();
|
|
256
|
-
expect(events[0].refineFrames).toBeUndefined();
|
|
257
|
-
expect(events[0].refineError).toBeUndefined();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('subscribes on the correct channel name "IncrementalStateUpdate" (cross-platform contract)', () => {
|
|
261
|
-
// If anyone renames the event constant on either side, the
|
|
262
|
-
// subscriber stops receiving events. Pin the literal here.
|
|
263
|
-
let receivedOnRight = false;
|
|
264
|
-
let receivedOnWrong = false;
|
|
265
|
-
subscribeIncrementalState(() => {
|
|
266
|
-
receivedOnRight = true;
|
|
267
|
-
});
|
|
268
|
-
// Fire on a deliberately-wrong channel — should NOT deliver.
|
|
269
|
-
lastEmitter!._emit('SomeOtherChannel', makeBaseState());
|
|
270
|
-
expect(receivedOnRight).toBe(false);
|
|
271
|
-
expect(receivedOnWrong).toBe(false);
|
|
272
|
-
// Fire on the right channel — should deliver.
|
|
273
|
-
lastEmitter!._emit('IncrementalStateUpdate', makeBaseState());
|
|
274
|
-
expect(receivedOnRight).toBe(true);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for `useStitcherWorklet`.
|
|
4
|
-
*
|
|
5
|
-
* Coverage focus (v0.11.1):
|
|
6
|
-
*
|
|
7
|
-
* - **AR-source short-circuit.** The hook's docstring promises
|
|
8
|
-
* that AR-mode hosts can call `stitcher.call(frame)` from a
|
|
9
|
-
* single composed worklet body without per-mode branching; AR
|
|
10
|
-
* stitching runs natively via the AR-side dispatcher. Pre-
|
|
11
|
-
* v0.11.1 the code didn't enforce that — `stitcher.call` would
|
|
12
|
-
* invoke the vc Frame Processor plugin even on AR-source
|
|
13
|
-
* frames, which throws `getPropertyAsObject: property '__frame'
|
|
14
|
-
* is undefined` because AR frames are `StitcherFrameHostObject`
|
|
15
|
-
* instances and don't carry vc's JSI `Frame` proxy marker. The
|
|
16
|
-
* throw was caught silently by the per-worklet error handler in
|
|
17
|
-
* `RNSARWorkletRuntime.mm`, surfacing only as an `os_log` entry
|
|
18
|
-
* — invisible to JS, which is why composed hosts saw their
|
|
19
|
-
* post-`stitcher.call` lines (`fireFrameProcessorLog`,
|
|
20
|
-
* `runOnJS` callbacks) silently never execute in AR mode. Test
|
|
21
|
-
* 2 of `docs/v0.11.0-manual-verification-checklist.md`
|
|
22
|
-
* reproduced this on Ram's iPhone. This test pins the fix.
|
|
23
|
-
*
|
|
24
|
-
* - **vc-source happy path.** vc-source frames (and frames whose
|
|
25
|
-
* `source` is `undefined` — which is what vc's raw `Frame`
|
|
26
|
-
* looks like; the lib doesn't wrap vc frames in Phase 4a) MUST
|
|
27
|
-
* still invoke the plugin.
|
|
28
|
-
*
|
|
29
|
-
* ## Why mock React's hooks directly
|
|
30
|
-
*
|
|
31
|
-
* The hook owns state via `useState` (the JSI plugin handle) and
|
|
32
|
-
* side effects via `useEffect` (plugin acquisition retry loop + gyro
|
|
33
|
-
* subscription). The existing test pattern in this directory (see
|
|
34
|
-
* `useThrottledFrameProcessor.test.ts`) doesn't use a React renderer
|
|
35
|
-
* — instead it mocks the hooks the SUT calls so the SUT can be
|
|
36
|
-
* executed as a plain function. Same approach here: we mock
|
|
37
|
-
* `useState` to return a pre-resolved plugin, `useCallback` to
|
|
38
|
-
* return the function as-is, `useEffect` as a no-op (we don't need
|
|
39
|
-
* the plugin-acquisition retry or gyro for the call-routing test).
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
import type { StitcherFrame } from '../StitcherFrame';
|
|
43
|
-
|
|
44
|
-
// ─── Mock vision-camera ──────────────────────────────────────────
|
|
45
|
-
const pluginCallSpy = jest.fn();
|
|
46
|
-
const fakePlugin = { call: pluginCallSpy } as unknown as object;
|
|
47
|
-
|
|
48
|
-
jest.mock('react-native-vision-camera', () => ({
|
|
49
|
-
VisionCameraProxy: {
|
|
50
|
-
initFrameProcessorPlugin: jest.fn(() => fakePlugin),
|
|
51
|
-
},
|
|
52
|
-
}));
|
|
53
|
-
|
|
54
|
-
// ─── Mock react-native-worklets-core ─────────────────────────────
|
|
55
|
-
jest.mock('react-native-worklets-core', () => ({
|
|
56
|
-
useSharedValue: (initial: number) => ({ value: initial }),
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
// ─── Mock react-native-sensors ───────────────────────────────────
|
|
60
|
-
jest.mock('react-native-sensors', () => ({
|
|
61
|
-
gyroscope: { subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) },
|
|
62
|
-
setUpdateIntervalForType: jest.fn(),
|
|
63
|
-
SensorTypes: { gyroscope: 'gyroscope' },
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
|
-
// ─── Mock React's hooks so the SUT runs as a plain function ──────
|
|
67
|
-
//
|
|
68
|
-
// `useState` returns the plugin pre-resolved. `useCallback` returns
|
|
69
|
-
// the function identity (deps array ignored — we're not testing
|
|
70
|
-
// re-render semantics). `useEffect` is a no-op (no plugin retry,
|
|
71
|
-
// no gyro subscription). This lets us call the hook synchronously
|
|
72
|
-
// and exercise the worklet body via the returned `call` function.
|
|
73
|
-
jest.mock('react', () => {
|
|
74
|
-
const actual = jest.requireActual('react');
|
|
75
|
-
return {
|
|
76
|
-
...actual,
|
|
77
|
-
useState: <T,>(initial: T): [T, (next: T) => void] => {
|
|
78
|
-
// For the [plugin, setPlugin] tuple: return the fake plugin
|
|
79
|
-
// immediately rather than starting at `null`. This skips the
|
|
80
|
-
// plugin-acquisition retry path and lets `call` actually
|
|
81
|
-
// invoke `plugin.call(...)`.
|
|
82
|
-
const resolved = (initial === null ? fakePlugin : initial) as T;
|
|
83
|
-
return [resolved, () => {}];
|
|
84
|
-
},
|
|
85
|
-
useEffect: () => {},
|
|
86
|
-
useCallback: <T,>(fn: T): T => fn,
|
|
87
|
-
};
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// SUT — imported AFTER mocks so the hook sees them.
|
|
91
|
-
// eslint-disable-next-line import/first
|
|
92
|
-
import { useStitcherWorklet } from '../useStitcherWorklet';
|
|
93
|
-
|
|
94
|
-
describe('useStitcherWorklet', () => {
|
|
95
|
-
beforeEach(() => {
|
|
96
|
-
pluginCallSpy.mockReset();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('AR-source short-circuit (v0.11.1 fix)', () => {
|
|
100
|
-
it('does NOT invoke the vc plugin for AR-source frames', () => {
|
|
101
|
-
const { call } = useStitcherWorklet();
|
|
102
|
-
const arFrame: StitcherFrame = {
|
|
103
|
-
width: 1920,
|
|
104
|
-
height: 1080,
|
|
105
|
-
pixelFormat: 'yuv',
|
|
106
|
-
orientation: 'landscape-right',
|
|
107
|
-
timestamp: 0,
|
|
108
|
-
toArrayBuffer: () => new ArrayBuffer(0),
|
|
109
|
-
source: 'ar',
|
|
110
|
-
pose: { rotation: [0, 0, 0, 1] },
|
|
111
|
-
};
|
|
112
|
-
call(arFrame);
|
|
113
|
-
expect(pluginCallSpy).not.toHaveBeenCalled();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('does NOT invoke the vc plugin for AR-source frames even when called repeatedly', () => {
|
|
117
|
-
const { call } = useStitcherWorklet();
|
|
118
|
-
const arFrame: StitcherFrame = {
|
|
119
|
-
width: 1920,
|
|
120
|
-
height: 1080,
|
|
121
|
-
pixelFormat: 'yuv',
|
|
122
|
-
orientation: 'landscape-right',
|
|
123
|
-
timestamp: 0,
|
|
124
|
-
toArrayBuffer: () => new ArrayBuffer(0),
|
|
125
|
-
source: 'ar',
|
|
126
|
-
pose: { rotation: [0, 0, 0, 1] },
|
|
127
|
-
};
|
|
128
|
-
for (let i = 0; i < 30; i++) call(arFrame);
|
|
129
|
-
expect(pluginCallSpy).not.toHaveBeenCalled();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('vc-source happy path', () => {
|
|
134
|
-
it('invokes the vc plugin for vc-source frames', () => {
|
|
135
|
-
const { call } = useStitcherWorklet();
|
|
136
|
-
const vcFrame: StitcherFrame = {
|
|
137
|
-
width: 1920,
|
|
138
|
-
height: 1080,
|
|
139
|
-
pixelFormat: 'yuv',
|
|
140
|
-
orientation: 'landscape-right',
|
|
141
|
-
timestamp: 0,
|
|
142
|
-
toArrayBuffer: () => new ArrayBuffer(0),
|
|
143
|
-
source: 'vc',
|
|
144
|
-
pose: { rotation: [0, 0, 0, 1] },
|
|
145
|
-
};
|
|
146
|
-
call(vcFrame);
|
|
147
|
-
expect(pluginCallSpy).toHaveBeenCalledTimes(1);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('invokes the vc plugin for frames with undefined source (raw vc Frame)', () => {
|
|
151
|
-
// vc's raw `Frame` doesn't carry the `source` field — the lib's
|
|
152
|
-
// Phase 4a deferral means we don't wrap vc frames into
|
|
153
|
-
// `StitcherFrame`. The AR-source check must treat undefined
|
|
154
|
-
// as "not AR" to preserve the non-AR worklet path.
|
|
155
|
-
const { call } = useStitcherWorklet();
|
|
156
|
-
const rawVcFrame = {
|
|
157
|
-
width: 1920,
|
|
158
|
-
height: 1080,
|
|
159
|
-
pixelFormat: 'yuv',
|
|
160
|
-
orientation: 'landscape-right',
|
|
161
|
-
timestamp: 0,
|
|
162
|
-
toArrayBuffer: () => new ArrayBuffer(0),
|
|
163
|
-
// `source` intentionally absent
|
|
164
|
-
} as unknown as StitcherFrame;
|
|
165
|
-
call(rawVcFrame);
|
|
166
|
-
expect(pluginCallSpy).toHaveBeenCalledTimes(1);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('plugin.call payload shape', () => {
|
|
171
|
-
it('passes the frame + a numeric-intrinsics params object', () => {
|
|
172
|
-
const { call } = useStitcherWorklet();
|
|
173
|
-
const vcFrame: StitcherFrame = {
|
|
174
|
-
width: 1920,
|
|
175
|
-
height: 1080,
|
|
176
|
-
pixelFormat: 'yuv',
|
|
177
|
-
orientation: 'landscape-right',
|
|
178
|
-
timestamp: 0,
|
|
179
|
-
toArrayBuffer: () => new ArrayBuffer(0),
|
|
180
|
-
source: 'vc',
|
|
181
|
-
pose: { rotation: [0, 0, 0, 1] },
|
|
182
|
-
};
|
|
183
|
-
call(vcFrame);
|
|
184
|
-
expect(pluginCallSpy).toHaveBeenCalledWith(
|
|
185
|
-
vcFrame,
|
|
186
|
-
expect.objectContaining({
|
|
187
|
-
tx: 0, ty: 0, tz: 0,
|
|
188
|
-
qx: expect.any(Number),
|
|
189
|
-
qy: expect.any(Number),
|
|
190
|
-
qz: expect.any(Number),
|
|
191
|
-
qw: expect.any(Number),
|
|
192
|
-
fx: expect.any(Number),
|
|
193
|
-
fy: expect.any(Number),
|
|
194
|
-
cx: 960,
|
|
195
|
-
cy: 540,
|
|
196
|
-
imageWidth: 1920,
|
|
197
|
-
imageHeight: 1080,
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|