/** * 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