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.
Files changed (146) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/README.md +131 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
  4. package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
  5. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  6. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
  7. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  10. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  11. package/cpp/crop_quad.cpp +162 -0
  12. package/cpp/crop_quad.hpp +163 -0
  13. package/cpp/keyframe_gate.cpp +54 -15
  14. package/cpp/keyframe_gate.hpp +33 -0
  15. package/cpp/stitcher.cpp +1122 -132
  16. package/cpp/stitcher.hpp +62 -0
  17. package/cpp/warp_guard.hpp +212 -0
  18. package/dist/camera/Camera.d.ts +209 -12
  19. package/dist/camera/Camera.js +575 -36
  20. package/dist/camera/CameraView.js +35 -16
  21. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  22. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  23. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  24. package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
  25. package/dist/camera/CaptureMemoryPill.d.ts +24 -8
  26. package/dist/camera/CaptureMemoryPill.js +37 -12
  27. package/dist/camera/CapturePreview.js +2 -1
  28. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  29. package/dist/camera/CaptureStatusOverlay.js +22 -5
  30. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  31. package/dist/camera/LateralMotionModal.d.ts +85 -0
  32. package/dist/camera/LateralMotionModal.js +134 -0
  33. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  34. package/dist/camera/PanHowToOverlay.js +222 -0
  35. package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
  36. package/dist/camera/PanoramaBandOverlay.js +9 -3
  37. package/dist/camera/PanoramaSettings.d.ts +8 -6
  38. package/dist/camera/PanoramaSettings.js +19 -1
  39. package/dist/camera/PanoramaSettingsModal.js +4 -4
  40. package/dist/camera/RectCropPreview.d.ts +135 -0
  41. package/dist/camera/RectCropPreview.js +370 -0
  42. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  43. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  44. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  45. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  46. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  47. package/dist/camera/cameraErrorMessages.js +26 -10
  48. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  49. package/dist/camera/cameraGuidanceCopy.js +80 -0
  50. package/dist/camera/captureCountdown.d.ts +52 -0
  51. package/dist/camera/captureCountdown.js +76 -0
  52. package/dist/camera/captureWarnings.d.ts +90 -0
  53. package/dist/camera/captureWarnings.js +108 -0
  54. package/dist/camera/classifyStitchError.d.ts +30 -0
  55. package/dist/camera/classifyStitchError.js +42 -0
  56. package/dist/camera/cropGeometry.d.ts +136 -0
  57. package/dist/camera/cropGeometry.js +223 -0
  58. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  59. package/dist/camera/displayDecodeImageProps.js +29 -0
  60. package/dist/camera/guidanceGraphics.d.ts +58 -0
  61. package/dist/camera/guidanceGraphics.js +280 -0
  62. package/dist/camera/guidanceTokens.d.ts +54 -0
  63. package/dist/camera/guidanceTokens.js +58 -0
  64. package/dist/camera/panModeGate.d.ts +54 -0
  65. package/dist/camera/panModeGate.js +62 -0
  66. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  67. package/dist/camera/pickCaptureFormat.js +85 -0
  68. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  69. package/dist/camera/stitchDebugInfo.js +55 -0
  70. package/dist/camera/usePanMotion.d.ts +250 -0
  71. package/dist/camera/usePanMotion.js +451 -0
  72. package/dist/index.d.ts +24 -3
  73. package/dist/index.js +33 -2
  74. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  75. package/dist/stitching/computeInscribedRect.js +55 -0
  76. package/dist/stitching/cropQuad.d.ts +78 -0
  77. package/dist/stitching/cropQuad.js +116 -0
  78. package/dist/stitching/incremental.d.ts +74 -0
  79. package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
  80. package/dist/stitching/useIncrementalStitcher.js +7 -1
  81. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
  82. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
  83. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
  84. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  85. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  86. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  87. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
  88. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  89. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  90. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  91. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  92. package/package.json +5 -1
  93. package/src/camera/Camera.tsx +945 -47
  94. package/src/camera/CameraView.tsx +48 -16
  95. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  96. package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
  97. package/src/camera/CaptureMemoryPill.tsx +50 -12
  98. package/src/camera/CapturePreview.tsx +5 -0
  99. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  100. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  101. package/src/camera/LateralMotionModal.tsx +199 -0
  102. package/src/camera/PanHowToOverlay.tsx +246 -0
  103. package/src/camera/PanoramaBandOverlay.tsx +9 -1
  104. package/src/camera/PanoramaSettings.ts +27 -7
  105. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  106. package/src/camera/RectCropPreview.tsx +638 -0
  107. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  108. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  109. package/src/camera/cameraErrorMessages.ts +39 -2
  110. package/src/camera/cameraGuidanceCopy.ts +145 -0
  111. package/src/camera/captureCountdown.ts +83 -0
  112. package/src/camera/captureWarnings.ts +190 -0
  113. package/src/camera/classifyStitchError.ts +68 -0
  114. package/src/camera/cropGeometry.ts +268 -0
  115. package/src/camera/displayDecodeImageProps.ts +25 -0
  116. package/src/camera/guidanceGraphics.tsx +347 -0
  117. package/src/camera/guidanceTokens.ts +57 -0
  118. package/src/camera/panModeGate.ts +81 -0
  119. package/src/camera/pickCaptureFormat.ts +130 -0
  120. package/src/camera/stitchDebugInfo.ts +71 -0
  121. package/src/camera/usePanMotion.ts +667 -0
  122. package/src/index.ts +66 -3
  123. package/src/stitching/computeInscribedRect.ts +81 -0
  124. package/src/stitching/cropQuad.ts +167 -0
  125. package/src/stitching/incremental.ts +74 -0
  126. package/src/stitching/useIncrementalStitcher.ts +13 -0
  127. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
  128. package/cpp/tests/CMakeLists.txt +0 -104
  129. package/cpp/tests/README.md +0 -86
  130. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  131. package/cpp/tests/pose_test.cpp +0 -74
  132. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  133. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  134. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  135. package/cpp/tests/warp_guard_test.cpp +0 -48
  136. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  137. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  138. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  139. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  140. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  141. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  142. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  143. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  144. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  145. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  146. 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
- });