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.
@@ -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
- * As of v0.5 (F8.3) this prop is **deprecated for the standard
307
- * non-AR capture flow**: the SDK now installs its own frame
308
- * processor via `useFrameProcessorDriver` that pipes pixel
309
- * buffers into the incremental stitcher with synthesised pose.
310
- * Setting this prop in the default mode will be IGNORED with a
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
- * Three coexistence rules:
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
- * Default `false` (use the new `useFrameProcessorDriver`, which
331
- * runs the gate on the camera producer thread at native frame
332
- * rate via a vision-camera Frame Processor plugin). The legacy
333
- * path will be removed in v0.6 — set this only if you hit a
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
- legacyDriver?: boolean;
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
- // JS-driver for non-AR captures (iOS + Android). In AR mode the
794
- // engine consumes frames from the ARSession stream natively, so this
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
- // `jsDriver` in its deps would teardown + restart the driver on
803
- // every event, resetting the gyro accumulator (yaw/pitch) to zero
804
- // each cycle and nulling the cameraRef during the brief gap. The
805
- // user-visible symptom was "only the first keyframe is accepted,
806
- // every subsequent snapshot sees pose=(0,0) and is rejected as a
807
- // duplicate of the first". Matching AuditCaptureScreen's proven
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: ensure both drivers are stopped if the component unmounts
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(() => () => { jsDriver.stop(); fpDriver.stop(); }, []);
820
-
821
- // F8.3 — one-shot deprecation warning when the host supplies their
822
- // own `frameProcessor` while running in the default (Frame
823
- // Processor driver) mode. Two worklets racing on the same
824
- // producer thread would corrupt the engine's workQueue ordering;
825
- // the SDK's own worklet wins and the host's is ignored. Hosts
826
- // that *need* a custom worklet must opt into `legacyDriver={true}`
827
- // (which switches off the SDK's worklet entirely).
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 when the default driver is active '
839
- + '(legacyDriver=false). Either remove the prop or set '
840
- + 'legacyDriver={true} to opt into the legacy path.',
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 actually bound to vision-camera's
844
- // Camera. Resolution order:
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): originally gated this to
907
- // `legacyDriver` because the Frame Processor driver doesn't
908
- // consult `imuGate` for its own pose synthesis. That ignored a
909
- // load-bearing side effect: `imuGate.resetAnchor()` bounds the
910
- // IIR-integrator drift window per-accept, and
911
- // `imuGate.getTotalAbsMetres()` is read at finalize time
912
- // (Camera.tsx:1097) as `imuTranslationMetres` into the native
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. Keep the
916
- // reset firing for ALL non-AR modes.
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
- // F8.3 — non-AR captures pick between the new Frame Processor
1048
- // driver (default) and the legacy JS-snapshot driver (opt-in
1049
- // via `legacyDriver={true}`). AR captures always use the
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 frame source. AR mode feeds natively from
1070
- // ARSession so both drivers stay idle in that path.
1071
- // * Default: Frame Processor driver worklet runs on the
1072
- // producer thread, plugin calls `consumeFrameFromPlugin`
1073
- // directly. No camera ref needed (vision-camera owns it).
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
- if (legacyDriver) {
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. Both stop() calls are no-ops when the
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
- export { useIncrementalJSDriver } from './stitching/useIncrementalJSDriver';
181
- export type {
182
- UseIncrementalJSDriverOptions,
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 encodes
214
- * accepted frames to JPEG inline (the platform-specific
215
- * engine-input divergence is tracked as F8.6). Use in non-AR
216
- * captures driven by `useFrameProcessorDriver`. Pairs with
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' | 'jsDriver' | 'frameProcessor';
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. Replaces
5
- * `useIncrementalJSDriver` in non-AR captures.
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 takes a JPEG snapshot every ~250 ms and feeds the
10
- * path to `IncrementalStitcher.processFrameAtPath`. That path
11
- * has three costs:
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
- // Signs match the legacy `useIncrementalJSDriver` for x/y; z
307
- // follows the same right-hand-rule convention. If field
308
- // captures show inverted roll, flip the sign on `z * dt` below.
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
- // (matches legacy useIncrementalJSDriver semantics).
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