react-native-image-stitcher 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +181 -0
- package/README.md +33 -17
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +33 -5
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +73 -1
- package/dist/camera/Camera.d.ts +226 -0
- package/dist/camera/Camera.js +208 -20
- package/dist/camera/CameraView.d.ts +6 -0
- package/dist/camera/CameraView.js +2 -2
- package/dist/camera/CaptureHeader.js +39 -16
- package/dist/camera/CapturePreview.js +13 -1
- package/dist/camera/CaptureThumbnailStrip.d.ts +25 -1
- package/dist/camera/CaptureThumbnailStrip.js +17 -4
- package/dist/camera/PanoramaBandOverlay.d.ts +76 -0
- package/dist/camera/PanoramaBandOverlay.js +90 -33
- package/dist/camera/PanoramaConfirmModal.js +11 -1
- package/dist/camera/selectCaptureDevice.d.ts +93 -0
- package/dist/camera/selectCaptureDevice.js +131 -0
- package/dist/camera/useCapture.d.ts +40 -0
- package/dist/camera/useCapture.js +50 -12
- package/dist/camera/useContentRotation.d.ts +99 -0
- package/dist/camera/useContentRotation.js +124 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.js +6 -5
- package/package.json +1 -1
- package/src/camera/Camera.tsx +546 -32
- package/src/camera/CameraView.tsx +9 -0
- package/src/camera/CaptureHeader.tsx +39 -16
- package/src/camera/CapturePreview.tsx +12 -0
- package/src/camera/CaptureThumbnailStrip.tsx +44 -4
- package/src/camera/PanoramaBandOverlay.tsx +97 -35
- package/src/camera/PanoramaConfirmModal.tsx +10 -0
- package/src/camera/__tests__/bandThumbRotation.test.ts +120 -0
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +116 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +177 -0
- package/src/camera/__tests__/useContentRotation.test.ts +89 -0
- package/src/camera/selectCaptureDevice.ts +187 -0
- package/src/camera/useCapture.ts +99 -11
- package/src/camera/useContentRotation.ts +149 -0
- package/src/index.ts +6 -2
package/src/camera/Camera.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import React, {
|
|
|
48
48
|
} from 'react';
|
|
49
49
|
import {
|
|
50
50
|
NativeModules,
|
|
51
|
+
Platform,
|
|
51
52
|
Pressable,
|
|
52
53
|
StyleSheet,
|
|
53
54
|
Text,
|
|
@@ -67,6 +68,12 @@ import { useARSession } from '../ar/useARSession';
|
|
|
67
68
|
import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
|
|
68
69
|
import { CameraShutter } from './CameraShutter';
|
|
69
70
|
import { CameraView } from './CameraView';
|
|
71
|
+
import { CaptureHeader, type CaptureHeaderProps } from './CaptureHeader';
|
|
72
|
+
import { CapturePreview, type CapturePreviewAction } from './CapturePreview';
|
|
73
|
+
import {
|
|
74
|
+
CaptureThumbnailStrip,
|
|
75
|
+
type CaptureThumbnailItem,
|
|
76
|
+
} from './CaptureThumbnailStrip';
|
|
70
77
|
import { CaptureStatusOverlay, type CaptureStatusPhase } from './CaptureStatusOverlay';
|
|
71
78
|
import { CaptureDebugOverlay } from './CaptureDebugOverlay';
|
|
72
79
|
import { CaptureMemoryPill } from './CaptureMemoryPill';
|
|
@@ -84,6 +91,7 @@ import {
|
|
|
84
91
|
import { isLowMemDevice } from './lowMemDevice';
|
|
85
92
|
import { useCapture } from './useCapture';
|
|
86
93
|
import { useDeviceOrientation, type DeviceOrientation } from './useDeviceOrientation';
|
|
94
|
+
import { useContentRotation } from './useContentRotation';
|
|
87
95
|
import { useOrientationDrift } from './useOrientationDrift';
|
|
88
96
|
import { OrientationDriftModal } from './OrientationDriftModal';
|
|
89
97
|
import {
|
|
@@ -107,6 +115,17 @@ import {
|
|
|
107
115
|
// ─── Types ──────────────────────────────────────────────────────────
|
|
108
116
|
|
|
109
117
|
export type CaptureSource = 'ar' | 'non-ar';
|
|
118
|
+
/**
|
|
119
|
+
* v0.13.2 — which capture sources the host ALLOWS. A constraint on top
|
|
120
|
+
* of `defaultCaptureSource` (which picks the initial source within this
|
|
121
|
+
* constraint):
|
|
122
|
+
* 'both' — AR and non-AR both available; AR toggle is shown.
|
|
123
|
+
* 'ar' — AR only; AR toggle hidden (nothing to switch to), and the
|
|
124
|
+
* 0.5× lens chooser is hidden (ARKit/ARCore don't expose the
|
|
125
|
+
* ultra-wide).
|
|
126
|
+
* 'non-ar' — non-AR only; AR toggle hidden.
|
|
127
|
+
*/
|
|
128
|
+
export type CaptureSourcesMode = 'ar' | 'non-ar' | 'both';
|
|
110
129
|
export type CameraLens = '1x' | '0.5x';
|
|
111
130
|
export type StitchMode = 'auto' | 'panorama' | 'scans';
|
|
112
131
|
export type Blender = 'multiband' | 'feather';
|
|
@@ -235,6 +254,19 @@ export interface CameraProps {
|
|
|
235
254
|
enablePhotoMode?: boolean;
|
|
236
255
|
enablePanoramaMode?: boolean;
|
|
237
256
|
showSettingsButton?: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* v0.13.2 — which capture sources the host allows (default `'both'`).
|
|
259
|
+
* Constrains both the runtime AR toggle and `defaultCaptureSource`:
|
|
260
|
+
* - `'both'` : AR + non-AR; the AR toggle is shown so the user can
|
|
261
|
+
* switch at runtime.
|
|
262
|
+
* - `'ar'` : AR only. AR toggle hidden (nothing to toggle); the
|
|
263
|
+
* 0.5× lens chooser is also hidden (ARKit/ARCore can't use the
|
|
264
|
+
* ultra-wide), so the camera stays on the AR-capable 1× lens.
|
|
265
|
+
* - `'non-ar'`: non-AR only. AR toggle hidden.
|
|
266
|
+
* When set to a single source, that source wins regardless of
|
|
267
|
+
* `defaultCaptureSource`.
|
|
268
|
+
*/
|
|
269
|
+
captureSources?: CaptureSourcesMode;
|
|
238
270
|
style?: StyleProp<ViewStyle>;
|
|
239
271
|
|
|
240
272
|
/**
|
|
@@ -317,6 +349,173 @@ export interface CameraProps {
|
|
|
317
349
|
*/
|
|
318
350
|
onCaptureAbandoned?: (reason: 'orientation-drift') => void;
|
|
319
351
|
|
|
352
|
+
/**
|
|
353
|
+
* v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
|
|
354
|
+
*
|
|
355
|
+
* - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
|
|
356
|
+
* state internally. Tapping the built-in flash button toggles
|
|
357
|
+
* it on/off. `onFlashChange` (if supplied) fires for telemetry.
|
|
358
|
+
* - **Controlled** (supply `flash`): the parent owns the state.
|
|
359
|
+
* The built-in button still renders and fires `onFlashChange`
|
|
360
|
+
* on press, but it's a no-op unless the parent updates `flash`
|
|
361
|
+
* in response.
|
|
362
|
+
*
|
|
363
|
+
* Both shapes coexist with the v0.13 "flash button is on by default"
|
|
364
|
+
* built-in (see the bottom-left bar slot in the JSX). Hosts that
|
|
365
|
+
* want their own flash chrome can opt out via `showFlashButton={false}`
|
|
366
|
+
* and drive the underlying torch by controlling `flash` directly.
|
|
367
|
+
*
|
|
368
|
+
* ## AR-mode behaviour
|
|
369
|
+
*
|
|
370
|
+
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
|
|
371
|
+
* ARKit / ARCore own the `AVCaptureDevice` and don't expose the
|
|
372
|
+
* torch through vision-camera's pipeline. The built-in flash
|
|
373
|
+
* button renders as visibly disabled (a11y label "Flash unavailable
|
|
374
|
+
* in AR mode") and `flash` is forced to `'off'` regardless of
|
|
375
|
+
* controlled/uncontrolled state. Hosts that need flash should
|
|
376
|
+
* toggle to non-AR before enabling.
|
|
377
|
+
*/
|
|
378
|
+
flash?: 'on' | 'off';
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* v0.13.0 — fires when the user taps the built-in flash button.
|
|
382
|
+
* In uncontrolled mode, the internal state has already flipped
|
|
383
|
+
* (single render delay). In controlled mode, the parent must
|
|
384
|
+
* update the `flash` prop in response or the visual toggle is
|
|
385
|
+
* a no-op. Useful in either mode for telemetry.
|
|
386
|
+
*/
|
|
387
|
+
onFlashChange?: (next: 'on' | 'off') => void;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* v0.13.0 — show the built-in flash button in the bottom-left
|
|
391
|
+
* slot. Defaults to `true`. Hosts that render their own flash
|
|
392
|
+
* chrome (and drive the underlying torch via the controlled
|
|
393
|
+
* `flash` prop) can opt out by setting this to `false`.
|
|
394
|
+
*/
|
|
395
|
+
showFlashButton?: boolean;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
|
|
399
|
+
* renders a top-of-screen header showing this title (centred)
|
|
400
|
+
* with an optional back affordance + guidance subtitle + the
|
|
401
|
+
* existing settings gear absorbed into the header's right side.
|
|
402
|
+
*
|
|
403
|
+
* When `headerTitle` is undefined the header is not rendered
|
|
404
|
+
* (matches pre-v0.13 behaviour: top of preview is bare except
|
|
405
|
+
* for the standalone settings gear gated on `showSettingsButton`).
|
|
406
|
+
*
|
|
407
|
+
* Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
|
|
408
|
+
* and `headerColors` to customise the rest of the header. Hosts
|
|
409
|
+
* that need richer header chrome can omit `headerTitle` and
|
|
410
|
+
* compose their own `<CaptureHeader>` above `<Camera>`.
|
|
411
|
+
*/
|
|
412
|
+
headerTitle?: string;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* v0.13.0 — header back-button callback. When supplied (and
|
|
416
|
+
* `headerTitle` is set), the header renders a back affordance
|
|
417
|
+
* on the left. Omitted ⇒ no back button (the title stays
|
|
418
|
+
* centred).
|
|
419
|
+
*/
|
|
420
|
+
onHeaderBack?: () => void;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* v0.13.0 — header back-button label. Defaults to "‹ Back".
|
|
424
|
+
* No effect unless `headerTitle` and `onHeaderBack` are both set.
|
|
425
|
+
*/
|
|
426
|
+
headerBackLabel?: string;
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* v0.13.0 — optional second-line subtitle shown below the
|
|
430
|
+
* header title. E.g. "Photograph the promotional cola end cap."
|
|
431
|
+
* Renders nothing when undefined. No effect unless `headerTitle`
|
|
432
|
+
* is set.
|
|
433
|
+
*/
|
|
434
|
+
headerGuidance?: string;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* v0.13.0 — colour overrides for the built-in header. Defaults
|
|
438
|
+
* are white-on-black to stay legible over the camera preview.
|
|
439
|
+
* No effect unless `headerTitle` is set.
|
|
440
|
+
*/
|
|
441
|
+
headerColors?: CaptureHeaderProps['colors'];
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
|
|
445
|
+
* built-in `CaptureThumbnailStrip` above the bottom controls
|
|
446
|
+
* showing the host's capture history. Each item is a plain
|
|
447
|
+
* `{ id, uri, width?, height? }` object; the strip handles
|
|
448
|
+
* aspect-ratio rendering, tap-to-preview, and the count line.
|
|
449
|
+
*
|
|
450
|
+
* Omit (`undefined`) to skip the strip entirely. Hosts using
|
|
451
|
+
* the strip independently (e.g. on a non-camera screen) can keep
|
|
452
|
+
* importing `CaptureThumbnailStrip` directly from the library —
|
|
453
|
+
* the prop here is the convenience wiring for in-`<Camera>` use.
|
|
454
|
+
*
|
|
455
|
+
* Captures emitted by `<Camera>`'s `onCapture` are NOT added to
|
|
456
|
+
* this array automatically — the host owns the canonical list
|
|
457
|
+
* (typically persisted to its own DB) and updates the prop in
|
|
458
|
+
* response. This matches the SDK's "Camera owns runtime state,
|
|
459
|
+
* host persists" pattern.
|
|
460
|
+
*/
|
|
461
|
+
thumbnails?: CaptureThumbnailItem[];
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* v0.13.0 — minimum-photos hint for the count line. Renders
|
|
465
|
+
* "n / minPhotos min" with the success colour when reached,
|
|
466
|
+
* warning colour otherwise.
|
|
467
|
+
*/
|
|
468
|
+
thumbnailsMin?: number;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* v0.13.0 — maximum-photos hint for the count line. Renders
|
|
472
|
+
* "· maxPhotos max" suffix. No enforcement — the host decides
|
|
473
|
+
* what to do at the cap.
|
|
474
|
+
*/
|
|
475
|
+
thumbnailsMax?: number;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* v0.13.0 — tap handler for thumbnails. When set, replaces the
|
|
479
|
+
* strip's built-in tap-to-preview modal; the host shows its own
|
|
480
|
+
* preview UI (e.g. with delete / recapture buttons gated on
|
|
481
|
+
* sync state). Omit to use the built-in preview.
|
|
482
|
+
*/
|
|
483
|
+
onThumbnailPress?: (item: CaptureThumbnailItem) => void;
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
|
|
487
|
+
* modal as `visible`. Use this for post-stitch confirmation:
|
|
488
|
+
* after `onCapture` emits, the host stores the result and sets
|
|
489
|
+
* `capturePreview` to the new image, with `capturePreviewActions`
|
|
490
|
+
* = `[Discard, Save]` (or similar). Setting `undefined` hides
|
|
491
|
+
* the modal.
|
|
492
|
+
*
|
|
493
|
+
* Hosts using the modal for thumbnail tap-to-preview can leave
|
|
494
|
+
* this undefined and let the built-in strip's preview handle
|
|
495
|
+
* that case.
|
|
496
|
+
*/
|
|
497
|
+
capturePreview?: {
|
|
498
|
+
imageUri: string;
|
|
499
|
+
imageWidth?: number;
|
|
500
|
+
imageHeight?: number;
|
|
501
|
+
title?: string;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* v0.13.0 — action buttons rendered along the bottom of the
|
|
506
|
+
* `CapturePreview` modal. Empty array (or undefined) renders
|
|
507
|
+
* no buttons, only the close affordance.
|
|
508
|
+
*/
|
|
509
|
+
capturePreviewActions?: CapturePreviewAction[];
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* v0.13.0 — fires when the user dismisses the `capturePreview`
|
|
513
|
+
* modal (tap close, backdrop tap, hardware back on Android).
|
|
514
|
+
* The host is expected to clear the `capturePreview` prop in
|
|
515
|
+
* response.
|
|
516
|
+
*/
|
|
517
|
+
onCapturePreviewClose?: () => void;
|
|
518
|
+
|
|
320
519
|
/**
|
|
321
520
|
* Optional host-supplied vision-camera frame processor.
|
|
322
521
|
*
|
|
@@ -422,12 +621,19 @@ interface LensChipProps {
|
|
|
422
621
|
lens: CameraLens;
|
|
423
622
|
onChange: (lens: CameraLens) => void;
|
|
424
623
|
has0_5x: boolean;
|
|
624
|
+
/**
|
|
625
|
+
* v0.13.1 — counter-rotation applied to the label TEXT (not the pill
|
|
626
|
+
* container) so the "0.5×"/"1×" glyphs read upright when the device
|
|
627
|
+
* is held landscape under a portrait-locked host, while the pill
|
|
628
|
+
* itself stays fixed in the layout. `{}` (no-op) in the upright cases.
|
|
629
|
+
*/
|
|
630
|
+
contentRotation?: { transform?: ViewStyle['transform'] };
|
|
425
631
|
}
|
|
426
|
-
function LensChip({ lens, onChange, has0_5x }: LensChipProps): React.JSX.Element {
|
|
632
|
+
function LensChip({ lens, onChange, has0_5x, contentRotation }: LensChipProps): React.JSX.Element {
|
|
427
633
|
if (!has0_5x) {
|
|
428
634
|
return (
|
|
429
635
|
<View style={[lensChipStyles.container, lensChipStyles.singleLens]}>
|
|
430
|
-
<Text style={lensChipStyles.label}>1×</Text>
|
|
636
|
+
<Text style={[lensChipStyles.label, contentRotation]}>1×</Text>
|
|
431
637
|
</View>
|
|
432
638
|
);
|
|
433
639
|
}
|
|
@@ -447,6 +653,7 @@ function LensChip({ lens, onChange, has0_5x }: LensChipProps): React.JSX.Element
|
|
|
447
653
|
style={[
|
|
448
654
|
lensChipStyles.label,
|
|
449
655
|
lens === '0.5x' && lensChipStyles.labelActive,
|
|
656
|
+
contentRotation,
|
|
450
657
|
]}
|
|
451
658
|
>
|
|
452
659
|
0.5×
|
|
@@ -466,6 +673,7 @@ function LensChip({ lens, onChange, has0_5x }: LensChipProps): React.JSX.Element
|
|
|
466
673
|
style={[
|
|
467
674
|
lensChipStyles.label,
|
|
468
675
|
lens === '1x' && lensChipStyles.labelActive,
|
|
676
|
+
contentRotation,
|
|
469
677
|
]}
|
|
470
678
|
>
|
|
471
679
|
1×
|
|
@@ -515,8 +723,15 @@ const lensChipStyles = StyleSheet.create({
|
|
|
515
723
|
interface ARToggleProps {
|
|
516
724
|
arEnabled: boolean;
|
|
517
725
|
onToggle: () => void;
|
|
726
|
+
/**
|
|
727
|
+
* v0.13.1 — counter-rotation applied to the "AR" label TEXT (not the
|
|
728
|
+
* pill container) so the glyph reads upright when the device is held
|
|
729
|
+
* landscape under a portrait-locked host, while the pill stays fixed.
|
|
730
|
+
* `{}` no-op in the upright cases.
|
|
731
|
+
*/
|
|
732
|
+
contentRotation?: { transform?: ViewStyle['transform'] };
|
|
518
733
|
}
|
|
519
|
-
function ARToggle({ arEnabled, onToggle }: ARToggleProps): React.JSX.Element {
|
|
734
|
+
function ARToggle({ arEnabled, onToggle, contentRotation }: ARToggleProps): React.JSX.Element {
|
|
520
735
|
return (
|
|
521
736
|
<Pressable
|
|
522
737
|
onPress={onToggle}
|
|
@@ -529,6 +744,7 @@ function ARToggle({ arEnabled, onToggle }: ARToggleProps): React.JSX.Element {
|
|
|
529
744
|
style={[
|
|
530
745
|
arToggleStyles.label,
|
|
531
746
|
arEnabled && arToggleStyles.labelOn,
|
|
747
|
+
contentRotation,
|
|
532
748
|
]}
|
|
533
749
|
>
|
|
534
750
|
AR
|
|
@@ -670,6 +886,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
670
886
|
const {
|
|
671
887
|
defaultCaptureSource = 'ar',
|
|
672
888
|
defaultLens = '1x',
|
|
889
|
+
captureSources = 'both',
|
|
673
890
|
enablePhotoMode = true,
|
|
674
891
|
enablePanoramaMode = true,
|
|
675
892
|
showSettingsButton = false,
|
|
@@ -681,10 +898,33 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
681
898
|
onFramesDropped,
|
|
682
899
|
onError,
|
|
683
900
|
onCaptureAbandoned,
|
|
901
|
+
flash: controlledFlash,
|
|
902
|
+
onFlashChange,
|
|
903
|
+
showFlashButton = true,
|
|
904
|
+
headerTitle,
|
|
905
|
+
onHeaderBack,
|
|
906
|
+
headerBackLabel,
|
|
907
|
+
headerGuidance,
|
|
908
|
+
headerColors,
|
|
909
|
+
thumbnails,
|
|
910
|
+
thumbnailsMin,
|
|
911
|
+
thumbnailsMax,
|
|
912
|
+
onThumbnailPress,
|
|
913
|
+
capturePreview,
|
|
914
|
+
capturePreviewActions,
|
|
915
|
+
onCapturePreviewClose,
|
|
684
916
|
frameProcessor: hostFrameProcessor,
|
|
685
917
|
engine = 'batch-keyframe',
|
|
686
918
|
} = props;
|
|
687
919
|
|
|
920
|
+
// v0.13.2 — capture-source constraint (default 'both'). Derives which
|
|
921
|
+
// sources are permitted; `captureSources` overrides any conflicting
|
|
922
|
+
// `defaultCaptureSource`. Used to constrain the initial AR preference
|
|
923
|
+
// and to hide the AR toggle / lens chooser below.
|
|
924
|
+
const arAllowed = captureSources !== 'non-ar';
|
|
925
|
+
const nonArAllowed = captureSources !== 'ar';
|
|
926
|
+
const arOnly = captureSources === 'ar';
|
|
927
|
+
|
|
688
928
|
const insets = useSafeAreaInsets();
|
|
689
929
|
// v0.12.0 — JS-layout orientation independent of device-physical.
|
|
690
930
|
// `useWindowDimensions().width > height` tells us if the OS
|
|
@@ -696,10 +936,21 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
696
936
|
const jsLandscape = jsWindow.width > jsWindow.height;
|
|
697
937
|
|
|
698
938
|
// ── State ───────────────────────────────────────────────────────
|
|
939
|
+
// v0.13.2 — initial AR preference honours `defaultCaptureSource` but
|
|
940
|
+
// is clamped to the `captureSources` constraint: 'ar' forces on,
|
|
941
|
+
// 'non-ar' forces off, 'both' uses the default.
|
|
699
942
|
const [arPreference, setArPreference] = useState(
|
|
700
|
-
defaultCaptureSource === 'ar',
|
|
943
|
+
!arAllowed ? false : !nonArAllowed ? true : defaultCaptureSource === 'ar',
|
|
701
944
|
);
|
|
702
|
-
|
|
945
|
+
// v0.13.2 — `arOnly` forces the 1× lens (the ultra-wide isn't usable
|
|
946
|
+
// in AR), and the lens chooser is hidden in that mode.
|
|
947
|
+
const [lens, setLens] = useState<CameraLens>(arOnly ? '1x' : defaultLens);
|
|
948
|
+
// v0.13.0 — flash state. Controlled by `controlledFlash` when the
|
|
949
|
+
// host supplies the `flash` prop; otherwise owned internally and
|
|
950
|
+
// toggled by the built-in flash button. `effectiveFlash` below
|
|
951
|
+
// also forces 'off' in AR mode (ARKit / ARCore own the device's
|
|
952
|
+
// torch and don't surface it through vision-camera's pipeline).
|
|
953
|
+
const [internalFlash, setInternalFlash] = useState<'on' | 'off'>('off');
|
|
703
954
|
const [settings, setSettings] = useState<PanoramaSettings>(() =>
|
|
704
955
|
buildPanoramaInitialSettings(
|
|
705
956
|
extractPanoramaOverrides(props),
|
|
@@ -739,6 +990,15 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
739
990
|
const isNonAR = !isAR;
|
|
740
991
|
const deviceOrientation = useDeviceOrientation();
|
|
741
992
|
|
|
993
|
+
// v0.13.1 — counter-rotation for control CONTENT (AR toggle, lens
|
|
994
|
+
// pill, flash icon, thumbnails) so their labels read upright relative
|
|
995
|
+
// to gravity when the device is held landscape under a PORTRAIT-LOCKED
|
|
996
|
+
// host (the recommended config — the JS framebuffer stays portrait, so
|
|
997
|
+
// without this the labels render at 90°). Returns `{}` (no-op) in the
|
|
998
|
+
// common upright cases, including non-locked hosts where the OS already
|
|
999
|
+
// rotated the framebuffer. See `useContentRotation` truth table.
|
|
1000
|
+
const contentRotation = useContentRotation();
|
|
1001
|
+
|
|
742
1002
|
// ── Camera handoff gate ─────────────────────────────────────────
|
|
743
1003
|
//
|
|
744
1004
|
// The placeholder rendered while the underlying camera identity
|
|
@@ -770,6 +1030,35 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
770
1030
|
|| cameraTransitioning;
|
|
771
1031
|
|
|
772
1032
|
|
|
1033
|
+
// ── v0.13.1 — Android portrait lock ─────────────────────────────
|
|
1034
|
+
//
|
|
1035
|
+
// Android lets a mounted view force its host Activity's orientation,
|
|
1036
|
+
// so `<Camera>` guarantees a portrait capture surface regardless of
|
|
1037
|
+
// the host app's manifest (even a landscape/unlocked host gets a
|
|
1038
|
+
// portrait camera while `<Camera>` is mounted). The lock lives on
|
|
1039
|
+
// the Activity via the native `RNSARSession` module, so it covers
|
|
1040
|
+
// BOTH the AR (ARCore) and non-AR (vision-camera) capture paths.
|
|
1041
|
+
//
|
|
1042
|
+
// iOS is intentionally NOT locked here: iOS supported orientations
|
|
1043
|
+
// are a static Info.plist declaration the host owns, and we want iOS
|
|
1044
|
+
// hosts to be able to support landscape/unlocked capture. Hosts that
|
|
1045
|
+
// want a portrait-only iOS app set UISupportedInterfaceOrientations
|
|
1046
|
+
// themselves.
|
|
1047
|
+
//
|
|
1048
|
+
// Empty dep array — lock on mount, restore the host's PRIOR
|
|
1049
|
+
// orientation on unmount (the native side captures it).
|
|
1050
|
+
useEffect(() => {
|
|
1051
|
+
if (Platform.OS !== 'android') return undefined;
|
|
1052
|
+
const arModule = (NativeModules as Record<string, unknown>)
|
|
1053
|
+
.RNSARSession as
|
|
1054
|
+
| { lockPortrait?: () => void; unlockOrientation?: () => void }
|
|
1055
|
+
| undefined;
|
|
1056
|
+
arModule?.lockPortrait?.();
|
|
1057
|
+
return () => {
|
|
1058
|
+
arModule?.unlockOrientation?.();
|
|
1059
|
+
};
|
|
1060
|
+
}, []);
|
|
1061
|
+
|
|
773
1062
|
// ── Notify parent of capture-source changes ─────────────────────
|
|
774
1063
|
const lastEmittedSourceRef = useRef<CaptureSource | null>(null);
|
|
775
1064
|
useEffect(() => {
|
|
@@ -779,21 +1068,23 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
779
1068
|
}
|
|
780
1069
|
}, [effectiveCaptureSource, onCaptureSourceChange]);
|
|
781
1070
|
|
|
782
|
-
// ── Lens chip availability ──────────────────────────────────────
|
|
783
|
-
// TODO follow-up: probe the device's available physical lenses via
|
|
784
|
-
// vision-camera's `useCameraDevices` and surface in
|
|
785
|
-
// `useCapture().availablePhysicalDevices`. For now we assume the
|
|
786
|
-
// 0.5x ultra-wide exists on modern devices. When it doesn't, the
|
|
787
|
-
// lens chip degenerates to a static 1× indicator (see LensChip).
|
|
788
|
-
const has0_5x = true;
|
|
789
|
-
|
|
790
1071
|
// ── Capture hooks ───────────────────────────────────────────────
|
|
1072
|
+
// v0.13.2 — pass the active `lens` so useCapture uses capability-aware
|
|
1073
|
+
// selection (multi-cam zoom-switch where available, standalone-ultra-
|
|
1074
|
+
// wide swap otherwise). Replaces the old per-lens
|
|
1075
|
+
// `preferredPhysicalDevice` request that mis-selected on some phones.
|
|
791
1076
|
const capture = useCapture({
|
|
792
1077
|
cameraPosition: 'back',
|
|
793
1078
|
enableQualityChecks: false,
|
|
794
|
-
|
|
795
|
-
lens === '0.5x' ? 'ultra-wide-angle-camera' : 'wide-angle-camera',
|
|
1079
|
+
lens,
|
|
796
1080
|
});
|
|
1081
|
+
|
|
1082
|
+
// ── Lens chip availability ──────────────────────────────────────
|
|
1083
|
+
// v0.13.2 — real device capability from `useCapture` (which uses
|
|
1084
|
+
// `selectCaptureDevice`). True only when the device actually exposes
|
|
1085
|
+
// an ultra-wide reachable via a multi-cam zoom OR a standalone
|
|
1086
|
+
// ultra-wide device; false on wide-only hardware (chip hides).
|
|
1087
|
+
const has0_5x = capture.has0_5x;
|
|
797
1088
|
const incremental = useIncrementalStitcher();
|
|
798
1089
|
const visionCameraRef = useRef<VisionCamera | null>(null);
|
|
799
1090
|
const arViewRef = useRef<ARCameraViewHandle | null>(null);
|
|
@@ -1349,6 +1640,47 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1349
1640
|
setArPreference((prev) => !prev);
|
|
1350
1641
|
}, []);
|
|
1351
1642
|
|
|
1643
|
+
// ── v0.13.0 — Flash control ─────────────────────────────────────
|
|
1644
|
+
//
|
|
1645
|
+
// `flashRequested` is what the host / built-in button asks for.
|
|
1646
|
+
// `effectiveFlash` is what we drive into vision-camera (non-AR). AR
|
|
1647
|
+
// mode forces 'off' (flash is hidden in AR; ARKit/ARCore own the
|
|
1648
|
+
// device) so vision-camera — which isn't the active camera in AR —
|
|
1649
|
+
// doesn't fight for it.
|
|
1650
|
+
//
|
|
1651
|
+
// v0.13.1 — the ACTIVE device's torch capability is the source of
|
|
1652
|
+
// truth. The ultra-wide (0.5×) lens has no flash/torch unit on most
|
|
1653
|
+
// phones, so vision-camera throws `flash-not-available` if we pass
|
|
1654
|
+
// flash="on" while it's selected. `capture.device.hasTorch` (from
|
|
1655
|
+
// vision-camera's device list) tells us definitively; we hide the
|
|
1656
|
+
// flash control and force 'off' when the device can't flash.
|
|
1657
|
+
// v0.13.2 — `capture.deviceHasTorch` reflects the MOUNTED device. In
|
|
1658
|
+
// multi-cam mode this is the multi-cam device (has a torch → flash
|
|
1659
|
+
// works on both 1× and 0.5× via zoom). In standalone-uw mode on 0.5×
|
|
1660
|
+
// the mounted device is the torchless ultra-wide → flash hides.
|
|
1661
|
+
const deviceHasTorch = capture.deviceHasTorch;
|
|
1662
|
+
const flashRequested: 'on' | 'off' = controlledFlash ?? internalFlash;
|
|
1663
|
+
const effectiveFlash: 'on' | 'off' =
|
|
1664
|
+
isAR || !deviceHasTorch ? 'off' : flashRequested;
|
|
1665
|
+
const toggleFlash = useCallback(() => {
|
|
1666
|
+
const next: 'on' | 'off' = flashRequested === 'on' ? 'off' : 'on';
|
|
1667
|
+
if (controlledFlash == null) setInternalFlash(next);
|
|
1668
|
+
onFlashChange?.(next);
|
|
1669
|
+
}, [flashRequested, controlledFlash, onFlashChange]);
|
|
1670
|
+
|
|
1671
|
+
// v0.13.1 — top-right control pills (flash + AR) stack vertically
|
|
1672
|
+
// UNDER the settings affordance. Anchor depends on what's above:
|
|
1673
|
+
// - headerTitle set → pills clear the CaptureHeader bar
|
|
1674
|
+
// (title row ≈ topInset + ~36; guidance pill adds ~28 when present)
|
|
1675
|
+
// - standalone gear → pills clear the 40px gear at topInset + 8
|
|
1676
|
+
// - neither → pills start where the gear would be
|
|
1677
|
+
const pillStackTop =
|
|
1678
|
+
headerTitle != null
|
|
1679
|
+
? insets.top + (headerGuidance != null ? 72 : 40)
|
|
1680
|
+
: showSettingsButton
|
|
1681
|
+
? insets.top + 8 + 44
|
|
1682
|
+
: insets.top + 8;
|
|
1683
|
+
|
|
1352
1684
|
// ── JSX ─────────────────────────────────────────────────────────
|
|
1353
1685
|
|
|
1354
1686
|
return (
|
|
@@ -1380,7 +1712,12 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1380
1712
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
1381
1713
|
// which has run on `video` (true) for months without issue.
|
|
1382
1714
|
video
|
|
1383
|
-
flash=
|
|
1715
|
+
flash={effectiveFlash}
|
|
1716
|
+
// v0.13.2 — in multi-cam mode the lens is switched via zoom
|
|
1717
|
+
// on a single mounted device (0.5× → ultra-wide end, 1× →
|
|
1718
|
+
// wide baseline). undefined in standalone/wide-only modes
|
|
1719
|
+
// (lens = device identity, no zoom).
|
|
1720
|
+
zoom={capture.deviceZoom}
|
|
1384
1721
|
style={StyleSheet.absoluteFill}
|
|
1385
1722
|
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
1386
1723
|
// the camera producer thread for every frame. Only wired
|
|
@@ -1416,6 +1753,13 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1416
1753
|
recordingStartedAt={recordingStartedAt ?? undefined}
|
|
1417
1754
|
/>
|
|
1418
1755
|
|
|
1756
|
+
{/* v0.13.1 — the built-in pan-guidance overlays
|
|
1757
|
+
(IncrementalPanGuide drift marker + PanoramaGuidance speed
|
|
1758
|
+
pill) were removed from the public surface. They remain in
|
|
1759
|
+
the tree as internal-only components but <Camera> no longer
|
|
1760
|
+
renders them and the `panGuide` / `panoramaGuidance` props
|
|
1761
|
+
are gone. Re-wire here if a host need resurfaces. */}
|
|
1762
|
+
|
|
1419
1763
|
{/*
|
|
1420
1764
|
2026-05-22 (audit F9 + F3) — debug UI suite, all gated on
|
|
1421
1765
|
settings.debug. Mounts in <Camera> automatically; Layer-2
|
|
@@ -1459,12 +1803,34 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1459
1803
|
topInset={insets.top}
|
|
1460
1804
|
/>
|
|
1461
1805
|
|
|
1462
|
-
{/*
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1806
|
+
{/* v0.13.0 — built-in CaptureHeader, gated on `headerTitle`.
|
|
1807
|
+
When the header is mounted, it absorbs the settings gear
|
|
1808
|
+
on its right side (avoids stacking with the standalone
|
|
1809
|
+
gear). Hosts that DON'T set `headerTitle` get the legacy
|
|
1810
|
+
standalone gear, still gated on `showSettingsButton`. */}
|
|
1811
|
+
{headerTitle != null ? (
|
|
1812
|
+
<View style={styles.headerWrap} pointerEvents="box-none">
|
|
1813
|
+
<CaptureHeader
|
|
1814
|
+
title={headerTitle}
|
|
1815
|
+
onBack={onHeaderBack}
|
|
1816
|
+
backLabel={headerBackLabel}
|
|
1817
|
+
guidance={headerGuidance}
|
|
1818
|
+
colors={headerColors}
|
|
1819
|
+
topInset={insets.top}
|
|
1820
|
+
onSettingsPress={
|
|
1821
|
+
showSettingsButton
|
|
1822
|
+
? () => setSettingsModalVisible(true)
|
|
1823
|
+
: undefined
|
|
1824
|
+
}
|
|
1825
|
+
/>
|
|
1826
|
+
</View>
|
|
1827
|
+
) : (
|
|
1828
|
+
showSettingsButton && (
|
|
1829
|
+
<SettingsButton
|
|
1830
|
+
topInset={insets.top}
|
|
1831
|
+
onPress={() => setSettingsModalVisible(true)}
|
|
1832
|
+
/>
|
|
1833
|
+
)
|
|
1468
1834
|
)}
|
|
1469
1835
|
|
|
1470
1836
|
{/*
|
|
@@ -1500,18 +1866,52 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1500
1866
|
/>
|
|
1501
1867
|
)}
|
|
1502
1868
|
|
|
1869
|
+
{/* v0.13.0 — built-in capture-history thumbnail strip. Lives
|
|
1870
|
+
INSIDE the orientation-aware bottomArea container so it
|
|
1871
|
+
rides along to the home-indicator edge in landscape rather
|
|
1872
|
+
than sitting at a hard-coded `bottom: 160` mid-screen.
|
|
1873
|
+
Hidden during recording so the PanoramaBandOverlay above
|
|
1874
|
+
it has room without overlap. Strip is intrinsically
|
|
1875
|
+
horizontal; v0.13.1 will add orientation-aware rotation
|
|
1876
|
+
for the thumbnails + tablet "user-bottom" placement. */}
|
|
1877
|
+
{thumbnails != null && statusPhase !== 'recording' && (
|
|
1878
|
+
<CaptureThumbnailStrip
|
|
1879
|
+
items={thumbnails}
|
|
1880
|
+
minPhotos={thumbnailsMin}
|
|
1881
|
+
maxPhotos={thumbnailsMax}
|
|
1882
|
+
onItemPress={onThumbnailPress}
|
|
1883
|
+
// v0.13.1 — stack the idle strip vertically when the
|
|
1884
|
+
// home-indicator anchor is on a side edge (non-locked host
|
|
1885
|
+
// in landscape), matching PanoramaBandOverlay's `vertical`
|
|
1886
|
+
// so the strip rides the home-indicator edge instead of
|
|
1887
|
+
// running horizontally across the rotated screen.
|
|
1888
|
+
vertical={isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation))}
|
|
1889
|
+
// v0.13.1 — counter-rotate the thumbnail images so the
|
|
1890
|
+
// captured scene reads upright in portrait-locked landscape.
|
|
1891
|
+
contentRotation={contentRotation}
|
|
1892
|
+
/>
|
|
1893
|
+
)}
|
|
1894
|
+
|
|
1503
1895
|
{/* Shutter row. Horizontal row when home-indicator is on
|
|
1504
1896
|
top/bottom (lens left / shutter center / AR right);
|
|
1505
1897
|
vertical column when on left/right (slots stack along
|
|
1506
1898
|
the narrow strip). Touch targets stay axis-aligned. */}
|
|
1507
1899
|
<View style={bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation))}>
|
|
1900
|
+
{/* v0.13.1 — flash + AR moved to the top-right pill stack (see
|
|
1901
|
+
below). Left/right slots stay as flex spacers so the shutter
|
|
1902
|
+
+ lens chip remain centred. */}
|
|
1508
1903
|
<View style={styles.bottomBarLeft} />
|
|
1509
1904
|
<View style={styles.bottomBarCenter}>
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1905
|
+
{/* v0.13.2 — lens chooser hidden in AR-only mode (ARKit/ARCore
|
|
1906
|
+
can't use the ultra-wide, so there's nothing to choose). */}
|
|
1907
|
+
{!arOnly && (
|
|
1908
|
+
<LensChip
|
|
1909
|
+
lens={lens}
|
|
1910
|
+
onChange={handleLensChange}
|
|
1911
|
+
has0_5x={has0_5x}
|
|
1912
|
+
contentRotation={contentRotation}
|
|
1913
|
+
/>
|
|
1914
|
+
)}
|
|
1515
1915
|
<View style={styles.shutterWrap}>
|
|
1516
1916
|
<CameraShutter
|
|
1517
1917
|
onTap={handleTap}
|
|
@@ -1522,14 +1922,53 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1522
1922
|
/>
|
|
1523
1923
|
</View>
|
|
1524
1924
|
</View>
|
|
1525
|
-
<View style={styles.bottomBarRight}
|
|
1526
|
-
{lens === '1x' && isARSupportedOnDevice && (
|
|
1527
|
-
<ARToggle arEnabled={arPreference} onToggle={handleARToggle} />
|
|
1528
|
-
)}
|
|
1529
|
-
</View>
|
|
1925
|
+
<View style={styles.bottomBarRight} />
|
|
1530
1926
|
</View>
|
|
1531
1927
|
</View>
|
|
1532
1928
|
|
|
1929
|
+
{/* v0.13.1 — top-right control pill stack, anchored UNDER the
|
|
1930
|
+
settings affordance. Vertical column; pills match the AR
|
|
1931
|
+
toggle's shape. ORDER MATTERS: AR pill is FIRST (top) so it
|
|
1932
|
+
stays anchored when the flash pill below it shows/hides
|
|
1933
|
+
(flash is hidden in AR mode, and when the active device has no
|
|
1934
|
+
torch — e.g. the ultra-wide 0.5× lens). AR toggle shows only
|
|
1935
|
+
when the lens is 1× (ARKit/ARCore don't expose the ultra-wide)
|
|
1936
|
+
and the device supports AR. */}
|
|
1937
|
+
<View
|
|
1938
|
+
style={[styles.pillStack, { top: pillStackTop }]}
|
|
1939
|
+
pointerEvents="box-none"
|
|
1940
|
+
>
|
|
1941
|
+
{/* v0.13.2 — AR toggle only when BOTH sources are allowed
|
|
1942
|
+
(captureSources='both'); a single-source constraint has
|
|
1943
|
+
nothing to toggle. Still gated on 1× + device AR support. */}
|
|
1944
|
+
{arAllowed && nonArAllowed && lens === '1x' && isARSupportedOnDevice && (
|
|
1945
|
+
<ARToggle arEnabled={arPreference} onToggle={handleARToggle} contentRotation={contentRotation} />
|
|
1946
|
+
)}
|
|
1947
|
+
{showFlashButton && !isAR && deviceHasTorch && (
|
|
1948
|
+
<Pressable
|
|
1949
|
+
onPress={toggleFlash}
|
|
1950
|
+
accessibilityRole="button"
|
|
1951
|
+
accessibilityLabel={`Flash ${flashRequested === 'on' ? 'on' : 'off'}`}
|
|
1952
|
+
accessibilityState={{ selected: flashRequested === 'on' }}
|
|
1953
|
+
hitSlop={8}
|
|
1954
|
+
style={[
|
|
1955
|
+
pillStyles.pill,
|
|
1956
|
+
flashRequested === 'on' && pillStyles.pillActive,
|
|
1957
|
+
]}
|
|
1958
|
+
>
|
|
1959
|
+
<Text
|
|
1960
|
+
style={[
|
|
1961
|
+
pillStyles.flashGlyph,
|
|
1962
|
+
flashRequested === 'on' && pillStyles.glyphActive,
|
|
1963
|
+
contentRotation,
|
|
1964
|
+
]}
|
|
1965
|
+
>
|
|
1966
|
+
⚡
|
|
1967
|
+
</Text>
|
|
1968
|
+
</Pressable>
|
|
1969
|
+
)}
|
|
1970
|
+
</View>
|
|
1971
|
+
|
|
1533
1972
|
{/* Settings modal (rendered always, visible-gated). */}
|
|
1534
1973
|
<PanoramaSettingsModal
|
|
1535
1974
|
visible={settingsModalVisible}
|
|
@@ -1550,6 +1989,21 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1550
1989
|
currentOrientation={drift.currentOrientation}
|
|
1551
1990
|
onAcknowledge={() => setDriftModalDismissed(true)}
|
|
1552
1991
|
/>
|
|
1992
|
+
|
|
1993
|
+
{/* v0.13.0 — built-in post-stitch / tap-to-preview modal.
|
|
1994
|
+
Visible when the host supplies `capturePreview`. When
|
|
1995
|
+
undefined the modal stays hidden (visible=false) so it
|
|
1996
|
+
doesn't intercept touches. Host is expected to clear
|
|
1997
|
+
`capturePreview` via `onCapturePreviewClose` on dismiss. */}
|
|
1998
|
+
<CapturePreview
|
|
1999
|
+
visible={capturePreview != null}
|
|
2000
|
+
imageUri={capturePreview?.imageUri ?? ''}
|
|
2001
|
+
imageWidth={capturePreview?.imageWidth}
|
|
2002
|
+
imageHeight={capturePreview?.imageHeight}
|
|
2003
|
+
title={capturePreview?.title}
|
|
2004
|
+
actions={capturePreviewActions}
|
|
2005
|
+
onClose={onCapturePreviewClose ?? noop}
|
|
2006
|
+
/>
|
|
1553
2007
|
</View>
|
|
1554
2008
|
);
|
|
1555
2009
|
}
|
|
@@ -1615,6 +2069,17 @@ function isSideEdge(edge: HomeIndicatorEdge): boolean {
|
|
|
1615
2069
|
return edge === 'left' || edge === 'right';
|
|
1616
2070
|
}
|
|
1617
2071
|
|
|
2072
|
+
// v0.13.1 — test-only exports of the pure orientation-decision
|
|
2073
|
+
// functions. `homeIndicatorEdge` + `isSideEdge` together produce the
|
|
2074
|
+
// `vertical` flag that drives PanoramaBandOverlay and
|
|
2075
|
+
// CaptureThumbnailStrip layout, so they carry the orientation contract.
|
|
2076
|
+
// Unit-tested via these handles (the lib's jest config is pure-TS and
|
|
2077
|
+
// can't mount <Camera>; see jest.config.js).
|
|
2078
|
+
/** @internal test-only — see `homeIndicatorEdge`. */
|
|
2079
|
+
export const _homeIndicatorEdgeForTests = homeIndicatorEdge;
|
|
2080
|
+
/** @internal test-only — see `isSideEdge`. */
|
|
2081
|
+
export const _isSideEdgeForTests = isSideEdge;
|
|
2082
|
+
|
|
1618
2083
|
|
|
1619
2084
|
/**
|
|
1620
2085
|
* v0.12.0 — bottom-controls outer container positioning. Anchors
|
|
@@ -1731,6 +2196,8 @@ const styles = StyleSheet.create({
|
|
|
1731
2196
|
},
|
|
1732
2197
|
bottomBarLeft: {
|
|
1733
2198
|
flex: 1,
|
|
2199
|
+
alignItems: 'flex-start',
|
|
2200
|
+
justifyContent: 'flex-end',
|
|
1734
2201
|
},
|
|
1735
2202
|
bottomBarCenter: {
|
|
1736
2203
|
flex: 1,
|
|
@@ -1744,4 +2211,51 @@ const styles = StyleSheet.create({
|
|
|
1744
2211
|
shutterWrap: {
|
|
1745
2212
|
marginTop: 12,
|
|
1746
2213
|
},
|
|
2214
|
+
headerWrap: {
|
|
2215
|
+
position: 'absolute',
|
|
2216
|
+
top: 0,
|
|
2217
|
+
left: 0,
|
|
2218
|
+
right: 0,
|
|
2219
|
+
},
|
|
2220
|
+
// v0.13.1 — `thumbnailStripWrap` removed. The strip now renders
|
|
2221
|
+
// inside the orientation-aware bottomArea container (alongside
|
|
2222
|
+
// PanoramaBandOverlay and the bottom bar) rather than as a
|
|
2223
|
+
// position-absolute overlay at hard-coded `bottom: 160`.
|
|
2224
|
+
//
|
|
2225
|
+
// v0.13.1 — top-right control pill stack (flash + AR). Absolute,
|
|
2226
|
+
// pinned to the right edge under the settings affordance; `top` is
|
|
2227
|
+
// set inline from `pillStackTop`. Column so the pills stack
|
|
2228
|
+
// vertically; gap keeps them from touching.
|
|
2229
|
+
pillStack: {
|
|
2230
|
+
position: 'absolute',
|
|
2231
|
+
right: 14,
|
|
2232
|
+
alignItems: 'flex-end',
|
|
2233
|
+
gap: 10,
|
|
2234
|
+
},
|
|
2235
|
+
});
|
|
2236
|
+
|
|
2237
|
+
|
|
2238
|
+
// v0.13.1 — shared pill style for the top-right control stack. The
|
|
2239
|
+
// flash pill matches the AR toggle's shape (same padding / radius /
|
|
2240
|
+
// background) so the two read as a set.
|
|
2241
|
+
const pillStyles = StyleSheet.create({
|
|
2242
|
+
pill: {
|
|
2243
|
+
paddingHorizontal: 14,
|
|
2244
|
+
paddingVertical: 8,
|
|
2245
|
+
borderRadius: 16,
|
|
2246
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
2247
|
+
minWidth: 56,
|
|
2248
|
+
alignItems: 'center',
|
|
2249
|
+
justifyContent: 'center',
|
|
2250
|
+
},
|
|
2251
|
+
pillActive: {
|
|
2252
|
+
backgroundColor: '#ffd34d',
|
|
2253
|
+
},
|
|
2254
|
+
flashGlyph: {
|
|
2255
|
+
color: '#ffffff',
|
|
2256
|
+
fontSize: 18,
|
|
2257
|
+
},
|
|
2258
|
+
glyphActive: {
|
|
2259
|
+
color: '#1a1a1a',
|
|
2260
|
+
},
|
|
1747
2261
|
});
|