react-native-image-stitcher 0.5.1 → 0.6.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 +111 -1
- package/android/src/main/java/io/imagestitcher/rn/CvFlowGateFrameProcessor.kt +2 -2
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +2 -30
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +55 -368
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +6 -3
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +17 -30
- package/dist/camera/Camera.d.ts +11 -27
- package/dist/camera/Camera.js +46 -78
- package/dist/index.d.ts +0 -2
- package/dist/index.js +4 -6
- package/dist/stitching/incremental.d.ts +10 -11
- package/dist/stitching/useFrameProcessorDriver.d.ts +7 -6
- package/dist/stitching/useFrameProcessorDriver.js +12 -11
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +25 -206
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +0 -8
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +6 -126
- package/ios/Sources/RNImageStitcher/KeyframeGateFrameProcessor.mm +6 -6
- package/package.json +1 -1
- package/src/camera/Camera.tsx +57 -106
- package/src/index.ts +3 -8
- package/src/stitching/incremental.ts +10 -11
- package/src/stitching/useFrameProcessorDriver.ts +12 -11
- package/dist/stitching/useIncrementalJSDriver.d.ts +0 -74
- package/dist/stitching/useIncrementalJSDriver.js +0 -220
- package/src/stitching/useIncrementalJSDriver.ts +0 -297
package/src/camera/Camera.tsx
CHANGED
|
@@ -89,7 +89,6 @@ import {
|
|
|
89
89
|
subscribeIncrementalState,
|
|
90
90
|
type IncrementalState,
|
|
91
91
|
} from '../stitching/incremental';
|
|
92
|
-
import { useIncrementalJSDriver } from '../stitching/useIncrementalJSDriver';
|
|
93
92
|
import { useFrameProcessorDriver } from '../stitching/useFrameProcessorDriver';
|
|
94
93
|
import { useIncrementalStitcher } from '../stitching/useIncrementalStitcher';
|
|
95
94
|
import { useIMUTranslationGate } from '../sensors/useIMUTranslationGate';
|
|
@@ -303,37 +302,20 @@ export interface CameraProps {
|
|
|
303
302
|
* Introduced for F8 (FrameProcessor port) — see
|
|
304
303
|
* `docs/f8-frame-processor-plan.md`.
|
|
305
304
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* one-time console.warn — supplying your own worklet would race
|
|
312
|
-
* with the SDK's pixel-buffer feed.
|
|
305
|
+
* The SDK installs its own frame processor via
|
|
306
|
+
* `useFrameProcessorDriver`. Setting this prop is ignored with
|
|
307
|
+
* a one-time `console.warn` — supplying a host worklet would
|
|
308
|
+
* race with the SDK's pixel-buffer feed. Either remove the prop
|
|
309
|
+
* or fork the SDK if you genuinely need a custom worklet.
|
|
313
310
|
*
|
|
314
|
-
*
|
|
315
|
-
* * Default (modern non-AR): SDK owns the worklet, this prop
|
|
316
|
-
* is ignored.
|
|
317
|
-
* * `legacyDriver={true}`: SDK uses the old `useIncrementalJSDriver`
|
|
318
|
-
* (takeSnapshot path). Honoured for diagnostics or as an
|
|
319
|
-
* escape hatch.
|
|
320
|
-
* * AR mode: vision-camera Camera isn't mounted, this prop is
|
|
321
|
-
* irrelevant.
|
|
322
|
-
*/
|
|
323
|
-
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Opt back into the legacy `useIncrementalJSDriver` for non-AR
|
|
327
|
-
* captures (the v0.4 path: `takeSnapshot` → JPEG → cache file →
|
|
328
|
-
* `IncrementalStitcher.processFrameAtPath`).
|
|
311
|
+
* AR mode is irrelevant: vision-camera's Camera isn't mounted.
|
|
329
312
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* specific issue with the new driver and need to ship a fix.
|
|
313
|
+
* (v0.5 had a `legacyDriver` escape hatch that routed back to
|
|
314
|
+
* `useIncrementalJSDriver`. That hook + prop were removed in
|
|
315
|
+
* v0.6 per the deprecation timeline announced in the v0.5.0
|
|
316
|
+
* CHANGELOG.)
|
|
335
317
|
*/
|
|
336
|
-
|
|
318
|
+
frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor;
|
|
337
319
|
}
|
|
338
320
|
|
|
339
321
|
|
|
@@ -609,7 +591,6 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
609
591
|
onFramesDropped,
|
|
610
592
|
onError,
|
|
611
593
|
frameProcessor: hostFrameProcessor,
|
|
612
|
-
legacyDriver = false,
|
|
613
594
|
engine = 'batch-keyframe',
|
|
614
595
|
} = props;
|
|
615
596
|
|
|
@@ -790,63 +771,49 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
790
771
|
},
|
|
791
772
|
});
|
|
792
773
|
|
|
793
|
-
//
|
|
794
|
-
// engine consumes frames from the ARSession stream
|
|
795
|
-
// hook stays idle.
|
|
774
|
+
// Frame Processor driver for non-AR captures (iOS + Android).
|
|
775
|
+
// In AR mode the engine consumes frames from the ARSession stream
|
|
776
|
+
// natively, so this hook stays idle.
|
|
796
777
|
//
|
|
797
778
|
// IMPORTANT: start()/stop() are called imperatively from the hold
|
|
798
779
|
// handlers below — NOT from a useEffect driven by statusPhase. The
|
|
799
780
|
// hook returns a fresh object identity on every render, and during
|
|
800
781
|
// a recording the engine emits IncrementalStateUpdate events that
|
|
801
|
-
// cause re-renders multiple times per second. An effect with
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
// imperative pattern (start on hold-start, stop on hold-end) avoids
|
|
809
|
-
// the re-render churn entirely.
|
|
810
|
-
const jsDriver = useIncrementalJSDriver();
|
|
811
|
-
// F8.3 — vision-camera Frame Processor variant. Always
|
|
812
|
-
// instantiated so we don't have conditional hook calls; only one
|
|
813
|
-
// of the two drivers actually .start()s per capture. Stop() on
|
|
814
|
-
// an idle driver is a no-op.
|
|
782
|
+
// cause re-renders multiple times per second. An effect with the
|
|
783
|
+
// driver in its deps would teardown + restart on every event,
|
|
784
|
+
// resetting the gyro accumulator (yaw/pitch) to zero each cycle.
|
|
785
|
+
// User-visible symptom: "only the first keyframe is accepted, every
|
|
786
|
+
// subsequent ingest sees pose=(0,0) and is rejected as a duplicate".
|
|
787
|
+
// The imperative pattern (start on hold-start, stop on hold-end)
|
|
788
|
+
// avoids the re-render churn entirely.
|
|
815
789
|
const fpDriver = useFrameProcessorDriver();
|
|
816
|
-
// Safety:
|
|
817
|
-
// mid-recording. Empty deps so this only fires on unmount.
|
|
790
|
+
// Safety: stop the driver if the component unmounts mid-recording.
|
|
818
791
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
819
|
-
useEffect(() => () => {
|
|
820
|
-
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
792
|
+
useEffect(() => () => { fpDriver.stop(); }, []);
|
|
793
|
+
|
|
794
|
+
// One-shot deprecation warning when the host supplies their own
|
|
795
|
+
// `frameProcessor` prop. Two worklets racing on the same
|
|
796
|
+
// producer thread would corrupt the engine's workQueue ordering,
|
|
797
|
+
// so the SDK's own worklet wins and the host's is silently
|
|
798
|
+
// ignored. (v0.5 had a `legacyDriver` opt-out for hosts that
|
|
799
|
+
// wanted to route around the SDK driver; that was removed in
|
|
800
|
+
// v0.6 along with `useIncrementalJSDriver`.)
|
|
828
801
|
const hostFrameProcessorIgnoredWarnedRef = useRef(false);
|
|
829
802
|
if (
|
|
830
803
|
hostFrameProcessor != null
|
|
831
|
-
&& !legacyDriver
|
|
832
804
|
&& !hostFrameProcessorIgnoredWarnedRef.current
|
|
833
805
|
) {
|
|
834
806
|
hostFrameProcessorIgnoredWarnedRef.current = true;
|
|
835
807
|
// eslint-disable-next-line no-console
|
|
836
808
|
console.warn(
|
|
837
809
|
'[react-native-image-stitcher] The `frameProcessor` prop on '
|
|
838
|
-
+ '<Camera> is ignored
|
|
839
|
-
+ '
|
|
840
|
-
+ '
|
|
810
|
+
+ '<Camera> is ignored — the SDK installs its own worklet '
|
|
811
|
+
+ 'via useFrameProcessorDriver. Remove the prop, or fork '
|
|
812
|
+
+ 'the SDK if you genuinely need a custom worklet.',
|
|
841
813
|
);
|
|
842
814
|
}
|
|
843
|
-
// The Frame Processor worklet
|
|
844
|
-
|
|
845
|
-
// 1. Legacy mode: honor the host's prop (or null).
|
|
846
|
-
// 2. Modern mode: SDK driver's worklet, regardless of host's prop.
|
|
847
|
-
const effectiveFrameProcessor = legacyDriver
|
|
848
|
-
? (hostFrameProcessor ?? null)
|
|
849
|
-
: fpDriver.frameProcessor;
|
|
815
|
+
// The Frame Processor worklet bound to vision-camera's Camera.
|
|
816
|
+
const effectiveFrameProcessor = fpDriver.frameProcessor;
|
|
850
817
|
|
|
851
818
|
// ── Subscribe to engine state for live keyframe thumbs ──────────
|
|
852
819
|
useEffect(() => {
|
|
@@ -903,17 +870,17 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
903
870
|
const accepted = incrementalState?.acceptedCount ?? 0;
|
|
904
871
|
if (accepted > lastAcceptedCountRef.current) {
|
|
905
872
|
lastAcceptedCountRef.current = accepted;
|
|
906
|
-
// F8.3 review-of-review (M3 revert):
|
|
907
|
-
// `legacyDriver` because the Frame
|
|
908
|
-
// consult `imuGate` for its own pose
|
|
909
|
-
// load-bearing side effect:
|
|
910
|
-
// IIR-integrator drift
|
|
911
|
-
// `imuGate.getTotalAbsMetres()` is read
|
|
912
|
-
//
|
|
873
|
+
// F8.3 review-of-review (M3 revert): an earlier draft gated
|
|
874
|
+
// this on the pre-v0.6 `legacyDriver` prop because the Frame
|
|
875
|
+
// Processor driver doesn't consult `imuGate` for its own pose
|
|
876
|
+
// synthesis. That ignored a load-bearing side effect:
|
|
877
|
+
// `imuGate.resetAnchor()` bounds the IIR-integrator drift
|
|
878
|
+
// window per-accept, and `imuGate.getTotalAbsMetres()` is read
|
|
879
|
+
// at finalize time as `imuTranslationMetres` into the native
|
|
913
880
|
// stitchMode auto-resolver (PANORAMA vs SCANS). Without the
|
|
914
881
|
// per-accept reset, long FP-driver captures let IIR drift
|
|
915
|
-
// compound → inflated metres → biased toward SCANS.
|
|
916
|
-
//
|
|
882
|
+
// compound → inflated metres → biased toward SCANS. Now fires
|
|
883
|
+
// for ALL non-AR captures (the only non-AR driver post-v0.6).
|
|
917
884
|
if (isNonAR) {
|
|
918
885
|
imuGate.resetAnchor();
|
|
919
886
|
}
|
|
@@ -1044,13 +1011,11 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1044
1011
|
snapshotEveryNAccepts: 1,
|
|
1045
1012
|
frameRotationDegrees: orientationRotation,
|
|
1046
1013
|
captureOrientation: deviceOrientation,
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
// ARSession-driven path.
|
|
1051
|
-
frameSourceMode: isNonAR
|
|
1052
|
-
? (legacyDriver ? 'jsDriver' : 'frameProcessor')
|
|
1053
|
-
: 'arSession',
|
|
1014
|
+
// Non-AR captures use the Frame Processor driver
|
|
1015
|
+
// (vision-camera producer-thread worklet → cv_flow_gate
|
|
1016
|
+
// plugin → IncrementalStitcher.consumeFrame). AR captures
|
|
1017
|
+
// use the ARSession-driven path.
|
|
1018
|
+
frameSourceMode: isNonAR ? 'frameProcessor' : 'arSession',
|
|
1054
1019
|
composeWidth: 1920,
|
|
1055
1020
|
composeHeight: 1080,
|
|
1056
1021
|
canvasWidth: 5000,
|
|
@@ -1066,21 +1031,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1066
1031
|
// matching comment on the per-accept reset useEffect above).
|
|
1067
1032
|
// Keep firing it on every capture start, not just legacy mode.
|
|
1068
1033
|
imuGate.resetAnchor();
|
|
1069
|
-
// Start the non-AR
|
|
1070
|
-
// ARSession so
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
// * Legacy: JS driver — `takeSnapshot` + `processFrameAtPath`
|
|
1075
|
-
// via the cameraRef.
|
|
1076
|
-
// Imperative-pattern rationale: see the useIncrementalJSDriver
|
|
1077
|
-
// comment above re. why this isn't a useEffect.
|
|
1034
|
+
// Start the Frame Processor driver for non-AR captures. AR
|
|
1035
|
+
// mode feeds natively from ARSession so the driver stays idle.
|
|
1036
|
+
// Imperative pattern (vs useEffect) because the driver's start
|
|
1037
|
+
// resets pose accumulators that should only fire at the
|
|
1038
|
+
// hold-start moment, not on every re-render.
|
|
1078
1039
|
if (isNonAR) {
|
|
1079
|
-
|
|
1080
|
-
jsDriver.start(visionCameraRef);
|
|
1081
|
-
} else {
|
|
1082
|
-
fpDriver.start();
|
|
1083
|
-
}
|
|
1040
|
+
fpDriver.start();
|
|
1084
1041
|
}
|
|
1085
1042
|
} catch (err) {
|
|
1086
1043
|
setStatusPhase('idle');
|
|
@@ -1100,9 +1057,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1100
1057
|
settings,
|
|
1101
1058
|
effectiveCaptureSource,
|
|
1102
1059
|
imuGate,
|
|
1103
|
-
jsDriver,
|
|
1104
1060
|
fpDriver,
|
|
1105
|
-
legacyDriver,
|
|
1106
1061
|
engine,
|
|
1107
1062
|
onError,
|
|
1108
1063
|
]);
|
|
@@ -1112,10 +1067,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1112
1067
|
setStatusPhase('stitching');
|
|
1113
1068
|
// Stop pumping new frames before finalizing so the engine isn't
|
|
1114
1069
|
// racing the final cv::Stitcher pass against late-arriving
|
|
1115
|
-
// keyframes.
|
|
1116
|
-
// corresponding driver wasn't started (AR mode, or the inactive
|
|
1117
|
-
// driver in non-AR mode).
|
|
1118
|
-
jsDriver.stop();
|
|
1070
|
+
// keyframes. No-op in AR mode (the driver was never started).
|
|
1119
1071
|
fpDriver.stop();
|
|
1120
1072
|
try {
|
|
1121
1073
|
// Compose the panorama output path: host-controlled if
|
|
@@ -1193,7 +1145,6 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1193
1145
|
onFramesDropped,
|
|
1194
1146
|
onError,
|
|
1195
1147
|
recordingStartedAt,
|
|
1196
|
-
jsDriver,
|
|
1197
1148
|
fpDriver,
|
|
1198
1149
|
// F10 Phase 2 review N1 — these four were missing pre-fix. The
|
|
1199
1150
|
// callback reads `settings.debug` (to gate the stitchToast),
|
package/src/index.ts
CHANGED
|
@@ -177,14 +177,9 @@ export {
|
|
|
177
177
|
} from './stitching/incremental';
|
|
178
178
|
export type { IncrementalState } from './stitching/incremental';
|
|
179
179
|
export { useIncrementalStitcher } from './stitching/useIncrementalStitcher';
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
IncrementalJSDriverHandle,
|
|
184
|
-
} from './stitching/useIncrementalJSDriver';
|
|
185
|
-
// F8.3 — vision-camera Frame Processor variant of the non-AR
|
|
186
|
-
// driver. Preferred over `useIncrementalJSDriver` in v0.5+; the
|
|
187
|
-
// JS driver stays exported as a deprecated fallback until v0.6.
|
|
180
|
+
// vision-camera Frame Processor driver for non-AR captures. As
|
|
181
|
+
// of v0.6 the only non-AR driver exported (the legacy
|
|
182
|
+
// `useIncrementalJSDriver` was removed; was deprecated in v0.5).
|
|
188
183
|
export { useFrameProcessorDriver } from './stitching/useFrameProcessorDriver';
|
|
189
184
|
export type {
|
|
190
185
|
UseFrameProcessorDriverOptions,
|
|
@@ -200,23 +200,22 @@ export interface IncrementalStartOptions {
|
|
|
200
200
|
* bridge.start() requires `RNSARSession.start()` to
|
|
201
201
|
* have already been called.
|
|
202
202
|
*
|
|
203
|
-
* - 'jsDriver' — engine skips AR-session registration; JS
|
|
204
|
-
* feeds frames via `processFrameAtPath`. Use in iOS non-AR
|
|
205
|
-
* captures (vision-camera + gyro). No AR session required.
|
|
206
|
-
* LEGACY; deprecated in v0.5, removed in v0.6.
|
|
207
|
-
*
|
|
208
203
|
* - 'frameProcessor' (F8.3 iOS / F8.4 Android, v0.5+) — engine
|
|
209
204
|
* flips on `frameProcessorIngestEnabled` so the vision-camera
|
|
210
205
|
* Frame Processor plugin (`cv_flow_gate_process_frame`) can
|
|
211
206
|
* feed pixel data directly into the engine's gate path. iOS
|
|
212
207
|
* passes the `CVPixelBuffer` straight to `consumeFrame`;
|
|
213
|
-
* Android extracts the Y plane to a ByteArray and
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* captures driven by `useFrameProcessorDriver`. Pairs
|
|
217
|
-
* `Camera`'s default driver mode.
|
|
208
|
+
* Android extracts the Y plane to a ByteArray and (since
|
|
209
|
+
* F8.6, v0.5.1) routes live-engine ingest through
|
|
210
|
+
* `addFramePixelData` without a JPEG round-trip. Use in
|
|
211
|
+
* non-AR captures driven by `useFrameProcessorDriver`. Pairs
|
|
212
|
+
* with `Camera`'s default driver mode.
|
|
213
|
+
*
|
|
214
|
+
* `'jsDriver'` was removed in v0.6 (deprecated in v0.5). Hosts
|
|
215
|
+
* that used it should switch to `useFrameProcessorDriver` (or
|
|
216
|
+
* just let `<Camera>` use its default).
|
|
218
217
|
*/
|
|
219
|
-
frameSourceMode?: 'arSession' | '
|
|
218
|
+
frameSourceMode?: 'arSession' | 'frameProcessor';
|
|
220
219
|
/** Compose-resolution width in pixels (default 720 for portrait, 960 for landscape). */
|
|
221
220
|
composeWidth?: number;
|
|
222
221
|
/** Compose-resolution height in pixels (default 960 for portrait, 720 for landscape). */
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* useFrameProcessorDriver — vision-camera Frame Processor + gyro
|
|
4
|
-
* driver for the incremental panorama engine.
|
|
5
|
-
* `useIncrementalJSDriver`
|
|
4
|
+
* driver for the incremental panorama engine. Sole non-AR driver
|
|
5
|
+
* from v0.6 onward (replaced the deprecated `useIncrementalJSDriver`
|
|
6
|
+
* hook, which was removed in v0.6).
|
|
6
7
|
*
|
|
7
|
-
* Why this exists (vs the JS-driver predecessor)
|
|
8
|
+
* Why this exists (vs the pre-v0.6 JS-driver predecessor)
|
|
8
9
|
*
|
|
9
|
-
* The JS driver
|
|
10
|
-
* path to `IncrementalStitcher.processFrameAtPath
|
|
11
|
-
*
|
|
10
|
+
* The old JS driver took a JPEG snapshot every ~250 ms and fed the
|
|
11
|
+
* path to `IncrementalStitcher.processFrameAtPath` (both removed in
|
|
12
|
+
* v0.6). That path had three costs:
|
|
12
13
|
*
|
|
13
14
|
* 1. JPEG encode (`takeSnapshot` ≈ 30–80 ms on iPhone 16 Pro)
|
|
14
15
|
* 2. Disk write of the JPEG
|
|
@@ -303,9 +304,9 @@ export function useFrameProcessorDriver(
|
|
|
303
304
|
// y = horizontal pan (yaw, about world-Y)
|
|
304
305
|
// x = vertical tilt (pitch, about world-X)
|
|
305
306
|
// z = wrist-twist roll (about world-Z, normal to the screen)
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
307
|
+
// Right-hand-rule convention throughout — same signs the pre-v0.6
|
|
308
|
+
// `useIncrementalJSDriver` produced. If field captures show
|
|
309
|
+
// inverted roll, flip the sign on `z * dt` below.
|
|
309
310
|
setUpdateIntervalForType(SensorTypes.gyroscope, gyroIntervalMs);
|
|
310
311
|
gyroSubRef.current = gyroscope.subscribe({
|
|
311
312
|
next: ({ x, y, z }) => {
|
|
@@ -382,8 +383,8 @@ export function useFrameProcessorDriver(
|
|
|
382
383
|
imageWidth: w, imageHeight: h,
|
|
383
384
|
timestampMs: 0,
|
|
384
385
|
// 2 == RNSARTrackingState.tracking — we always claim "good
|
|
385
|
-
// tracking" because there's no ARKit signal to differentiate
|
|
386
|
-
// (
|
|
386
|
+
// tracking" because there's no ARKit signal to differentiate.
|
|
387
|
+
// (Same contract as the pre-v0.6 useIncrementalJSDriver.)
|
|
387
388
|
trackingStateRaw: 2,
|
|
388
389
|
});
|
|
389
390
|
// Deps array intentionally minimal: only `plugin` actually
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useIncrementalJSDriver — vision-camera + gyro frame driver for
|
|
3
|
-
* the incremental panorama engine, used in non-AR captures on both
|
|
4
|
-
* iOS and Android.
|
|
5
|
-
*
|
|
6
|
-
* History: previously called `useIncrementalAndroidDriver` because
|
|
7
|
-
* it was Android-only. As of 2026-05-17 (Issue #2), the native
|
|
8
|
-
* `processFrameAtPath` entry point exists on both platforms and the
|
|
9
|
-
* hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
|
|
10
|
-
* that.
|
|
11
|
-
*
|
|
12
|
-
* Why this exists
|
|
13
|
-
* In AR captures the engine consumes frames from the ARSession
|
|
14
|
-
* stream natively (60 Hz pose + image delivery, zero JS
|
|
15
|
-
* involvement once started). In NON-AR captures there is no AR
|
|
16
|
-
* session — vision-camera owns the camera — so the engine needs
|
|
17
|
-
* another frame source. This hook fills the gap:
|
|
18
|
-
*
|
|
19
|
-
* - vision-camera keeps the camera viewport
|
|
20
|
-
* - `takeSnapshot()` runs at ~250 ms intervals during press-hold
|
|
21
|
-
* - `react-native-sensors` gyroscope is integrated to estimate
|
|
22
|
-
* cumulative yaw/pitch (drives the FoV-overlap gate)
|
|
23
|
-
* - Each snapshot path + integrated pose is fed to
|
|
24
|
-
* `IncrementalStitcher.processFrameAtPath()`
|
|
25
|
-
*
|
|
26
|
-
* Trade-off vs the AR path
|
|
27
|
-
* Gyro integration drifts ~1–2° per minute. Acceptable for the
|
|
28
|
-
* typical 5–15 s shelf pan; not great for ambitious 360° captures.
|
|
29
|
-
* Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
|
|
30
|
-
* frame-selection only — the actual image alignment is feature-
|
|
31
|
-
* matched + RANSAC-fit, so quality of the panorama itself isn't
|
|
32
|
-
* bounded by gyro accuracy.
|
|
33
|
-
*
|
|
34
|
-
* Lifecycle
|
|
35
|
-
* `start({ cameraRef })` enables the loop; `stop()` tears down.
|
|
36
|
-
* Both should be called by the host's hold-start / hold-complete
|
|
37
|
-
* handlers. Safe to call on either platform; the hook only
|
|
38
|
-
* activates inside the start/stop block.
|
|
39
|
-
*/
|
|
40
|
-
import type { Camera } from 'react-native-vision-camera';
|
|
41
|
-
export interface UseIncrementalJSDriverOptions {
|
|
42
|
-
/**
|
|
43
|
-
* Snapshot interval in ms. Default 250 (≈ 4 Hz). Lower = more
|
|
44
|
-
* candidate frames + more disk I/O. Don't go below 200 — vision-
|
|
45
|
-
* camera's snapshot pipeline can't keep up reliably below that.
|
|
46
|
-
*/
|
|
47
|
-
snapshotIntervalMs?: number;
|
|
48
|
-
/**
|
|
49
|
-
* Gyro sample rate in ms (~30 Hz default matches the existing
|
|
50
|
-
* `PanoramaGuidance` cadence). Used for pose integration only —
|
|
51
|
-
* not the snapshot rate.
|
|
52
|
-
*/
|
|
53
|
-
gyroIntervalMs?: number;
|
|
54
|
-
/**
|
|
55
|
-
* Approximate horizontal FoV of the device camera. Drives the
|
|
56
|
-
* overlap-percent calculation in the native engine. Default 65°
|
|
57
|
-
* is a reasonable mid-tier smartphone average.
|
|
58
|
-
*/
|
|
59
|
-
fovHorizDegrees?: number;
|
|
60
|
-
/**
|
|
61
|
-
* Approximate vertical FoV of the device camera. Default 50° for
|
|
62
|
-
* typical 4:3 phone cameras. When ARCore-driven path is in use
|
|
63
|
-
* the engine receives both FoVs straight from intrinsics; the
|
|
64
|
-
* gyro driver is a fallback so the defaults are good enough.
|
|
65
|
-
*/
|
|
66
|
-
fovVertDegrees?: number;
|
|
67
|
-
}
|
|
68
|
-
export interface IncrementalJSDriverHandle {
|
|
69
|
-
start: (cameraRef: React.RefObject<Camera | null>) => void;
|
|
70
|
-
stop: () => void;
|
|
71
|
-
isRunning: boolean;
|
|
72
|
-
}
|
|
73
|
-
export declare function useIncrementalJSDriver(options?: UseIncrementalJSDriverOptions): IncrementalJSDriverHandle;
|
|
74
|
-
//# sourceMappingURL=useIncrementalJSDriver.d.ts.map
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* useIncrementalJSDriver — vision-camera + gyro frame driver for
|
|
5
|
-
* the incremental panorama engine, used in non-AR captures on both
|
|
6
|
-
* iOS and Android.
|
|
7
|
-
*
|
|
8
|
-
* History: previously called `useIncrementalAndroidDriver` because
|
|
9
|
-
* it was Android-only. As of 2026-05-17 (Issue #2), the native
|
|
10
|
-
* `processFrameAtPath` entry point exists on both platforms and the
|
|
11
|
-
* hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
|
|
12
|
-
* that.
|
|
13
|
-
*
|
|
14
|
-
* Why this exists
|
|
15
|
-
* In AR captures the engine consumes frames from the ARSession
|
|
16
|
-
* stream natively (60 Hz pose + image delivery, zero JS
|
|
17
|
-
* involvement once started). In NON-AR captures there is no AR
|
|
18
|
-
* session — vision-camera owns the camera — so the engine needs
|
|
19
|
-
* another frame source. This hook fills the gap:
|
|
20
|
-
*
|
|
21
|
-
* - vision-camera keeps the camera viewport
|
|
22
|
-
* - `takeSnapshot()` runs at ~250 ms intervals during press-hold
|
|
23
|
-
* - `react-native-sensors` gyroscope is integrated to estimate
|
|
24
|
-
* cumulative yaw/pitch (drives the FoV-overlap gate)
|
|
25
|
-
* - Each snapshot path + integrated pose is fed to
|
|
26
|
-
* `IncrementalStitcher.processFrameAtPath()`
|
|
27
|
-
*
|
|
28
|
-
* Trade-off vs the AR path
|
|
29
|
-
* Gyro integration drifts ~1–2° per minute. Acceptable for the
|
|
30
|
-
* typical 5–15 s shelf pan; not great for ambitious 360° captures.
|
|
31
|
-
* Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
|
|
32
|
-
* frame-selection only — the actual image alignment is feature-
|
|
33
|
-
* matched + RANSAC-fit, so quality of the panorama itself isn't
|
|
34
|
-
* bounded by gyro accuracy.
|
|
35
|
-
*
|
|
36
|
-
* Lifecycle
|
|
37
|
-
* `start({ cameraRef })` enables the loop; `stop()` tears down.
|
|
38
|
-
* Both should be called by the host's hold-start / hold-complete
|
|
39
|
-
* handlers. Safe to call on either platform; the hook only
|
|
40
|
-
* activates inside the start/stop block.
|
|
41
|
-
*/
|
|
42
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.useIncrementalJSDriver = useIncrementalJSDriver;
|
|
44
|
-
const react_1 = require("react");
|
|
45
|
-
const react_native_1 = require("react-native");
|
|
46
|
-
const react_native_sensors_1 = require("react-native-sensors");
|
|
47
|
-
// One-shot deprecation flag — module-scoped so multiple host
|
|
48
|
-
// instances of the hook all share the same gate and we only emit
|
|
49
|
-
// the warning the first time anyone calls .start() in this
|
|
50
|
-
// process.
|
|
51
|
-
let deprecationWarningEmitted = false;
|
|
52
|
-
function getNativeIncremental() {
|
|
53
|
-
const m = react_native_1.NativeModules['IncrementalStitcher'];
|
|
54
|
-
if (!m || typeof m !== 'object')
|
|
55
|
-
return null;
|
|
56
|
-
return m;
|
|
57
|
-
}
|
|
58
|
-
function useIncrementalJSDriver(options = {}) {
|
|
59
|
-
const { snapshotIntervalMs = 250, gyroIntervalMs = 33, fovHorizDegrees = 65, fovVertDegrees = 50, } = options;
|
|
60
|
-
const intervalRef = (0, react_1.useRef)(null);
|
|
61
|
-
const gyroSubRef = (0, react_1.useRef)(null);
|
|
62
|
-
const cameraRef = (0, react_1.useRef)(null);
|
|
63
|
-
// Integrated pose accumulators, in radians. Reset on each
|
|
64
|
-
// start() call. Y-axis = horizontal pan (yaw), X-axis = vertical
|
|
65
|
-
// pan (pitch). Sign convention matches ARKit: counter-clockwise
|
|
66
|
-
// from above is positive yaw.
|
|
67
|
-
const yawRef = (0, react_1.useRef)(0);
|
|
68
|
-
const pitchRef = (0, react_1.useRef)(0);
|
|
69
|
-
const lastGyroAtRef = (0, react_1.useRef)(null);
|
|
70
|
-
// Single in-flight guard so we don't pile up overlapping snapshot
|
|
71
|
-
// promises on slow devices — if last snapshot hasn't finished
|
|
72
|
-
// when the next interval fires, skip.
|
|
73
|
-
const snapshotInFlightRef = (0, react_1.useRef)(false);
|
|
74
|
-
// Module-level "is the driver active right now" — exposed to the
|
|
75
|
-
// host because the hook itself doesn't trigger re-renders.
|
|
76
|
-
const isRunningRef = (0, react_1.useRef)(false);
|
|
77
|
-
const stop = (0, react_1.useCallback)(() => {
|
|
78
|
-
if (intervalRef.current) {
|
|
79
|
-
clearInterval(intervalRef.current);
|
|
80
|
-
intervalRef.current = null;
|
|
81
|
-
}
|
|
82
|
-
if (gyroSubRef.current) {
|
|
83
|
-
gyroSubRef.current.unsubscribe();
|
|
84
|
-
gyroSubRef.current = null;
|
|
85
|
-
}
|
|
86
|
-
cameraRef.current = null;
|
|
87
|
-
isRunningRef.current = false;
|
|
88
|
-
}, []);
|
|
89
|
-
const start = (0, react_1.useCallback)((cameraRefArg) => {
|
|
90
|
-
// 2026-05-17 (Issue #2) — removed the Android-only platform
|
|
91
|
-
// guard. iOS now also exposes `processFrameAtPath` (see the
|
|
92
|
-
// Swift bridge), so the same driver feeds both platforms in
|
|
93
|
-
// non-AR mode.
|
|
94
|
-
if (isRunningRef.current)
|
|
95
|
-
return;
|
|
96
|
-
// F8.5 — one-shot deprecation warning. v0.5.0 introduced
|
|
97
|
-
// `useFrameProcessorDriver` (vision-camera producer-thread
|
|
98
|
-
// path, native frame rate, no JPEG round-trip). The legacy
|
|
99
|
-
// takeSnapshot path stays available for one minor cycle to
|
|
100
|
-
// give hosts time to migrate, then is removed in v0.6.
|
|
101
|
-
if (!deprecationWarningEmitted) {
|
|
102
|
-
deprecationWarningEmitted = true;
|
|
103
|
-
// eslint-disable-next-line no-console
|
|
104
|
-
console.warn('[react-native-image-stitcher] `useIncrementalJSDriver` '
|
|
105
|
-
+ 'is DEPRECATED as of v0.5.0 and will be REMOVED in '
|
|
106
|
-
+ 'v0.6.0. Migrate to `useFrameProcessorDriver` (or '
|
|
107
|
-
+ 'simply let `<Camera>` use its default driver — no host '
|
|
108
|
-
+ 'code change needed). Opt-out via the `legacyDriver` '
|
|
109
|
-
+ 'prop on `<Camera>` if you need to stay on the legacy '
|
|
110
|
-
+ 'path temporarily.');
|
|
111
|
-
}
|
|
112
|
-
const native = getNativeIncremental();
|
|
113
|
-
if (!native)
|
|
114
|
-
return;
|
|
115
|
-
cameraRef.current = cameraRefArg;
|
|
116
|
-
yawRef.current = 0;
|
|
117
|
-
pitchRef.current = 0;
|
|
118
|
-
lastGyroAtRef.current = null;
|
|
119
|
-
snapshotInFlightRef.current = false;
|
|
120
|
-
isRunningRef.current = true;
|
|
121
|
-
// Gyro integration. Each sample carries angular velocity in
|
|
122
|
-
// rad/s; multiply by elapsed time to accumulate angular
|
|
123
|
-
// displacement. Note: the gyro axes are device-local; we use
|
|
124
|
-
// y for yaw and x for pitch on a device held in portrait.
|
|
125
|
-
// Landscape would swap, but the FoV-overlap gate is dominant-
|
|
126
|
-
// axis based on the .mm side, so the convention matters less
|
|
127
|
-
// than consistency.
|
|
128
|
-
(0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, gyroIntervalMs);
|
|
129
|
-
gyroSubRef.current = react_native_sensors_1.gyroscope.subscribe({
|
|
130
|
-
next: ({ x, y }) => {
|
|
131
|
-
const now = Date.now();
|
|
132
|
-
if (lastGyroAtRef.current === null) {
|
|
133
|
-
lastGyroAtRef.current = now;
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const dt = (now - lastGyroAtRef.current) / 1000.0;
|
|
137
|
-
lastGyroAtRef.current = now;
|
|
138
|
-
yawRef.current += y * dt;
|
|
139
|
-
pitchRef.current += x * dt;
|
|
140
|
-
},
|
|
141
|
-
error: (err) => {
|
|
142
|
-
// eslint-disable-next-line no-console
|
|
143
|
-
console.warn('[useIncrementalJSDriver] gyro error', err);
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
// Snapshot loop.
|
|
147
|
-
const tick = async () => {
|
|
148
|
-
if (snapshotInFlightRef.current)
|
|
149
|
-
return;
|
|
150
|
-
const cam = cameraRef.current?.current;
|
|
151
|
-
if (!cam)
|
|
152
|
-
return;
|
|
153
|
-
snapshotInFlightRef.current = true;
|
|
154
|
-
try {
|
|
155
|
-
const snap = await cam.takeSnapshot({ quality: 70 });
|
|
156
|
-
if (!snap?.path)
|
|
157
|
-
return;
|
|
158
|
-
// Synthesise a quaternion from integrated yaw + pitch.
|
|
159
|
-
// Yaw rotates about world Y (gravity), pitch about world X
|
|
160
|
-
// (perpendicular to gravity in the device's frame).
|
|
161
|
-
// Combined as q = q_yaw · q_pitch.
|
|
162
|
-
const halfYaw = yawRef.current / 2;
|
|
163
|
-
const halfPitch = pitchRef.current / 2;
|
|
164
|
-
const cy_ = Math.cos(halfYaw);
|
|
165
|
-
const sy_ = Math.sin(halfYaw);
|
|
166
|
-
const cp = Math.cos(halfPitch);
|
|
167
|
-
const sp = Math.sin(halfPitch);
|
|
168
|
-
// q_yaw = (0, sy, 0, cy)
|
|
169
|
-
// q_pitch = (sp, 0, 0, cp)
|
|
170
|
-
// q = q_yaw * q_pitch:
|
|
171
|
-
const qx = cy_ * sp;
|
|
172
|
-
const qy = sy_ * cp;
|
|
173
|
-
const qz = -sy_ * sp;
|
|
174
|
-
const qw = cy_ * cp;
|
|
175
|
-
// Vision-camera v4 doesn't expose camera intrinsics on
|
|
176
|
-
// Android, so we estimate fx/fy from the snapshot's pixel
|
|
177
|
-
// dimensions + assumed FoV. cx/cy at image centre. This
|
|
178
|
-
// is approximate; the proper Android live path is the
|
|
179
|
-
// ARCameraView, where ARCore gives us the real intrinsics.
|
|
180
|
-
const w = snap.width ?? 1920;
|
|
181
|
-
const h = snap.height ?? 1440;
|
|
182
|
-
const fx = w / (2.0 * Math.tan(((fovHorizDegrees * Math.PI) / 180) / 2));
|
|
183
|
-
const fy = h / (2.0 * Math.tan(((fovVertDegrees * Math.PI) / 180) / 2));
|
|
184
|
-
const cx = w / 2;
|
|
185
|
-
const cy = h / 2;
|
|
186
|
-
await native.processFrameAtPath({
|
|
187
|
-
path: snap.path,
|
|
188
|
-
yaw: yawRef.current,
|
|
189
|
-
pitch: pitchRef.current,
|
|
190
|
-
fovHorizDegrees,
|
|
191
|
-
fovVertDegrees,
|
|
192
|
-
trackingPoor: false,
|
|
193
|
-
qx, qy, qz, qw,
|
|
194
|
-
fx, fy, cx, cy,
|
|
195
|
-
imageWidth: w, imageHeight: h,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
catch (err) {
|
|
199
|
-
// Swallow per-frame errors so the loop keeps running.
|
|
200
|
-
// eslint-disable-next-line no-console
|
|
201
|
-
console.warn('[useIncrementalJSDriver] processFrame failed', err);
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
snapshotInFlightRef.current = false;
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
// Kick off an immediate first frame so the engine doesn't sit
|
|
208
|
-
// idle for the first interval period.
|
|
209
|
-
tick();
|
|
210
|
-
intervalRef.current = setInterval(tick, snapshotIntervalMs);
|
|
211
|
-
}, [snapshotIntervalMs, gyroIntervalMs, fovHorizDegrees, fovVertDegrees]);
|
|
212
|
-
return {
|
|
213
|
-
start,
|
|
214
|
-
stop,
|
|
215
|
-
get isRunning() {
|
|
216
|
-
return isRunningRef.current;
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
//# sourceMappingURL=useIncrementalJSDriver.js.map
|