/**
* PanoramaBandOverlay — V16 Phase 2 (merged band + strip).
*
* SINGLE source of truth for the live "progress strip" that sits on
* top of the camera preview during a panorama hold. Replaces what
* was previously TWO components rendered side-by-side:
*
* 1. live per-keyframe thumbnail strip — fed by accepted-frame URIs
* (batch-keyframe engine) OR by
* periodic vision-camera snapshots.
* 2. — a single cumulative-panorama
* thumbnail with a "fill ratio"
* bar growing with the pan.
*
* The split made the UI visually noisy AND made it differ between
* platforms when one side emitted keyframe events and the other
* didn't. V16 Phase 2 collapses them into ONE component that:
*
* • Renders a horizontally-scrolling list of per-keyframe
* thumbnails when `frameUris` is non-empty (batch-keyframe
* mode). Each frame the KeyframeGate accepts shows up as a
* mini-thumb.
*
* • Falls back to a SINGLE cumulative-panorama thumbnail (the
* V12.14.9 fill-ratio behaviour) when `frameUris` is empty —
* i.e. the live-stitching engines that don't surface
* per-keyframe paths. This preserves the existing visual for
* hybrid / firstwins / firstwins-rectilinear engines.
*
* • Edge-pinned to the BOTTOM of the camera area in portrait, and
* to the user's RIGHT in landscape (which corresponds to
* JS-bottom under the app's portrait-lock). Both anchors keep
* the band out of the centre of the scene the operator is
* framing.
*
* • Trailing arrow points along the pan axis (→ in portrait, ← in
* landscape-left's user perception). Arrow always sits at the
* pan-END side, so the LATEST keyframe abuts the arrow.
*
* • Auto-scrolls a `` so the latest keyframe stays
* visible regardless of how many frames have been accumulated.
*
* Empty-state intentional non-design:
* The KeyframeGate force-accepts the FIRST frame of every capture
* (see C++ `AcceptFirstAnchoredOnPlane` / `AcceptFirstNoPlane` in
* keyframe_gate.cpp). By the time the operator's perceived "the
* band appeared", we already have at least one thumb/snapshot in
* flight. We therefore don't render any "no frames yet"
* placeholder — the empty period is sub-perceptual.
*
* Why this component is in react-native-image-stitcher (not host):
* It's the same JSX shipped to iOS and Android. Differences in
* what shows up come only from native-emitted data
* (`state.batchKeyframeThumbnailPath` / `state.panoramaPath`),
* not from per-platform component code. That's exactly the parity
* property the user wants: "the UI should not differ between iOS
* and Android — it's the same UI reused".
*/
import React from 'react';
import type { IncrementalState } from '../stitching/incremental';
/**
* 2026-05-18 (Issue #3 fix) — 4-way capture orientation classifier.
* Replaces the 2-way `state.isLandscape` boolean which couldn't
* distinguish landscape-LEFT (home button on user's right) from
* landscape-RIGHT (home button on user's left). Required because
* the JS-coordinate mapping to user-perceived directions inverts
* between the two landscape rotations — `flexDirection: 'row'`
* gives oldest-at-user-top in landscape-LEFT but oldest-at-user-
* bottom in landscape-RIGHT, so we need to branch the layout.
*/
export type BandCaptureOrientation = 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
export interface PanoramaBandOverlayProps {
/**
* v0.12.0 — `true` when the band should render as a vertical
* column in JS (anchor edge is JS-left or JS-right, i.e.
* non-locked host with device-landscape). `false` (default)
* renders the legacy horizontal strip — covers portrait-locked
* hosts in any device orientation AND non-locked hosts in
* portrait. The flagship `` derives this from
* `useWindowDimensions()` + `useDeviceOrientation()` (see
* `homeIndicatorEdge` in `Camera.tsx`); Layer-2 hosts pass it
* directly.
*/
vertical?: boolean;
/**
* Latest engine state. Pass `useIncrementalStitcher().state`.
* Used for single-thumb fallback URI and fill-ratio when no
* per-keyframe URIs are provided. `state.isLandscape` is now
* superseded by `captureOrientation` below for layout selection.
*/
state: IncrementalState | null;
/**
* Optional list of per-keyframe thumbnail URIs accumulated by the
* host as the native batch-keyframe engine emits
* `batchKeyframeThumbnailPath` events. When non-empty, the band
* renders these as a scrolling mini-thumb strip. When empty or
* undefined, the band falls back to the single cumulative-panorama
* thumbnail (legacy live-engine visual).
*
* Caller should cap the list length itself if needed (e.g. the
* AuditCaptureScreen already trims at 24 entries). This component
* applies an internal hard cap as a safety net so a runaway
* emission doesn't blow up the scroll view.
*/
frameUris?: string[];
/**
* 2026-05-18 (Issue #3) — capture orientation passed from the host.
* Drives a 4-way layout switch so the band reads correctly in
* either landscape rotation (the 2-way `state.isLandscape` boolean
* collapses landscape-LEFT and landscape-RIGHT to the same render
* path, which inverts the user's perceived "oldest-top, grows
* down" intent in one of them). Pass
* `panoramaSettings.captureOrientation` from the host. Defaults
* to `'portrait'` when omitted (back-compat).
*/
captureOrientation?: BandCaptureOrientation;
}
/**
* Resolve band layout from capture orientation. 2026-05-18 (Issue #3)
* — uses the 4-way `BandCaptureOrientation` instead of the 2-way
* `state.isLandscape` so we can pick the right flex direction +
* arrow glyph in EACH landscape rotation.
*
* The two landscape rotations require different JS-coordinate setups
* because the phone tilts the JS coordinate system relative to the
* user differently:
*
* LANDSCAPE-LEFT (Apple: home indicator on user's RIGHT; phone
* rotated 90° CCW from portrait).
* JS-left = user-top
* JS-right = user-bottom
* Band at JS-bottom edge appears on user's RIGHT edge.
* For "oldest at user-top, newest at user-bottom":
* flexDirection = 'row' (array[0] at JS-left = user-top).
* For arrow appearing as user-DOWN-arrow:
* glyph `←` (rotated 90° CCW = points user-down).
*
* LANDSCAPE-RIGHT (Apple: home indicator on user's LEFT; phone
* rotated 90° CW from portrait).
* JS-left = user-bottom
* JS-right = user-top
* Band at JS-TOP edge appears on user's RIGHT edge (so we move
* the band to JS-top here, not JS-bottom).
* For "oldest at user-top, newest at user-bottom":
* flexDirection = 'row-reverse' (array[0] at JS-right = user-top).
* For arrow appearing as user-DOWN-arrow:
* glyph `→` (rotated 90° CW = points user-down).
*
* PORTRAIT (and portrait-upside-down — collapsed because the band's
* bottom-anchored position remains sensible either way):
* Band at JS-bottom = user-bottom. Row left-to-right. Arrow `→`
* reads as user-right-arrow (pointing along the horizontal pan
* direction).
*/
/**
* v0.13.1 — pure rotation-decision helpers, extracted for unit testing
* (the lib's jest config is pure-TS, no component mounting; see
* jest.config.js). These encode the orientation contract the band
* relies on, so a regression in the angles/branches is caught in CI
* rather than only on-device.
*
* `bandThumbRotation` — the CSS rotate transform that aligns a thumb's
* pixels with the band box. Returns the transform array RN expects, or
* `undefined` for "no rotation". Two regimes:
* - vertical=false (portrait-locked UI): the box is device-aligned, so
* a landscape device needs a 90° counter-rotation (CW for
* landscape-left, CCW for landscape-right).
* - vertical=true (non-locked, OS-rotated framebuffer): the screen
* rotation already did half the work, so the compensation is the
* OPPOSITE sign.
* Exported as `_bandThumbRotationForTests`.
*/
declare function bandThumbRotation(orientation: BandCaptureOrientation, vertical: boolean): Array<{
rotate: string;
}> | undefined;
/**
* v0.13.1 — the rotation actually applied to the per-keyframe (multi-
* thumb) TILES. This is the EXIF double-rotation fix: the saved
* `keyframe-N.jpg` is sensor-native landscape + EXIF Orientation 6, which
* RN's already auto-rotates upright. So in the portrait-locked
* (vertical=false) path NO further transform is applied — adding one
* double-rotates (the original v0.12 bug). Only the non-locked
* (vertical=true) path needs the compensation. Returns `undefined` for
* "no transform". Exported as `_tileRotationForTests`.
*/
declare function tileRotation(orientation: BandCaptureOrientation, vertical: boolean): Array<{
rotate: string;
}> | undefined;
/** @internal test-only export — see `bandThumbRotation`. */
export declare const _bandThumbRotationForTests: typeof bandThumbRotation;
/** @internal test-only export — see `tileRotation`. */
export declare const _tileRotationForTests: typeof tileRotation;
declare function PanoramaBandOverlayImpl({ state, frameUris, captureOrientation, vertical, }: PanoramaBandOverlayProps): React.JSX.Element | null;
export declare const PanoramaBandOverlay: React.MemoExoticComponent;
export {};
//# sourceMappingURL=PanoramaBandOverlay.d.ts.map