/** * Camera — the public, props-based camera component for the * `react-native-image-stitcher` library (publication target per the * 2026-05-15 design doc). * * One component, both modes: * - **Tap shutter** → single photo via vision-camera's takePhoto * (non-AR) or ARFrame.capturedImage (AR). * - **Hold shutter** → panorama capture; pan-and-release produces * a stitched panorama JPEG via the incremental stitcher. * * One component, both capture sources: * - **AR mode** (ARKit / ARCore) — used for pose-aware stitching * when the device supports it. * - **Non-AR mode** (vision-camera + IMU) — fallback path, * forced when the 0.5× ultra-wide lens is selected (AR sessions * are tied to a single physical lens; can't switch mid-session). * * The Camera component owns its runtime state (arPreference, lens, * settings). Parent props are read as INITIAL VALUES at mount; the * parent listens for state changes via the callback props. This * "uncontrolled" model matches React's `` convention and * matches the design doc's intent (NF — component owns runtime state, * parent persists via callbacks if desired). * * Scope note (step 2 of the SDK extract plan): * - Props-driven API for both photo + panorama modes — DONE here. * - Lens chip + AR toggle UI (U1) — DONE here. * - `showSettingsButton` gates the existing PanoramaSettingsModal — DONE. * - Imperative ref methods (`takePhoto()`, `startPanorama()`, * `stopPanorama()`) — deferred; the built-in shutter button is the * primary affordance for v0.1.0. * - Forward-looking props (`defaultCompositingResolMP`, * `defaultRegistrationResolMP`, `defaultSeamEstimationResolMP`) * are accepted but currently no-ops — those fields don't exist on * PanoramaSettings yet. They're declared so the public API is * stable before they wire through; the wiring is a follow-up. * * See: docs/site-content/design/2026-05-15-react-native-image-stitcher-publication.md */ import React from 'react'; import { type StyleProp, type ViewStyle } from 'react-native'; import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera'; import type { CameraFrameProcessor } from '../stitching/CameraFrame'; import type { ARFrameMeta, ARPluginResult } from '../stitching/ARFrameMeta'; import type { AROverlay } from '../stitching/AROverlay'; import type { AROverlayMethods } from './arOverlayController'; import { type CaptureHeaderProps } from './CaptureHeader'; import { type CapturePreviewAction } from './CapturePreview'; import { type CaptureThumbnailItem } from './CaptureThumbnailStrip'; import { type CaptureStatusPhase } from './CaptureStatusOverlay'; import { type PanoramaPropOverrides } from './buildPanoramaInitialSettings'; import { type DeviceOrientation } from './useDeviceOrientation'; import { type PanMode } from './panModeGate'; import type { LateralMotionModel } from './usePanMotion'; import { type GuidanceCopy } from './cameraGuidanceCopy'; import { type CaptureWarning } from './captureWarnings'; export type CaptureSource = 'ar' | 'non-ar'; /** * v0.13.2 — which capture sources the host ALLOWS. A constraint on top * of `defaultCaptureSource` (which picks the initial source within this * constraint): * 'both' — AR and non-AR both available; AR toggle is shown. * 'ar' — AR only; AR toggle hidden (nothing to switch to), and the * 0.5× lens chooser is hidden (ARKit/ARCore don't expose the * ultra-wide). * 'non-ar' — non-AR only; AR toggle hidden. */ export type CaptureSourcesMode = 'ar' | 'non-ar' | 'both'; export type CameraLens = '1x' | '0.5x'; export type StitchMode = 'auto' | 'panorama' | 'scans'; export type Blender = 'multiband' | 'feather'; export type SeamFinder = 'graphcut' | 'skip'; export type Warper = 'plane' | 'cylindrical' | 'spherical'; /** * Result emitted via `onCapture`. Discriminated union keyed FIRST on * `ok` (success vs. failure) and then on `type` (photo vs. panorama), so a * host handles EVERY capture outcome — success, degraded success, and * failure — through this one callback. * * ## v0.16 — unified success/failure + warnings (BREAKING) * * Previously `onCapture` fired only on success and carried no `ok` field; * failures went *solely* to `onError`. Hosts therefore had no single place * to learn whether a capture succeeded, and no programmatic signal that a * stitch was *degraded* (e.g. most frames dropped). Now: * * - `onCapture` ALWAYS fires once per capture attempt, with `ok:true` * (output present) or `ok:false` (carrying the `CameraError`). * - both success and failure carry `warnings: CaptureWarning[]` — non-fatal * quality signals (e.g. `LOW_FRAME_UTILIZATION` when <70 % of captured * frames survived, `LATERAL_DRIFT_FINALIZE` when item-6 stopped early). * - `onError` STILL fires on failure too (an unchanged mirror), so existing * error handling keeps working. * * Migration: gate on `ok` before reading `uri`/`width`/`height` — * `if (!result.ok) { handle(result.error); return; }`. * * Identifier `CameraCaptureResult` (vs. the SDK's existing `CaptureResult` * from `../types`) is intentional — the existing CaptureResult shape has * SDK-specific fields that don't belong in the public RN library's surface. */ export type CameraCaptureResult = { ok: true; type: 'photo'; uri: string; width: number; height: number; /** * iOS `captureDepthData` (NON-AR captures only) — path of the * `.depth.bin` depth sidecar saved next to the photo * (float32 metres row-major + JSON header with dims/intrinsics; * format spec in `website/docs/photo-depth.md`). Absent on * Android, in AR capture, on depth-less devices/formats, and * whenever the opt-in is off. */ depthPath?: string; /** * WHY `depthPath` is absent although `captureDepthData` was * requested (iOS non-AR): the extractor's reason slug * (`no-depth-aux` = no auxiliary depth in the capture — typically * a non-depth-capable mounted device; `native-module-missing` = * JS newer than the installed binary). Diagnostic only. */ depthUnavailableReason?: string; /** Non-fatal quality signals (empty when none). */ warnings: CaptureWarning[]; } | { ok: true; type: 'panorama'; uri: string; width: number; height: number; framesRequested: number; framesIncluded: number; framesDropped: number; finalConfidenceThresh: number; durationMs: number; /** * 2026-05-22 (audit F2g) — which cv::Stitcher pipeline the * batch finalize ran (after auto-resolution if applicable). * Useful for displaying a "Stitched as: scans" pill on the * output preview. Undefined when the engine wasn't * batch-keyframe (hybrid / slit-scan don't go through * cv::Stitcher at finalize). */ stitchModeResolved?: 'panorama' | 'scans'; /** * 2026-06-15 (DEV) — gyro rotation magnitude of the capture, in radians. * Shown on the dev preview so the panorama-vs-SCANS rotation threshold can * be tuned. `0` = no pose-derived rotation signal (non-AR with no poses). */ rRadians?: number; /** * 2026-06-16 (DEV) — translation magnitude (m) + auto decision ratio * (`>=0.55` → SCANS) that drove panorama-vs-SCANS. Shown on the dev * readout alongside `rRadians` to tune the threshold from real captures. */ tMeters?: number; decisionRatio?: number; /** * 2026-06-14 (DEV overlay) — semicolon-separated `key=value` trace of the * stitcher's runtime choices (pipe/warp/route/seam/blend) for this * output. Shown on the preview in __DEV__. iOS only for now. */ debugSummary?: string; /** * 2026-06-15 (iOS) — keyframe JPEG paths used for this stitch, so the * preview can re-stitch them on demand via `refinePanorama` (the * high-level tab). iOS only; undefined elsewhere. */ keyframePaths?: string[]; /** * 2026-06-15 (iOS) — orientation this stitch baked in. The on-demand * high-level re-stitch passes it back so it matches the manual output's * rotation (not the raw sensor landscape). iOS only. */ captureOrientation?: string; /** Non-fatal quality signals (empty when none). */ warnings: CaptureWarning[]; } | { ok: false; /** Which capture path failed. */ type: 'photo' | 'panorama'; /** The classified failure (same object handed to `onError`). */ error: CameraError; /** Any warnings gathered before the failure (usually empty). */ warnings: CaptureWarning[]; }; /** * The success-panorama variant of {@link CameraCaptureResult} — the exact * shape stashed for the crop editor and re-emitted (with adjusted dims) once * the user crops. Narrowed so the crop-confirm spread keeps `uri`/`width`/ * `height`/`ok` without a cast. */ export type PanoramaCaptureResult = Extract; /** * Errors surfaced via `onError`. Classified codes so consumers can * branch on the kind of failure (toast vs retry vs report). */ export type CameraErrorCode = 'CAMERA_PERMISSION_DENIED' | 'CAMERA_DEVICE_UNAVAILABLE' | 'PHOTO_CAPTURE_FAILED' | 'PANORAMA_START_FAILED' | 'PANORAMA_FINALIZE_FAILED' | 'STITCH_NEED_MORE_IMGS' | 'STITCH_HOMOGRAPHY_FAIL' | 'STITCH_CAMERA_PARAMS_FAIL' /** * v0.16 — the native post-stitch validator rejected the output: the * panorama came out disjoint / fragmented / wildly mis-proportioned * (frames didn't connect into one coherent image). Recoverable by * re-capturing, so it carries "try again" copy. */ | 'STITCH_LOW_QUALITY' | 'STITCH_OOM' | 'OUTPUT_WRITE_FAILED' /** * Vision-camera surfaced a runtime error that isn't a known * transient lifecycle event (those are swallowed inside the SDK's * ``). Examples that DO reach the host as this code: * `format/invalid-format`, `capture/recording-canceled`, * `device/microphone-permission-denied`, ... The full error * object is on `.cause` for inspection. */ | 'VISION_CAMERA_RUNTIME' | 'UNKNOWN'; export declare class CameraError extends Error { readonly code: CameraErrorCode; readonly cause?: unknown; constructor(code: CameraErrorCode, message: string, cause?: unknown); } /** * Frames-dropped info delivered via `onFramesDropped`. Fires once * per panorama capture if the native stitch retry (the flattened * 4-rung mode/threshold ladder since 2026-08-17) promoted a result * that dropped one or more input frames. */ export interface FramesDroppedInfo { requested: number; included: number; } /** * Camera component props. See the design doc's "Component API" * section for the full rationale per field. */ export interface CameraProps { /** * Initial capture source. Default `'non-ar'`. * * `'ar'` feeds the engine natively from the ARKit/ARCore session, so * it does NOT depend on the vision-camera frame-processor chain that * non-AR capture requires (see the "Frame processors" section of the * host-integration docs). If your fleet is AR-capable, opting in with * `defaultCaptureSource="ar"` — or locking it with `captureSources="ar"`, * which also hides the runtime AR toggle — avoids that whole class of * build-time integration failure. * * Caveats when choosing AR: on Android, devices without "Google Play * Services for AR" installed are prompted to install it, and a declined * install currently leaves the AR preview blank (no automatic downgrade * — fixed in a later release); AR tap-photos also come from the AR video * stream rather than the full-resolution still pipeline, the flash is * unavailable, and the iOS depth sidecar is not produced. * * Devices without AR support (and the 0.5× lens) always resolve to * non-AR regardless of this value. */ defaultCaptureSource?: CaptureSource; defaultLens?: CameraLens; defaultStitchMode?: StitchMode; defaultBlender?: Blender; defaultWarper?: Warper; defaultFlowNoveltyPercentile?: number; defaultFlowEvalEveryNFrames?: number; defaultFlowMaxTranslationCm?: number; defaultKeyframeMaxCount?: number; defaultKeyframeOverlapThreshold?: number; /** Time-budget force-accept (ms) for the keyframe gate — accept a * keyframe at least this often during a pan even if novelty is low, * so slow / static pans don't leave temporal gaps. `0` disables it. * Default 2000 (2 s). Applies to both AR and non-AR captures. */ defaultMaxKeyframeIntervalMs?: number; /** Forward-looking — wires through to cv::Stitcher's compositingResol * once PanoramaSettings exposes the field (currently a no-op). */ defaultCompositingResolMP?: number; /** Forward-looking — see above. */ defaultRegistrationResolMP?: number; /** Forward-looking — see above. */ defaultSeamEstimationResolMP?: number; /** * v0.16 — the stitch RECIPE as a JSON object (`stitchMode` / `warperType` / * `blenderType` / `enableMaxInscribedRectCrop` / `debugPack`). Partial; wins * over the flat `default*` props. v0.24 — the speed levers moved to {@link * perf}. */ stitcher?: PanoramaPropOverrides['stitcher']; /** * v0.16 — the keyframe GATE as a JSON object (`mode` / `maxKeyframes` / * `overlapThreshold` / `maxKeyframeIntervalMs` / `flow`). Partial; `flow` is * deep-merged. v0.24 — the anti-blur controls moved to {@link blur}. */ frameSelection?: PanoramaPropOverrides['frameSelection']; /** * v0.24 — **anti-blur** controls in one group: `sharpnessWindow` (pick- * sharpest-of-K) + exposure cap + motion gate + sharpness floor + hi-fps * format. Deep-merged over the SDK defaults (all ON). Set a knob to 0 / * false (or `sharpnessWindow: 1`) to disable it. */ blur?: PanoramaPropOverrides['blur']; /** * v0.24 — **perf** (stitch-speed) levers in one group: `seamFinderType` * (default 'voronoi'), `rangeMatcherWidth` (3), `numThreads` (0 = multi), * `adaptiveStitchMode` ('measured') + its `adaptiveMinOutputMP` / * `adaptiveSlowStitchMsPerFrame`. Wins over `stitcher`. */ perf?: PanoramaPropOverrides['perf']; /** * Crop strategy for the stitched panorama. `false` (default) keeps the * bounding-rect of non-black pixels, which preserves all stitched * content but may leave black corners. `true` crops to the maximum * axis-aligned rectangle inscribed in the coverage mask — clean edges, * no black corners (slightly more CPU at finalize) — but it can shrink * the output substantially on lopsided / ultra-wide masks, which is why * it's opt-in. * * Implemented as a start-time stitcher config (like the other * stitcher settings), so this value is read once at mount to seed the * initial setting; the in-app settings modal can override it at * runtime. It changes image geometry (the crop), not encoding. * * Since the default is `false`, only pass this prop to opt in: * @example * // Crop to a clean inscribed rectangle (no black corners): * */ maxInscribedRectCrop?: boolean; /** * Default `true`. Set `false` to disable single-TAP photo capture * entirely (`handleTap` no-ops, same as `shutterDisabled`) and hide the * native-0.5× external-camera fallback (`offerNativeUW`) — that * fallback captures ONE still via the OS camera, the same shape of * action this flag disables. * * PANO-ONLY RECIPE: `enablePhotoMode={false}` with `enablePanoramaMode` * left at its default `true` is already a pano-only `` — tap is * disabled, hold-to-pan still fires a capture. No separate flag needed. * Lens switching (the 0.5×/1× chip) is unaffected either way — it * selects which device the eventual hold-to-pan uses, it does not * itself capture. */ enablePhotoMode?: boolean; enablePanoramaMode?: boolean; /** * Hide the built-in shutter + AR-toggle so a HOST can render its own capture * controls and drive capture through the imperative handle * ({@link CameraHandle.takePhoto} / {@link CameraHandle.startPanorama} / * {@link CameraHandle.stopPanorama}). The lens chip is KEPT — it is a lens * selector, not a capture control, and the whole point is that the library * still owns lens selection. Default `false` (built-in shutter shown, every * existing consumer unchanged). */ hideBuiltInShutter?: boolean; /** * When a device offers only ONE usable lens (no in-app ultra-wide and no * native-0.5× fallback on offer), render NOTHING instead of a static "1×" * label — there is nothing to switch, so the chip is noise. Default `false` * keeps the "1×" label for back-compat. */ hideLensChipWhenSingle?: boolean; /** * Lift the bottom control cluster (lens chip + built-in shutter, if shown) by * this many px, so a host chrome docked below the preview (e.g. a mode * switcher) doesn't overlap it. Default `0`. Layout-only; no behaviour change. */ bottomBarOffset?: number; showSettingsButton?: boolean; /** * v0.13.2 — which capture sources the host allows (default `'both'`). * Constrains both the runtime AR toggle and `defaultCaptureSource`: * - `'both'` : AR + non-AR; the AR toggle is shown so the user can * switch at runtime. * - `'ar'` : AR only. AR toggle hidden (nothing to toggle); the * 0.5× lens chooser is also hidden (ARKit/ARCore can't use the * ultra-wide), so the camera stays on the AR-capable 1× lens. * - `'non-ar'`: non-AR only. AR toggle hidden. * When set to a single source, that source wins regardless of * `defaultCaptureSource`. */ captureSources?: CaptureSourcesMode; style?: StyleProp; /** * Which stitcher engine to drive. Only `'batch-keyframe'` is * supported (and the default): it collects accepted keyframe JPEGs * during the hold-pan-release capture and runs the stitch once at * finalize. The live engines (hybrid / slit-scan / firstwins) were * archived in the batch-keyframe cleanup — see `archive/`. */ engine?: 'batch-keyframe'; /** * Optional destination directory for captures. When set, the lib * lands tap-photos at `${outputDir}/photo-${ts}.jpg` and panoramas * at `${outputDir}/panorama-${ts}.jpg` and the returned uri points * at the persisted file (vs. vision-camera's tmp dir, which is * what you get when this prop is omitted). * * The host is solely responsible for: * - Choosing a writable directory (the lib does NOT pick this for * you on either platform — particularly relevant on Android, * where scoped-storage rules differ between app-private storage * and user-visible Documents/Pictures dirs). * - Ensuring the directory exists. The lib will create it if it * doesn't, but only inside paths the OS lets it write to. * - Making the path user-visible if that matters (`UIFileSharingEnabled` * on iOS for `FileSystem.documentDirectory`; MediaStore / * `Documents/...` on Android — see your platform's docs). * * On disk failure the capture promise rejects via `onError` with * `CameraError('OUTPUT_WRITE_FAILED', ...)`. No silent fallback to * tmp — that hides bugs. * * Requires `expo-file-system` (declared as an OPTIONAL peer dep; * only needed when this prop is set). * * Format: bare path or `file://` URI. Both accepted. */ outputDir?: string; /** * Disable the shutter — taps + holds are ignored and the button paints in * its disabled visual. The host drives this for capture-gating use cases: * e.g. a document scanner that only allows capture once the document fills * the framing guide, or a fixture flow that has reached its max photo count. * Independent of the SDK's own stitching-in-progress disable. Default * `false`. */ shutterDisabled?: boolean; onCapture?: (result: CameraCaptureResult) => void; onCaptureSourceChange?: (source: CaptureSource) => void; onLensChange?: (lens: CameraLens) => void; onFramesDropped?: (info: FramesDroppedInfo) => void; onError?: (err: CameraError) => void; /** * v0.12.0 — fires when the SDK auto-abandons an in-progress * capture without producing output. `reason` is a string union * so future reasons (network loss, low memory, etc.) can be added * without breaking the callback signature. * * Currently the only reason in v0.12 is `'orientation-drift'`: * the user rotated the device between Mode A (landscape + vertical * pan) and Mode B (portrait + horizontal pan) mid-capture. The * engine docstring at `incremental.ts:373-403` is explicit that * cross-mode capture is "best-effort, not supported," so the SDK * decisively cancels the capture (`incremental.cancel()`) and * surfaces `OrientationDriftModal` to explain what happened. * * v0.16 adds `'lateral-drift'`: the user moved the phone perpendicular to * the pan arrow before enough frames were captured to stitch. Rather than * finalize into a misleading "need more images" error, the SDK abandons the * capture and surfaces the `LateralMotionModal` with "follow the arrow" * copy. (A lateral drift AFTER enough frames still finalizes what was * captured and fires `onCapture` with a `LATERAL_DRIFT_FINALIZE` warning.) * * Hosts use this callback to clean up their own state (e.g., reset * a wizard step, log telemetry, surface their own retry UX in * addition to the SDK's built-in modal). No `onCapture` will fire * for an abandoned capture. */ onCaptureAbandoned?: (reason: 'orientation-drift' | 'lateral-drift') => void; /** * v0.13.0 — flash (torch) state. Controlled-or-uncontrolled. * * - **Uncontrolled** (omit `flash`): `` owns the flash * state internally. Tapping the built-in flash button toggles * it on/off. `onFlashChange` (if supplied) fires for telemetry. * - **Controlled** (supply `flash`): the parent owns the state. * The built-in button still renders and fires `onFlashChange` * on press, but it's a no-op unless the parent updates `flash` * in response. * * Both shapes coexist with the v0.13 "flash button is on by default" * built-in (see the bottom-left bar slot in the JSX). Hosts that * want their own flash chrome can opt out via `showFlashButton={false}` * and drive the underlying torch by controlling `flash` directly. * * ## AR-mode behaviour * * In AR mode (`defaultCaptureSource="ar"` or runtime-toggled), * ARKit / ARCore own the `AVCaptureDevice` and don't expose the * torch through vision-camera's pipeline. The built-in flash * button renders as visibly disabled (a11y label "Flash unavailable * in AR mode") and `flash` is forced to `'off'` regardless of * controlled/uncontrolled state. Hosts that need flash should * toggle to non-AR before enabling. */ flash?: 'on' | 'off'; /** * v0.13.0 — fires when the user taps the built-in flash button. * In uncontrolled mode, the internal state has already flipped * (single render delay). In controlled mode, the parent must * update the `flash` prop in response or the visual toggle is * a no-op. Useful in either mode for telemetry. */ onFlashChange?: (next: 'on' | 'off') => void; /** * v0.13.0 — show the built-in flash button in the bottom-left * slot. Defaults to `true`. Hosts that render their own flash * chrome (and drive the underlying torch via the controlled * `flash` prop) can opt out by setting this to `false`. */ showFlashButton?: boolean; /** * v0.13.0 — built-in CaptureHeader title. When set, `` * renders a top-of-screen header showing this title (centred) * with an optional back affordance + guidance subtitle + the * existing settings gear absorbed into the header's right side. * * When `headerTitle` is undefined the header is not rendered * (matches pre-v0.13 behaviour: top of preview is bare except * for the standalone settings gear gated on `showSettingsButton`). * * Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`, * and `headerColors` to customise the rest of the header. Hosts * that need richer header chrome can omit `headerTitle` and * compose their own `` above ``. */ headerTitle?: string; /** * v0.13.0 — header back-button callback. When supplied (and * `headerTitle` is set), the header renders a back affordance * on the left. Omitted ⇒ no back button (the title stays * centred). */ onHeaderBack?: () => void; /** * v0.13.0 — header back-button label. Defaults to "‹ Back". * No effect unless `headerTitle` and `onHeaderBack` are both set. */ headerBackLabel?: string; /** * v0.13.0 — optional second-line subtitle shown below the * header title. E.g. "Photograph the promotional cola end cap." * Renders nothing when undefined. No effect unless `headerTitle` * is set. */ headerGuidance?: string; /** * v0.13.0 — colour overrides for the built-in header. Defaults * are white-on-black to stay legible over the camera preview. * No effect unless `headerTitle` is set. */ headerColors?: CaptureHeaderProps['colors']; /** * v0.13.0 — when provided (even as `[]`), `` renders a * built-in `CaptureThumbnailStrip` above the bottom controls * showing the host's capture history. Each item is a plain * `{ id, uri, width?, height? }` object; the strip handles * aspect-ratio rendering, tap-to-preview, and the count line. * * Omit (`undefined`) to skip the strip entirely. Hosts using * the strip independently (e.g. on a non-camera screen) can keep * importing `CaptureThumbnailStrip` directly from the library — * the prop here is the convenience wiring for in-`` use. * * Captures emitted by ``'s `onCapture` are NOT added to * this array automatically — the host owns the canonical list * (typically persisted to its own DB) and updates the prop in * response. This matches the SDK's "Camera owns runtime state, * host persists" pattern. */ thumbnails?: CaptureThumbnailItem[]; /** * v0.13.0 — minimum-photos hint for the count line. Renders * "n / minPhotos min" with the success colour when reached, * warning colour otherwise. */ thumbnailsMin?: number; /** * v0.13.0 — maximum-photos hint for the count line. Renders * "· maxPhotos max" suffix. No enforcement — the host decides * what to do at the cap. */ thumbnailsMax?: number; /** * v0.13.0 — tap handler for thumbnails. When set, replaces the * strip's built-in tap-to-preview modal; the host shows its own * preview UI (e.g. with delete / recapture buttons gated on * sync state). Omit to use the built-in preview. */ onThumbnailPress?: (item: CaptureThumbnailItem) => void; /** * v0.13.0 — when set, `` renders a built-in `CapturePreview` * modal as `visible`. Use this for post-stitch confirmation: * after `onCapture` emits, the host stores the result and sets * `capturePreview` to the new image, with `capturePreviewActions` * = `[Discard, Save]` (or similar). Setting `undefined` hides * the modal. * * Hosts using the modal for thumbnail tap-to-preview can leave * this undefined and let the built-in strip's preview handle * that case. */ capturePreview?: { imageUri: string; imageWidth?: number; imageHeight?: number; title?: string; }; /** * v0.13.0 — action buttons rendered along the bottom of the * `CapturePreview` modal. Empty array (or undefined) renders * no buttons, only the close affordance. */ capturePreviewActions?: CapturePreviewAction[]; /** * v0.13.0 — fires when the user dismisses the `capturePreview` * modal (tap close, backdrop tap, hardware back on Android). * The host is expected to clear the `capturePreview` prop in * response. */ onCapturePreviewClose?: () => void; /** * Optional host-supplied vision-camera frame processor. * * ## When to set this prop * * v0.8.0+ canonical answer: use the lib's own `useFrameProcessor` * hook, NOT `react-native-vision-camera`'s. The lib's hook: * * - **AR mode**: auto-registers the worklet in the native * `__stitcherProxy` registry; the AR session's per-frame * dispatch fans out to it alongside the lib's first-party * stitching. No prop wiring needed — just mount the hook * anywhere in the tree. * - **Non-AR mode**: returns a vc processor object that this * prop accepts. Wiring it through enables the host's * worklet to fire on vc's Frame Processor runtime. * * ```tsx * import { Camera, useFrameProcessor, type CameraFrame } * from 'react-native-image-stitcher'; * * function MyScreen() { * const fp = useFrameProcessor((frame: CameraFrame) => { * 'worklet'; * // ... * }, []); * return ; * } * ``` * * ## Non-AR mode composition (v0.11.0+) * * vision-camera's `` accepts ONLY ONE frame processor. * The lib's internal `useFrameProcessorDriver` produces the * processor that drives first-party panorama stitching in non-AR * mode. If you supply your own via this prop, the lib's * default processor is REPLACED — but as of v0.11.0 you can * COMPOSE first-party stitching back into your worklet body * using `useStitcherWorklet`: * * ```tsx * import { * Camera, useFrameProcessor, useStitcherWorklet, * type CameraFrame, * } from 'react-native-image-stitcher'; * * function MyScreen() { * const stitcher = useStitcherWorklet(); * const fp = useFrameProcessor((frame: CameraFrame) => { * 'worklet'; * hostPreLogic(frame); * stitcher.call(frame); // ← first-party stitching * hostPostLogic(frame); * }, [stitcher.call]); * return ; * } * ``` * * Hosts that DON'T call `useStitcherWorklet` from their worklet * body replace first-party stitching for non-AR captures (a * one-shot console.info documents this when the prop is first * supplied). AR mode is unaffected either way — the AR-mode * dispatch path (v0.8.0 Phase 4b.i / 4b.iii) natively fans out * to both the lib's first-party stitching AND every registered * host worklet on every frame, with per-worklet failure * isolation. * * ## AR mode behaviour * * In AR mode (`defaultCaptureSource="ar"` or runtime-toggled), * vc's `` isn't mounted; this prop has no effect. * Host worklets registered via the lib's `useFrameProcessor` * fire automatically through the AR-session dispatch path * (iOS Phase 4b.i / Android Phase 4b.iii). * * ## Backwards compatibility * * The pre-v0.8.0 behaviour (warn + ignore) is preserved when the * supplied processor is recognisably from * `react-native-vision-camera`'s `useFrameProcessor` directly * (no `__stitcherFrame` marker). Hosts should migrate to the * lib's `useFrameProcessor` to benefit from AR-mode dispatch. * * (v0.5 had a `legacyDriver` escape hatch that routed back to * `useIncrementalJSDriver`. That hook + prop were removed in * v0.6 per the deprecation timeline announced in the v0.5.0 * CHANGELOG.) */ frameProcessor?: ReadonlyFrameProcessor | DrawableFrameProcessor; /** * AR-mode host worklet, invoked once per ARKit / ARCore frame * ALONGSIDE the lib's first-party stitching (composition, not * replacement). Receives a `CameraFrame` tagged `source: 'ar'` * with world-space `pose` + `arTrackingState`. Only fires in AR * capture (`captureSource === 'ar'`); the non-AR equivalent is * `frameProcessor` above (the two modes use different runtimes and * frame shapes). Must be a `'worklet'`-prefixed function; if the * native install is unavailable it silently never fires. */ arFrameProcessor?: CameraFrameProcessor; /** * Opt in to per-frame AR depth on the `arFrameProcessor` frame * (`CameraFrame.arDepth`). Default `false` — depth is the costliest * field (a per-frame buffer copy), so it's off until you need it. */ enableDepth?: boolean; /** * Opt in to high-resolution photo capture (iOS 16+, AR capture path). * When `true`, the AR session runs on the smallest video format that * supports `captureHighResolutionFrame`, so `takePhoto()` returns a true * full-res still (for document OCR / detail capture). Default `false` — * the live stream stays as small as possible; the panorama-stitch path is * unaffected (its keyframes are downscaled to a fixed budget regardless). * No-op on Android. */ highResCapture?: boolean; /** * PANORAMA-QUALITY keyframes (Android; see `ARCameraView` prop of the * same name): larger ARCore CPU-image config (long-edge ≤ 1920) + a * lifted keyframe encode budget (640 → 1280) so stitches stop being * assembled from 0.3 MP tiles on devices whose sole 4:3 config is tiny * (e.g. Galaxy A35). Costs stitch memory (~4× pixels per keyframe). * Default `false`. No-op on iOS (native-res keyframes already) and on * older binaries. */ keyframeQualityCapture?: boolean; /** * Native-camera 0.5× fallback (Android; default OFF). A list of device * MODEL identifiers (matched case-insensitively as a PREFIX of * `Platform.constants.Model`, so `"SM-A346"` covers every A34 SKU) or * `"manufacturer:"` wildcards, on which the ultra-wide is a * SYSTEM-ONLY camera unreachable by any third-party app (proven on the * Galaxy A34). On a matching device that ALSO has no in-app 0.5× * (`has0_5x=false`), the lens chip renders a "0.5×⤢" pill that fires * {@link onRequestNativeUltraWide} instead of a dead 1× label — the host * then hands off to the OS camera. Absent/empty → feature OFF, no change. * iOS ignores this (its virtual devices already reach the ultra-wide). */ nativeUltraWideModels?: readonly string[]; /** * Fired when the operator taps the "0.5×⤢" native-ultra-wide fallback pill * (see {@link nativeUltraWideModels}). The host launches the OS camera * (e.g. `react-native-image-picker` `launchCamera`) and routes the * returned photo into its own capture flow marked as external provenance — * the library does NOT launch anything or deliver the external photo. */ onRequestNativeUltraWide?: () => void; /** * iOS, NON-AR photo path — save each tap photo's AVDepthData as a * `.depth.bin` sidecar (float32 metres row-major + JSON header * with dims/intrinsics) and return its path as `depthPath` on the * photo {@link CameraCaptureResult}. Enables vision-camera depth * delivery, biases the format pick toward `supportsDepthCapture` * formats, and extracts the depth BEFORE the orientation re-encode * strips it. Produces stereo disparity-derived depth on dual-camera * iPhones and absolute LiDAR-backed depth on Pro models; requires the * mounted device to be depth-capable (the lens-driven multicam * selection qualifies — a plain single wide-angle does not). Silently * yields no sidecar on Android, in AR capture, and on depth-less * hardware. Distinct from `enableDepth` above, which is the AR * frame-processor's per-frame depth. Default `false` (depth delivery * adds per-shot latency). */ captureDepthData?: boolean; /** * Opt in to per-frame AR anchors (`CameraFrame.arAnchors` — detected * planes / images). Default `false`. */ enableAnchors?: boolean; /** * Opt in to scene-reconstruction mesh anchors (`type: 'mesh'` in * `arAnchors`, with `meshGeometry`). Default `false`. iOS enables * ARKit `sceneReconstruction` (LiDAR); Android reconstructs a rough * mesh from the depth map. Expensive — only on when needed. */ enableMesh?: boolean; /** * Opt in to the SLAM feature-point cloud in AR plugin contexts. Default * `false`. Available on ALL AR-capable devices — no LiDAR required. * Consumed natively by AR plugins only; does not appear in * {@link ARFrameMeta} or `CameraFrame`. * * - iOS → ARKit `rawFeaturePoints` in `RNISARFrameContext.featurePoints` * as world-space `[simd_float3]` (bare `x, y, z`). * - Android → ARCore `Frame.acquirePointCloud()` in * `ARFrameContext.featurePoints` as a flat stride-4 * `[x, y, z, confidence]` world-space `FloatArray` (the extra * per-point confidence lets native plugins filter ARCore's * sparser cloud). */ enableFeaturePoints?: boolean; /** * Which plane orientations to surface in `CameraFrame.arAnchors` * (requires `enableAnchors`; AR capture only). Default `'vertical'` * — the orientation the plane-projected stitch path has always used. * `'horizontal'` surfaces floors / tables; `'both'` surfaces every * detected plane. See `ARCameraView` for the per-platform details. */ planeDetection?: 'vertical' | 'horizontal' | 'both'; /** * v0.18.0 — LIGHT per-frame AR metadata callback, invoked on the JS * MAIN thread (NOT a worklet). Only fires in AR capture * (`captureSource === 'ar'`). Receives an {@link ARFrameMeta} carrying * pose, tracking state, intrinsics, and (when the matching `enable*` * prop is on) depth dimensions, anchors, and mesh counts. * * This is the recommended way to read AR metadata: it sidesteps the * worklet path entirely (the `arFrameProcessor` worklet can only safely * surface a worklets-core shared value, because capturing a host * callback crashes the worklet closure-wrap). Native builds the meta * and emits a device event; `` threads the handler through to * ``, which subscribes and invokes it on the main thread. */ onArFrame?: (meta: ARFrameMeta) => void; /** * v0.18.0 — throttle interval (ms) for {@link onArFrame}. Default `100` * (≈ 10 Hz). No effect unless `onArFrame` is provided. */ arFrameMetaInterval?: number; /** * v0.19.0 — ASYNCHRONOUS AR-plugin result callback (the AR plugin * framework), invoked on the JS MAIN thread (NOT a worklet). Only fires in * AR capture (`captureSource === 'ar'`). Host-registered native plugins * (see `RNISARPluginRegistry` / `RNSARPluginRegistry`) that offload heavy * per-frame work to their own queue push results via * `registry.emit(name, result)`; `` threads this handler to * ``, which subscribes to the `RNImageStitcherARPluginResult` * device event and invokes it with `{ plugin, result }`. * * SYNCHRONOUS plugin results (computed inline on the AR thread) instead ride * the throttled {@link onArFrame} event on {@link ARFrameMeta.plugins}. * Use `onArFrame` for the in-band sync channel and `onArPluginResult` for * the out-of-band async channel — a host can wire either or both. * * The SDK ships ONLY the generic plugin framework; there are no built-in * plugins, so this never fires unless the host registers native plugins. */ onArPluginResult?: (e: ARPluginResult) => void; /** * v0.20.0 — AR OVERLAY / ANNOTATION renderer. A declarative array of 2D * shapes drawn ON TOP of the AR camera preview, each anchored to WORLD * positions and REPROJECTED to screen on every AR frame from the current * camera pose + intrinsics (smooth display-rate tracking, no 3D engine). * Only meaningful in AR capture (`captureSource === 'ar'`); `` * threads this straight through to the underlying ``. * * State-driven: pass a React-state array and update it as your world points * change (e.g. from {@link CameraProps.onArFrame} plane anchors). The set is * diffed against the current overlays BY `id`. For zero-render-latency * mutations use the imperative ref methods on the `` handle instead * ({@link CameraHandle}: `setOverlays` / `addOverlay` / `updateOverlay` / * `removeOverlay` / `clearOverlays`) — both paths funnel through the same * native channel. JS-set overlays merge on the native side with overlays a * registered AR plugin placed directly (namespaced so neither clobbers the * other). See {@link AROverlay} for the shape. */ overlays?: AROverlay[]; /** * Which device holds the non-AR panorama capture accepts. * * - `'vertical'` (DEFAULT) — LANDSCAPE-only, top→bottom pan. Starting a * panorama in portrait is BLOCKED behind the rotate-to-landscape * prompt (item 2); the capture starts the instant they rotate to * landscape (either way up). * - `'horizontal'` — PORTRAIT-only, left→right pan. Starting in * landscape is BLOCKED behind the rotate-to-portrait prompt; capture * starts on rotating to portrait (either way up). * - `'both'` — landscape OR portrait; the rotate gate never fires, the * user captures in whichever hold they're already in. * * **BREAKING (since the previous release accepted both holds ungated):** * the default is now `'vertical'`. Hosts that want left→right (portrait) * panoramas use `panMode='horizontal'` (portrait-only) or `'both'`. See * CHANGELOG. */ panMode?: PanMode; /** * Master switch for the in-capture pan-guidance surfaces (rotate * prompt, pan how-to overlay, too-fast pill, blinking countdown). * Default `true`. Set `false` to suppress all of them (the lateral- * drift FINALIZE behaviour and the crop preview are governed by their * own props, not this flag). */ panGuidance?: boolean; /** * Optional hard recording-TIME ceiling for a non-AR panorama, in * milliseconds, used as a SAFETY cap alongside the primary keyframe-count * auto-stop. The default capture now finalizes when the configured * keyframe count is reached (see the frame counter HUD), so this is `0` * (disabled) by default. Set it to a positive value to ALSO cap the * recording by wall-clock time; when > 0 a blinking countdown (item 5) * shows the seconds remaining and the capture auto-finalizes at 0. * * v0.16 — default changed `9000` → `0` (time cap is now opt-in; the * keyframe-count stop is the default UX). */ maxPanDurationMs?: number; /** * Gyro rate (rad/s) above which the pan is flagged "moving too fast" * (item 4 — the transient amber pill). Optional; forwards to * `usePanMotion`'s `warnMaxRadPerSec` (default 1.0 rad/s there). */ panTooFastThreshold?: number; /** * Cross-pan (lateral) drift budget in CENTIMETRES (item 6). Once the * operator's integrated sideways translation exceeds this for the * hook's grace window, the capture is STOPPED. * * **Default `8`** (v0.25.3 — was `4`). `0` disables the lateral-drift * stop entirely. * * This is the SENSITIVITY knob: it decides how much sideways drift is * tolerated before a capture is stopped at all. What HAPPENS at that * stop — finalize the partial sweep, or discard it — is a separate * decision controlled by `lateralStopFinalizeMinFrames`. The two are * easy to confuse when tuning: if operators complain the stop fires * too eagerly, raise this; if they complain that stopped captures are * thrown away, lower that one. * * v0.25.3 raised the default from `4` after field reports of the stop * firing on minor drift. 4 cm of integrated sideways translation is * a small movement to hold to over a hand-held sweep — comfortably * inside the natural arc of pivoting on the spot — so it tripped on * captures the operator considered fine. The detector itself is * unchanged; only the budget it is measured against moved. */ lateralBudgetCm?: number; /** * Cross-pan ROTATION rate, rad/s, above which the capture is stopped * for lateral drift. Defaults to `DEFAULT_LATERAL_TURN_RAD_PER_SEC` * (0.15 rad/s ≈ 8.6 °/s) — unset reproduces today's behaviour. * * `lateralBudgetCm` is NOT the only lateral trigger. This gyro EMA * is a second, independent one, and historically the primary. A stop * you attribute to "drifting sideways" may be this rotation trigger * instead — check `latch=gyro|accel` in the `[panMotion]` telemetry * (see `panMotionDebug`) before tuning either number. * * `0` (or negative) disables THIS trigger only; `lateralBudgetCm={0}` * disables both. */ lateralTurnRateRadPerSec?: number; /** * Continuous over-threshold dwell, ms, before the ROTATION trigger stops the * capture. Default 500 ms — matching the displacement trigger, which has * always had one. * * Before v0.26.0 the rotation trigger latched on the FIRST sample over * threshold, so one brief wobble ended a capture permanently. `0` comes as * close to restoring that as the shared latch helper allows — it still costs * one gyro sample (~33 ms), because the dwell clock starts on the first * over-threshold sample and latches only on a later one. */ lateralTurnGraceMs?: number; /** * Absolute cross-pan ANGLE, DEGREES, at which the capture is stopped. * Default 25; `0` disables while still measuring. Works in BOTH AR and * non-AR (gyro-integrated), unlike `arLateralRotDeg` which needs the pose. * * Catches the slow pivot `lateralTurnRateRadPerSec` cannot: that is a rate * gate, so 6 deg/s turns 90 degrees over 15 s without tripping it. */ lateralTurnAngleDeg?: number; /** * ABSOLUTE cross-pan drift budget in CENTIMETRES, measured from the AR * camera POSE. AR captures only. Default 8 cm; `0` disables the stop * while still measuring and logging the distance. * * A genuine displacement, unlike `lateralBudgetCm`, which gates a * high-passed rate proxy. Recovering distance from an accelerometer needs * double integration, whose bias growth must be high-passed away — and that * same high-pass removes slow real motion, so the IMU guard structurally * cannot see slow sideways drift however it is tuned. ARKit's VIO position * needs no integration, so it can; and tilt cannot masquerade as * translation, because a position is not an acceleration. * * The axis is chosen so the sweep's own ARC never projects onto it — a * vertical pan measures the horizontal cross-view direction, a horizontal * pan measures world up — so an operator pivoting cleanly in place reads ~0 * regardless of sweep angle or arm length (measured < 5 mm over an 0.8 rad * sweep at pivot radii of 15-60 cm). */ arLateralBudgetCm?: number; /** * ABSOLUTE cross-pan ROTATION budget in DEGREES, from the AR camera pose. * AR captures only. Default 25 deg; `0` disables the stop while still * measuring. * * Closes the slow-pivot hole. `lateralTurnRateRadPerSec` is a RATE gate * (0.15 rad/s = 8.6 deg/s), so it cannot see a slow turn however far it * goes: 6 deg/s accumulates 90 DEGREES of yaw over 15 s and never trips it. * That is the rotation twin of the slow-translation blind spot — a rate gate * measures how FAST you turn, never how FAR you have turned. * * Measured on the camera's forward VECTOR, so the intended sweep contributes * nothing (a vertical pan measures azimuth, a horizontal pan elevation) and * ROLL about the view axis contributes nothing either — roll does not change * where the camera points, and it is already the motion that corrupted the * accelerometer channel. */ arLateralRotDeg?: number; /** * Cross-pan allowance as a RATIO of along-pan travel (AR only). * * A fixed centimetre budget cannot distinguish 6 cm of drift across a 60 cm * sweep (10 % — still overwhelmingly a pan, and about as straight as a hand * gets over half a metre) from the same 6 cm across a 10 cm one (60 % — not * a pan at all). The effective budget is therefore * `clamp(ratio * alongPanDistance, arLateralBudgetCm, arLateralMaxCm)`. * * `arLateralBudgetCm` becomes the FLOOR: short sweeps, and the opening of * every capture where along-pan travel is still ~0, keep exactly the old * absolute behaviour. `<= 0` disables the proportional term entirely. */ arLateralRatio?: number; /** Ceiling on the ratio allowance, CENTIMETRES. `<= 0` = uncapped. */ arLateralMaxCm?: number; /** * Which lateral-drift physics to run. Default `'fused'`. * * `'fused'` subtracts the device's FUSED GRAVITY SENSOR from each * accelerometer sample and derives the integration step from the * sample's own timestamp. `'legacy'` restores the pre-0.25.4 * behaviour bit-for-bit: an IIR gravity estimate and a hardcoded * 20 ms step. * * You almost certainly want the default. The legacy estimator cannot * distinguish a wrist TILT from a sideways SLIDE — a change in how * gravity projects onto the cross-pan axis is arithmetically * identical to real lateral acceleration — so it fabricates ~1.1 cm * of drift per degree of net tilt and latches the stop on ordinary * hand movement. This prop exists as an ESCAPE HATCH so a host that * hits an unexpected device-specific regression can back out without * pinning an old version of the library, not as an opt-in gate. * * Degrades on its own: if the device has no fused gravity sensor, or * it stops delivering mid-capture, the hook falls back to the legacy * estimator automatically for exactly as long as it needs to. */ lateralMotionModel?: LateralMotionModel; /** * Emit the throttled `[panMotion.*]` diagnostic logs. Default * `__DEV__`, i.e. unset behaves exactly as before. Set `true` to keep * them in a release build while diagnosing a field report — they are * the intended instrument for tuning `lateralBudgetCm` against real * captures. */ panMotionDebug?: boolean; /** * The accepted-keyframe count at or above which a lateral-drift stop * (item 6) FINALIZES the capture: the partial sweep is stitched and handed * to `onCapture` with a `LATERAL_DRIFT_FINALIZE` warning. BELOW it the * capture is DISCARDED instead — the engine is cancelled, nothing is * stitched, and `onCaptureAbandoned('lateral-drift')` fires. * * **Default `5`. This is a BEHAVIOUR CHANGE, not a no-op** — the SDK used * to hardcode `2`, so captures that accepted 2-4 keyframes previously * finalized and now DISCARD. That is intentional: a 2-to-4-frame remnant * of a sweep the operator drifted out of is not a usable shelf panorama, * and asking for a clean re-shoot beats handing a host output it has to * detect and reject downstream. **Pass `2` to restore the old * behaviour exactly.** * * - `0` — **ALWAYS DISCARD**, however many keyframes were accepted. This * is a genuine special case and NOT the `count >= 0` the arithmetic * would otherwise give you (that is unconditionally true, i.e. the * opposite). For hosts whose downstream pipeline treats any drifted * sweep as garbage, discarding costs nothing and saves the stitch, the * file, and the operator's attention on output they will bin anyway. * - `N >= 1` — finalize iff `acceptedKeyframeCount >= N`. * - `2` — the pre-policy behaviour: keep anything stitchable. * * Negative, `NaN` and infinite values normalise to the default, so a broken * host config degrades to the standard threshold rather than to "throw * every capture away"; fractional values round UP, as the prop counts whole * frames. * * The discard path shows a THIRD popup state whose copy does not promise a * stitch — `lateralStopDiscardedTitle` / `lateralStopDiscardedBody` in * {@link guidanceCopy}. At the default that state covers the 2-to-4 * keyframe band. Below 2 accepted keyframes nothing stitchable was * captured at all, so the popup keeps the existing "follow the arrow" * wrong-direction copy regardless of this threshold. */ lateralStopFinalizeMinFrames?: number; /** * v0.25 — whether a mid-capture device rotation auto-ABANDONS the * in-flight panorama (the OrientationDriftModal explains it to the * user). Default `true` (the behaviour since v0.12). Set `false` * to disable the guard entirely: the capture then continues across a * rotation and the output is best-effort (cross-mode captures can * stitch malformed — see `incremental.ts` stitch-mode notes). * * The detector itself is sensor-trust hardened as of v0.25: it never * snapshots or compares orientation until the accelerometer has * delivered a real sample, so hosts with broken/laggy * react-native-sensors delivery no longer see phantom "rotation" * abandons (field RCA: landscape captures auto-abandoning after one * frame while portrait worked). */ orientationDriftAbandon?: boolean; /** * v0.25 — the keyframe count below which a finished capture is flagged * with the `CAPTURE_TOO_SHORT` capture WARNING. **Default `1`, which * never warns** and reproduces the previous behaviour exactly. * * Set `2` to be told when a capture produced a single frame. The * capture still SUCCEEDS and still returns that frame — a one-shot * capture is a legitimate result — but it is no longer silent. That * silence is why the AR self-ending-hold failures were reported as * stitching bugs: the SDK returned the lone frame as an ordinary * panorama with `singleKeyframe: true` that nothing read. * * Evaluated from the FINALIZE RESULT, not from the live accepted count. * The live count omits any keyframe whose anti-blur sharpness window is * still open at release — the trailing keyframe of nearly every capture * — and it means different things on iOS and Android, so judging * "too short" from it would misfire constantly. An earlier draft of * this feature did exactly that and would have destroyed valid * captures; adversarial review caught it. */ minPanoramaKeyframes?: number; /** * Show the draggable-quad crop editor after a panorama finalizes, BEFORE * emitting it via `onCapture`. Default `false`. When `true`, the user * drags 4 corners over the stitched result; confirming crops in place * (perspective-rectify when the quad isn't axis-aligned), "Use original" * emits the un-cropped panorama, "Retake" discards it. Takes precedence * over {@link showPreview}. */ rectCrop?: boolean; /** * Show a plain review screen after a panorama finalizes — the stitched * image with [Retake] / [Confirm] and NO crop box. Default `false`. * Ignored when {@link rectCrop} is on (the crop editor is itself the * preview). With both off, `onCapture` fires immediately with no UI. */ showPreview?: boolean; /** * Copy overrides for every guidance string (rotate prompt, pan hint, * too-fast warning, lateral-stop popup, crop buttons). Partial — * unspecified keys fall back to {@link DEFAULT_GUIDANCE_COPY}. Hosts * localise or re-word the whole guidance surface in one place here. */ guidanceCopy?: Partial; } /** * v0.20.0 — imperative handle exposed via the `` ref. * * Currently scoped to the AR-overlay methods ({@link AROverlayMethods}: * `setOverlays` / `addOverlay` / `updateOverlay` / `removeOverlay` / * `clearOverlays`), which forward to the underlying ``'s overlay * channel when AR mode is mounted. They are no-ops while the camera is in * non-AR mode (no `` is mounted, and overlays only render over * the AR preview) — use the declarative {@link CameraProps.overlays} prop for * a set that survives AR↔non-AR transitions, since it re-applies automatically * whenever `` (re)mounts. * * The shape is identical to {@link ARCameraViewHandle}'s overlay subset so a * host can use either component with the same overlay code. Panorama capture * remains driven by the built-in shutter; single-photo capture can also be * triggered imperatively via {@link CameraHandle.takePhoto} (added 0.20.5 for * hands-free / auto-capture flows like the document scanner). */ export interface CameraHandle extends AROverlayMethods { /** * Imperatively fire a single-photo capture — identical to the user tapping * the shutter (same AR / non-AR routing, same `onCapture` callback, same * output path rules). Respects `enablePhotoMode` and `shutterDisabled`: a * no-op while photo mode is off or the shutter is gated, so callers can fire * it freely and let the gate decide. Resolves once the capture attempt * settles (success or handled error reported through `onCapture`). */ takePhoto(): Promise; /** * Imperatively START a panorama sweep — identical to the user beginning a * hold on the shutter (the incremental stitcher starts ingesting AR frames). * A no-op unless `enablePanoramaMode` is on and the shutter is not gated * (`shutterDisabled`), and while a capture is already recording/stitching — * the same gates `takePhoto` respects. Pair with {@link stopPanorama}. * Added for hosts that render their OWN shutter (see `hideBuiltInShutter`) * and drive capture through this handle instead of the built-in button. */ startPanorama(): void; /** * Imperatively STOP an in-flight panorama sweep — identical to releasing the * shutter hold: finalize the stitch and emit `onCapture`. Idempotent (a safe * no-op when nothing is recording). Resolves once the stop is dispatched. */ stopPanorama(): Promise; /** * Set the preferred capture source — for hosts that render their own chrome. * * `hideBuiltInShutter` hides the built-in shutter AND the AR toggle, on the * documented premise that the host draws its own capture controls. But * before v0.26.0 the host had no way to DRIVE the AR toggle: `arPreference` * was internal state with no prop and no handle method. So such a host * could replace the shutter (this handle covers capture) and could NOT * replace the AR control — leaving it locked to whatever * `defaultCaptureSource` resolved to at mount, with no indicator and no * switch. That is exactly what shipped in a field build. * * Sets the same PREFERENCE the built-in pill flips, so host chrome and the * built-in control are interchangeable rather than competing sources of * truth. The EFFECTIVE source is still clamped by `captureSources`, by * device AR support, and by the 0.5x lens (ARKit/ARCore cannot drive the * ultra-wide) — so requesting `'ar'` on an unsupported device stays non-AR. * Subscribe to `onCaptureSourceChange` for what actually took effect. */ setCaptureSource(source: CaptureSource): void; } /** * The public `` component. * * v0.20.0 — now a `forwardRef`. The ref exposes {@link CameraHandle} (the AR * overlay methods); existing callers that don't pass a ref are unaffected * (`forwardRef` makes the ref optional). */ export declare const Camera: React.ForwardRefExoticComponent>; /** * v0.12.0 — JS edge corresponding to the physical home-indicator * side of the device. This is where the shutter + controls anchor * to so they're always within thumb reach of the user's grip * (matching iOS Camera's behaviour). * * Combines two signals: * - `jsLandscape`: whether the OS rotated the framebuffer. True * only for non-locked hosts in device-landscape. * - `deviceOrient`: physical device orientation from the sensor. * * Truth table: * | jsLandscape | deviceOrient | edge | * |--- |--- |--- | * | false | any | bottom | (portrait JS coords — * | | | | device-bottom = JS-bottom * | | | | in both locked and * | | | | non-locked-portrait) * | true | landscape-left | right | (screen rotated, home * | | | | indicator on user-right) * | true | landscape-right | left | (mirror) * * Caveats: * - Non-locked + upside-down doesn't surface JS-top here because * upside-down doesn't change window dimensions; we can't * distinguish locked-portrait-with-device-flipped from * non-locked-portrait-with-screen-flipped-180°. Defaults to * JS-bottom which matches the more common locked case. Add * handling here when a host needs upside-down support. * - jsLandscape=true with non-landscape device shouldn't happen * in steady state — only during a transition mid-rotation. * Falls through to 'right' as a defensive default. */ type HomeIndicatorEdge = 'bottom' | 'top' | 'left' | 'right'; declare function homeIndicatorEdge(jsLandscape: boolean, deviceOrient: DeviceOrientation): HomeIndicatorEdge; /** * v0.12.0 — true when the anchor edge is on a side (left/right), so * the band + shutter row need to be vertical strips. Top/bottom * anchors yield horizontal strips. */ declare function isSideEdge(edge: HomeIndicatorEdge): boolean; /** @internal test-only — see `homeIndicatorEdge`. */ export declare const _homeIndicatorEdgeForTests: typeof homeIndicatorEdge; /** @internal test-only — see `isSideEdge`. */ export declare const _isSideEdgeForTests: typeof isSideEdge; /** * cameraShouldUnmount — whether the live camera ( / * ) should be UNMOUNTED (replaced by the placeholder) this * render rather than mounted. * * True while a camera-switch transition or AR-support probe is in flight, * OR during the stitch (statusPhase==='stitching'). The stitching case is * the V12.14.8 OOM fix: unmounting frees vision-camera's AVCaptureSession + * preview buffers (~150-250 MB) BEFORE the memory-heavy stitch, so the * live-camera footprint and the stitch peak never coexist and jetsam (iOS) * / lmkd (Android) don't OOM-kill the app. * * Pure + exported for test — the lib's jest config can't mount , * so this boolean is the unit-testable core of the OOM render gate. */ declare function cameraShouldUnmount(inFlightTransition: boolean, arSupportPending: boolean, statusPhase: CaptureStatusPhase): boolean; /** @internal test-only — see `cameraShouldUnmount`. */ export declare const _cameraShouldUnmountForTests: typeof cameraShouldUnmount; /** * v0.25 — must a hold DEFER because there is no camera to capture from? * * This is deliberately the first two terms of `cameraShouldUnmount` * above, and that is the whole point: the render gate and the hold gate * were reading different conditions, so a hold could start a capture * against a camera the renderer had just deliberately unmounted. * * The hole this closes: `arSupportPending` clears in the SAME render * that flips `isAR` false→true, which makes `inFlightTransition` true, * unmounts the camera and (on iOS) stops the AR session with a 250 ms * grace before the AR view may mount again. The v0.24.3 defer resumed * on `!arSupportPending` alone — i.e. at exactly the moment the * transition BEGAN — so the resumed capture ran against no frame source * and finalized with "0 keyframes saved". * * `statusPhase === 'stitching'` is intentionally NOT included: * `handleHoldStart` already rejects that phase outright rather than * queueing a deferred start. */ declare function holdShouldDeferForCamera(inFlightTransition: boolean, arSupportPending: boolean): boolean; /** @internal test-only — see `holdShouldDeferForCamera`. */ export declare const _holdShouldDeferForCameraForTests: typeof holdShouldDeferForCamera; export {}; //# sourceMappingURL=Camera.d.ts.map