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/dist/camera/Camera.d.ts
CHANGED
|
@@ -41,7 +41,22 @@
|
|
|
41
41
|
import React from 'react';
|
|
42
42
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
43
43
|
import type { DrawableFrameProcessor, ReadonlyFrameProcessor } from 'react-native-vision-camera';
|
|
44
|
+
import { type CaptureHeaderProps } from './CaptureHeader';
|
|
45
|
+
import { type CapturePreviewAction } from './CapturePreview';
|
|
46
|
+
import { type CaptureThumbnailItem } from './CaptureThumbnailStrip';
|
|
47
|
+
import { type DeviceOrientation } from './useDeviceOrientation';
|
|
44
48
|
export type CaptureSource = 'ar' | 'non-ar';
|
|
49
|
+
/**
|
|
50
|
+
* v0.13.2 — which capture sources the host ALLOWS. A constraint on top
|
|
51
|
+
* of `defaultCaptureSource` (which picks the initial source within this
|
|
52
|
+
* constraint):
|
|
53
|
+
* 'both' — AR and non-AR both available; AR toggle is shown.
|
|
54
|
+
* 'ar' — AR only; AR toggle hidden (nothing to switch to), and the
|
|
55
|
+
* 0.5× lens chooser is hidden (ARKit/ARCore don't expose the
|
|
56
|
+
* ultra-wide).
|
|
57
|
+
* 'non-ar' — non-AR only; AR toggle hidden.
|
|
58
|
+
*/
|
|
59
|
+
export type CaptureSourcesMode = 'ar' | 'non-ar' | 'both';
|
|
45
60
|
export type CameraLens = '1x' | '0.5x';
|
|
46
61
|
export type StitchMode = 'auto' | 'panorama' | 'scans';
|
|
47
62
|
export type Blender = 'multiband' | 'feather';
|
|
@@ -139,6 +154,19 @@ export interface CameraProps {
|
|
|
139
154
|
enablePhotoMode?: boolean;
|
|
140
155
|
enablePanoramaMode?: boolean;
|
|
141
156
|
showSettingsButton?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* v0.13.2 — which capture sources the host allows (default `'both'`).
|
|
159
|
+
* Constrains both the runtime AR toggle and `defaultCaptureSource`:
|
|
160
|
+
* - `'both'` : AR + non-AR; the AR toggle is shown so the user can
|
|
161
|
+
* switch at runtime.
|
|
162
|
+
* - `'ar'` : AR only. AR toggle hidden (nothing to toggle); the
|
|
163
|
+
* 0.5× lens chooser is also hidden (ARKit/ARCore can't use the
|
|
164
|
+
* ultra-wide), so the camera stays on the AR-capable 1× lens.
|
|
165
|
+
* - `'non-ar'`: non-AR only. AR toggle hidden.
|
|
166
|
+
* When set to a single source, that source wins regardless of
|
|
167
|
+
* `defaultCaptureSource`.
|
|
168
|
+
*/
|
|
169
|
+
captureSources?: CaptureSourcesMode;
|
|
142
170
|
style?: StyleProp<ViewStyle>;
|
|
143
171
|
/**
|
|
144
172
|
* Which incremental stitcher engine to drive. Default
|
|
@@ -211,6 +239,158 @@ export interface CameraProps {
|
|
|
211
239
|
* for an abandoned capture.
|
|
212
240
|
*/
|
|
213
241
|
onCaptureAbandoned?: (reason: 'orientation-drift') => void;
|
|
242
|
+
/**
|
|
243
|
+
* v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
|
|
244
|
+
*
|
|
245
|
+
* - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
|
|
246
|
+
* state internally. Tapping the built-in flash button toggles
|
|
247
|
+
* it on/off. `onFlashChange` (if supplied) fires for telemetry.
|
|
248
|
+
* - **Controlled** (supply `flash`): the parent owns the state.
|
|
249
|
+
* The built-in button still renders and fires `onFlashChange`
|
|
250
|
+
* on press, but it's a no-op unless the parent updates `flash`
|
|
251
|
+
* in response.
|
|
252
|
+
*
|
|
253
|
+
* Both shapes coexist with the v0.13 "flash button is on by default"
|
|
254
|
+
* built-in (see the bottom-left bar slot in the JSX). Hosts that
|
|
255
|
+
* want their own flash chrome can opt out via `showFlashButton={false}`
|
|
256
|
+
* and drive the underlying torch by controlling `flash` directly.
|
|
257
|
+
*
|
|
258
|
+
* ## AR-mode behaviour
|
|
259
|
+
*
|
|
260
|
+
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
|
|
261
|
+
* ARKit / ARCore own the `AVCaptureDevice` and don't expose the
|
|
262
|
+
* torch through vision-camera's pipeline. The built-in flash
|
|
263
|
+
* button renders as visibly disabled (a11y label "Flash unavailable
|
|
264
|
+
* in AR mode") and `flash` is forced to `'off'` regardless of
|
|
265
|
+
* controlled/uncontrolled state. Hosts that need flash should
|
|
266
|
+
* toggle to non-AR before enabling.
|
|
267
|
+
*/
|
|
268
|
+
flash?: 'on' | 'off';
|
|
269
|
+
/**
|
|
270
|
+
* v0.13.0 — fires when the user taps the built-in flash button.
|
|
271
|
+
* In uncontrolled mode, the internal state has already flipped
|
|
272
|
+
* (single render delay). In controlled mode, the parent must
|
|
273
|
+
* update the `flash` prop in response or the visual toggle is
|
|
274
|
+
* a no-op. Useful in either mode for telemetry.
|
|
275
|
+
*/
|
|
276
|
+
onFlashChange?: (next: 'on' | 'off') => void;
|
|
277
|
+
/**
|
|
278
|
+
* v0.13.0 — show the built-in flash button in the bottom-left
|
|
279
|
+
* slot. Defaults to `true`. Hosts that render their own flash
|
|
280
|
+
* chrome (and drive the underlying torch via the controlled
|
|
281
|
+
* `flash` prop) can opt out by setting this to `false`.
|
|
282
|
+
*/
|
|
283
|
+
showFlashButton?: boolean;
|
|
284
|
+
/**
|
|
285
|
+
* v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
|
|
286
|
+
* renders a top-of-screen header showing this title (centred)
|
|
287
|
+
* with an optional back affordance + guidance subtitle + the
|
|
288
|
+
* existing settings gear absorbed into the header's right side.
|
|
289
|
+
*
|
|
290
|
+
* When `headerTitle` is undefined the header is not rendered
|
|
291
|
+
* (matches pre-v0.13 behaviour: top of preview is bare except
|
|
292
|
+
* for the standalone settings gear gated on `showSettingsButton`).
|
|
293
|
+
*
|
|
294
|
+
* Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
|
|
295
|
+
* and `headerColors` to customise the rest of the header. Hosts
|
|
296
|
+
* that need richer header chrome can omit `headerTitle` and
|
|
297
|
+
* compose their own `<CaptureHeader>` above `<Camera>`.
|
|
298
|
+
*/
|
|
299
|
+
headerTitle?: string;
|
|
300
|
+
/**
|
|
301
|
+
* v0.13.0 — header back-button callback. When supplied (and
|
|
302
|
+
* `headerTitle` is set), the header renders a back affordance
|
|
303
|
+
* on the left. Omitted ⇒ no back button (the title stays
|
|
304
|
+
* centred).
|
|
305
|
+
*/
|
|
306
|
+
onHeaderBack?: () => void;
|
|
307
|
+
/**
|
|
308
|
+
* v0.13.0 — header back-button label. Defaults to "‹ Back".
|
|
309
|
+
* No effect unless `headerTitle` and `onHeaderBack` are both set.
|
|
310
|
+
*/
|
|
311
|
+
headerBackLabel?: string;
|
|
312
|
+
/**
|
|
313
|
+
* v0.13.0 — optional second-line subtitle shown below the
|
|
314
|
+
* header title. E.g. "Photograph the promotional cola end cap."
|
|
315
|
+
* Renders nothing when undefined. No effect unless `headerTitle`
|
|
316
|
+
* is set.
|
|
317
|
+
*/
|
|
318
|
+
headerGuidance?: string;
|
|
319
|
+
/**
|
|
320
|
+
* v0.13.0 — colour overrides for the built-in header. Defaults
|
|
321
|
+
* are white-on-black to stay legible over the camera preview.
|
|
322
|
+
* No effect unless `headerTitle` is set.
|
|
323
|
+
*/
|
|
324
|
+
headerColors?: CaptureHeaderProps['colors'];
|
|
325
|
+
/**
|
|
326
|
+
* v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
|
|
327
|
+
* built-in `CaptureThumbnailStrip` above the bottom controls
|
|
328
|
+
* showing the host's capture history. Each item is a plain
|
|
329
|
+
* `{ id, uri, width?, height? }` object; the strip handles
|
|
330
|
+
* aspect-ratio rendering, tap-to-preview, and the count line.
|
|
331
|
+
*
|
|
332
|
+
* Omit (`undefined`) to skip the strip entirely. Hosts using
|
|
333
|
+
* the strip independently (e.g. on a non-camera screen) can keep
|
|
334
|
+
* importing `CaptureThumbnailStrip` directly from the library —
|
|
335
|
+
* the prop here is the convenience wiring for in-`<Camera>` use.
|
|
336
|
+
*
|
|
337
|
+
* Captures emitted by `<Camera>`'s `onCapture` are NOT added to
|
|
338
|
+
* this array automatically — the host owns the canonical list
|
|
339
|
+
* (typically persisted to its own DB) and updates the prop in
|
|
340
|
+
* response. This matches the SDK's "Camera owns runtime state,
|
|
341
|
+
* host persists" pattern.
|
|
342
|
+
*/
|
|
343
|
+
thumbnails?: CaptureThumbnailItem[];
|
|
344
|
+
/**
|
|
345
|
+
* v0.13.0 — minimum-photos hint for the count line. Renders
|
|
346
|
+
* "n / minPhotos min" with the success colour when reached,
|
|
347
|
+
* warning colour otherwise.
|
|
348
|
+
*/
|
|
349
|
+
thumbnailsMin?: number;
|
|
350
|
+
/**
|
|
351
|
+
* v0.13.0 — maximum-photos hint for the count line. Renders
|
|
352
|
+
* "· maxPhotos max" suffix. No enforcement — the host decides
|
|
353
|
+
* what to do at the cap.
|
|
354
|
+
*/
|
|
355
|
+
thumbnailsMax?: number;
|
|
356
|
+
/**
|
|
357
|
+
* v0.13.0 — tap handler for thumbnails. When set, replaces the
|
|
358
|
+
* strip's built-in tap-to-preview modal; the host shows its own
|
|
359
|
+
* preview UI (e.g. with delete / recapture buttons gated on
|
|
360
|
+
* sync state). Omit to use the built-in preview.
|
|
361
|
+
*/
|
|
362
|
+
onThumbnailPress?: (item: CaptureThumbnailItem) => void;
|
|
363
|
+
/**
|
|
364
|
+
* v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
|
|
365
|
+
* modal as `visible`. Use this for post-stitch confirmation:
|
|
366
|
+
* after `onCapture` emits, the host stores the result and sets
|
|
367
|
+
* `capturePreview` to the new image, with `capturePreviewActions`
|
|
368
|
+
* = `[Discard, Save]` (or similar). Setting `undefined` hides
|
|
369
|
+
* the modal.
|
|
370
|
+
*
|
|
371
|
+
* Hosts using the modal for thumbnail tap-to-preview can leave
|
|
372
|
+
* this undefined and let the built-in strip's preview handle
|
|
373
|
+
* that case.
|
|
374
|
+
*/
|
|
375
|
+
capturePreview?: {
|
|
376
|
+
imageUri: string;
|
|
377
|
+
imageWidth?: number;
|
|
378
|
+
imageHeight?: number;
|
|
379
|
+
title?: string;
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* v0.13.0 — action buttons rendered along the bottom of the
|
|
383
|
+
* `CapturePreview` modal. Empty array (or undefined) renders
|
|
384
|
+
* no buttons, only the close affordance.
|
|
385
|
+
*/
|
|
386
|
+
capturePreviewActions?: CapturePreviewAction[];
|
|
387
|
+
/**
|
|
388
|
+
* v0.13.0 — fires when the user dismisses the `capturePreview`
|
|
389
|
+
* modal (tap close, backdrop tap, hardware back on Android).
|
|
390
|
+
* The host is expected to clear the `capturePreview` prop in
|
|
391
|
+
* response.
|
|
392
|
+
*/
|
|
393
|
+
onCapturePreviewClose?: () => void;
|
|
214
394
|
/**
|
|
215
395
|
* Optional host-supplied vision-camera frame processor.
|
|
216
396
|
*
|
|
@@ -305,4 +485,50 @@ export interface CameraProps {
|
|
|
305
485
|
* The public `<Camera>` component.
|
|
306
486
|
*/
|
|
307
487
|
export declare function Camera(props: CameraProps): React.JSX.Element;
|
|
488
|
+
/**
|
|
489
|
+
* v0.12.0 — JS edge corresponding to the physical home-indicator
|
|
490
|
+
* side of the device. This is where the shutter + controls anchor
|
|
491
|
+
* to so they're always within thumb reach of the user's grip
|
|
492
|
+
* (matching iOS Camera's behaviour).
|
|
493
|
+
*
|
|
494
|
+
* Combines two signals:
|
|
495
|
+
* - `jsLandscape`: whether the OS rotated the framebuffer. True
|
|
496
|
+
* only for non-locked hosts in device-landscape.
|
|
497
|
+
* - `deviceOrient`: physical device orientation from the sensor.
|
|
498
|
+
*
|
|
499
|
+
* Truth table:
|
|
500
|
+
* | jsLandscape | deviceOrient | edge |
|
|
501
|
+
* |--- |--- |--- |
|
|
502
|
+
* | false | any | bottom | (portrait JS coords —
|
|
503
|
+
* | | | | device-bottom = JS-bottom
|
|
504
|
+
* | | | | in both locked and
|
|
505
|
+
* | | | | non-locked-portrait)
|
|
506
|
+
* | true | landscape-left | right | (screen rotated, home
|
|
507
|
+
* | | | | indicator on user-right)
|
|
508
|
+
* | true | landscape-right | left | (mirror)
|
|
509
|
+
*
|
|
510
|
+
* Caveats:
|
|
511
|
+
* - Non-locked + upside-down doesn't surface JS-top here because
|
|
512
|
+
* upside-down doesn't change window dimensions; we can't
|
|
513
|
+
* distinguish locked-portrait-with-device-flipped from
|
|
514
|
+
* non-locked-portrait-with-screen-flipped-180°. Defaults to
|
|
515
|
+
* JS-bottom which matches the more common locked case. Add
|
|
516
|
+
* handling here when a host needs upside-down support.
|
|
517
|
+
* - jsLandscape=true with non-landscape device shouldn't happen
|
|
518
|
+
* in steady state — only during a transition mid-rotation.
|
|
519
|
+
* Falls through to 'right' as a defensive default.
|
|
520
|
+
*/
|
|
521
|
+
type HomeIndicatorEdge = 'bottom' | 'top' | 'left' | 'right';
|
|
522
|
+
declare function homeIndicatorEdge(jsLandscape: boolean, deviceOrient: DeviceOrientation): HomeIndicatorEdge;
|
|
523
|
+
/**
|
|
524
|
+
* v0.12.0 — true when the anchor edge is on a side (left/right), so
|
|
525
|
+
* the band + shutter row need to be vertical strips. Top/bottom
|
|
526
|
+
* anchors yield horizontal strips.
|
|
527
|
+
*/
|
|
528
|
+
declare function isSideEdge(edge: HomeIndicatorEdge): boolean;
|
|
529
|
+
/** @internal test-only — see `homeIndicatorEdge`. */
|
|
530
|
+
export declare const _homeIndicatorEdgeForTests: typeof homeIndicatorEdge;
|
|
531
|
+
/** @internal test-only — see `isSideEdge`. */
|
|
532
|
+
export declare const _isSideEdgeForTests: typeof isSideEdge;
|
|
533
|
+
export {};
|
|
308
534
|
//# sourceMappingURL=Camera.d.ts.map
|
package/dist/camera/Camera.js
CHANGED
|
@@ -74,7 +74,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
74
74
|
};
|
|
75
75
|
})();
|
|
76
76
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
77
|
-
exports.CameraError = void 0;
|
|
77
|
+
exports._isSideEdgeForTests = exports._homeIndicatorEdgeForTests = exports.CameraError = void 0;
|
|
78
78
|
exports.Camera = Camera;
|
|
79
79
|
const react_1 = __importStar(require("react"));
|
|
80
80
|
const react_native_1 = require("react-native");
|
|
@@ -83,6 +83,9 @@ const useARSession_1 = require("../ar/useARSession");
|
|
|
83
83
|
const ARCameraView_1 = require("./ARCameraView");
|
|
84
84
|
const CameraShutter_1 = require("./CameraShutter");
|
|
85
85
|
const CameraView_1 = require("./CameraView");
|
|
86
|
+
const CaptureHeader_1 = require("./CaptureHeader");
|
|
87
|
+
const CapturePreview_1 = require("./CapturePreview");
|
|
88
|
+
const CaptureThumbnailStrip_1 = require("./CaptureThumbnailStrip");
|
|
86
89
|
const CaptureStatusOverlay_1 = require("./CaptureStatusOverlay");
|
|
87
90
|
const CaptureDebugOverlay_1 = require("./CaptureDebugOverlay");
|
|
88
91
|
const CaptureMemoryPill_1 = require("./CaptureMemoryPill");
|
|
@@ -96,6 +99,7 @@ const buildPanoramaInitialSettings_1 = require("./buildPanoramaInitialSettings")
|
|
|
96
99
|
const lowMemDevice_1 = require("./lowMemDevice");
|
|
97
100
|
const useCapture_1 = require("./useCapture");
|
|
98
101
|
const useDeviceOrientation_1 = require("./useDeviceOrientation");
|
|
102
|
+
const useContentRotation_1 = require("./useContentRotation");
|
|
99
103
|
const useOrientationDrift_1 = require("./useOrientationDrift");
|
|
100
104
|
const OrientationDriftModal_1 = require("./OrientationDriftModal");
|
|
101
105
|
const incremental_1 = require("../stitching/incremental");
|
|
@@ -113,10 +117,10 @@ class CameraError extends Error {
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
exports.CameraError = CameraError;
|
|
116
|
-
function LensChip({ lens, onChange, has0_5x }) {
|
|
120
|
+
function LensChip({ lens, onChange, has0_5x, contentRotation }) {
|
|
117
121
|
if (!has0_5x) {
|
|
118
122
|
return (react_1.default.createElement(react_native_1.View, { style: [lensChipStyles.container, lensChipStyles.singleLens] },
|
|
119
|
-
react_1.default.createElement(react_native_1.Text, { style: lensChipStyles.label }, "1\u00D7")));
|
|
123
|
+
react_1.default.createElement(react_native_1.Text, { style: [lensChipStyles.label, contentRotation] }, "1\u00D7")));
|
|
120
124
|
}
|
|
121
125
|
return (react_1.default.createElement(react_native_1.View, { style: lensChipStyles.container },
|
|
122
126
|
react_1.default.createElement(react_native_1.Pressable, { onPress: () => onChange('0.5x'), accessibilityRole: "button", accessibilityLabel: "0.5x ultra-wide lens", accessibilityState: { selected: lens === '0.5x' }, style: [
|
|
@@ -126,6 +130,7 @@ function LensChip({ lens, onChange, has0_5x }) {
|
|
|
126
130
|
react_1.default.createElement(react_native_1.Text, { style: [
|
|
127
131
|
lensChipStyles.label,
|
|
128
132
|
lens === '0.5x' && lensChipStyles.labelActive,
|
|
133
|
+
contentRotation,
|
|
129
134
|
] }, "0.5\u00D7")),
|
|
130
135
|
react_1.default.createElement(react_native_1.Pressable, { onPress: () => onChange('1x'), accessibilityRole: "button", accessibilityLabel: "1x wide-angle lens", accessibilityState: { selected: lens === '1x' }, style: [
|
|
131
136
|
lensChipStyles.pill,
|
|
@@ -134,6 +139,7 @@ function LensChip({ lens, onChange, has0_5x }) {
|
|
|
134
139
|
react_1.default.createElement(react_native_1.Text, { style: [
|
|
135
140
|
lensChipStyles.label,
|
|
136
141
|
lens === '1x' && lensChipStyles.labelActive,
|
|
142
|
+
contentRotation,
|
|
137
143
|
] }, "1\u00D7"))));
|
|
138
144
|
}
|
|
139
145
|
const lensChipStyles = react_native_1.StyleSheet.create({
|
|
@@ -166,11 +172,12 @@ const lensChipStyles = react_native_1.StyleSheet.create({
|
|
|
166
172
|
color: '#1a1a1a',
|
|
167
173
|
},
|
|
168
174
|
});
|
|
169
|
-
function ARToggle({ arEnabled, onToggle }) {
|
|
175
|
+
function ARToggle({ arEnabled, onToggle, contentRotation }) {
|
|
170
176
|
return (react_1.default.createElement(react_native_1.Pressable, { onPress: onToggle, accessibilityRole: "switch", accessibilityLabel: `AR mode ${arEnabled ? 'on' : 'off'}`, accessibilityState: { checked: arEnabled }, style: [arToggleStyles.container, arEnabled && arToggleStyles.containerOn] },
|
|
171
177
|
react_1.default.createElement(react_native_1.Text, { style: [
|
|
172
178
|
arToggleStyles.label,
|
|
173
179
|
arEnabled && arToggleStyles.labelOn,
|
|
180
|
+
contentRotation,
|
|
174
181
|
] }, "AR")));
|
|
175
182
|
}
|
|
176
183
|
const arToggleStyles = react_native_1.StyleSheet.create({
|
|
@@ -272,7 +279,14 @@ function extractPanoramaOverrides(props) {
|
|
|
272
279
|
* The public `<Camera>` component.
|
|
273
280
|
*/
|
|
274
281
|
function Camera(props) {
|
|
275
|
-
const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
|
|
282
|
+
const { defaultCaptureSource = 'ar', defaultLens = '1x', captureSources = 'both', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
|
|
283
|
+
// v0.13.2 — capture-source constraint (default 'both'). Derives which
|
|
284
|
+
// sources are permitted; `captureSources` overrides any conflicting
|
|
285
|
+
// `defaultCaptureSource`. Used to constrain the initial AR preference
|
|
286
|
+
// and to hide the AR toggle / lens chooser below.
|
|
287
|
+
const arAllowed = captureSources !== 'non-ar';
|
|
288
|
+
const nonArAllowed = captureSources !== 'ar';
|
|
289
|
+
const arOnly = captureSources === 'ar';
|
|
276
290
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
277
291
|
// v0.12.0 — JS-layout orientation independent of device-physical.
|
|
278
292
|
// `useWindowDimensions().width > height` tells us if the OS
|
|
@@ -283,8 +297,19 @@ function Camera(props) {
|
|
|
283
297
|
const jsWindow = (0, react_native_1.useWindowDimensions)();
|
|
284
298
|
const jsLandscape = jsWindow.width > jsWindow.height;
|
|
285
299
|
// ── State ───────────────────────────────────────────────────────
|
|
286
|
-
|
|
287
|
-
|
|
300
|
+
// v0.13.2 — initial AR preference honours `defaultCaptureSource` but
|
|
301
|
+
// is clamped to the `captureSources` constraint: 'ar' forces on,
|
|
302
|
+
// 'non-ar' forces off, 'both' uses the default.
|
|
303
|
+
const [arPreference, setArPreference] = (0, react_1.useState)(!arAllowed ? false : !nonArAllowed ? true : defaultCaptureSource === 'ar');
|
|
304
|
+
// v0.13.2 — `arOnly` forces the 1× lens (the ultra-wide isn't usable
|
|
305
|
+
// in AR), and the lens chooser is hidden in that mode.
|
|
306
|
+
const [lens, setLens] = (0, react_1.useState)(arOnly ? '1x' : defaultLens);
|
|
307
|
+
// v0.13.0 — flash state. Controlled by `controlledFlash` when the
|
|
308
|
+
// host supplies the `flash` prop; otherwise owned internally and
|
|
309
|
+
// toggled by the built-in flash button. `effectiveFlash` below
|
|
310
|
+
// also forces 'off' in AR mode (ARKit / ARCore own the device's
|
|
311
|
+
// torch and don't surface it through vision-camera's pipeline).
|
|
312
|
+
const [internalFlash, setInternalFlash] = (0, react_1.useState)('off');
|
|
288
313
|
const [settings, setSettings] = (0, react_1.useState)(() => (0, buildPanoramaInitialSettings_1.buildPanoramaInitialSettings)(extractPanoramaOverrides(props), (0, lowMemDevice_1.isLowMemDevice)()));
|
|
289
314
|
const [settingsModalVisible, setSettingsModalVisible] = (0, react_1.useState)(false);
|
|
290
315
|
const [statusPhase, setStatusPhase] = (0, react_1.useState)('idle');
|
|
@@ -308,6 +333,14 @@ function Camera(props) {
|
|
|
308
333
|
const isAR = effectiveCaptureSource === 'ar';
|
|
309
334
|
const isNonAR = !isAR;
|
|
310
335
|
const deviceOrientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
|
|
336
|
+
// v0.13.1 — counter-rotation for control CONTENT (AR toggle, lens
|
|
337
|
+
// pill, flash icon, thumbnails) so their labels read upright relative
|
|
338
|
+
// to gravity when the device is held landscape under a PORTRAIT-LOCKED
|
|
339
|
+
// host (the recommended config — the JS framebuffer stays portrait, so
|
|
340
|
+
// without this the labels render at 90°). Returns `{}` (no-op) in the
|
|
341
|
+
// common upright cases, including non-locked hosts where the OS already
|
|
342
|
+
// rotated the framebuffer. See `useContentRotation` truth table.
|
|
343
|
+
const contentRotation = (0, useContentRotation_1.useContentRotation)();
|
|
311
344
|
// ── Camera handoff gate ─────────────────────────────────────────
|
|
312
345
|
//
|
|
313
346
|
// The placeholder rendered while the underlying camera identity
|
|
@@ -336,6 +369,33 @@ function Camera(props) {
|
|
|
336
369
|
const inFlightTransition = settledIsARRef.current !== isAR
|
|
337
370
|
|| settledLensRef.current !== lens
|
|
338
371
|
|| cameraTransitioning;
|
|
372
|
+
// ── v0.13.1 — Android portrait lock ─────────────────────────────
|
|
373
|
+
//
|
|
374
|
+
// Android lets a mounted view force its host Activity's orientation,
|
|
375
|
+
// so `<Camera>` guarantees a portrait capture surface regardless of
|
|
376
|
+
// the host app's manifest (even a landscape/unlocked host gets a
|
|
377
|
+
// portrait camera while `<Camera>` is mounted). The lock lives on
|
|
378
|
+
// the Activity via the native `RNSARSession` module, so it covers
|
|
379
|
+
// BOTH the AR (ARCore) and non-AR (vision-camera) capture paths.
|
|
380
|
+
//
|
|
381
|
+
// iOS is intentionally NOT locked here: iOS supported orientations
|
|
382
|
+
// are a static Info.plist declaration the host owns, and we want iOS
|
|
383
|
+
// hosts to be able to support landscape/unlocked capture. Hosts that
|
|
384
|
+
// want a portrait-only iOS app set UISupportedInterfaceOrientations
|
|
385
|
+
// themselves.
|
|
386
|
+
//
|
|
387
|
+
// Empty dep array — lock on mount, restore the host's PRIOR
|
|
388
|
+
// orientation on unmount (the native side captures it).
|
|
389
|
+
(0, react_1.useEffect)(() => {
|
|
390
|
+
if (react_native_1.Platform.OS !== 'android')
|
|
391
|
+
return undefined;
|
|
392
|
+
const arModule = react_native_1.NativeModules
|
|
393
|
+
.RNSARSession;
|
|
394
|
+
arModule?.lockPortrait?.();
|
|
395
|
+
return () => {
|
|
396
|
+
arModule?.unlockOrientation?.();
|
|
397
|
+
};
|
|
398
|
+
}, []);
|
|
339
399
|
// ── Notify parent of capture-source changes ─────────────────────
|
|
340
400
|
const lastEmittedSourceRef = (0, react_1.useRef)(null);
|
|
341
401
|
(0, react_1.useEffect)(() => {
|
|
@@ -344,19 +404,22 @@ function Camera(props) {
|
|
|
344
404
|
onCaptureSourceChange?.(effectiveCaptureSource);
|
|
345
405
|
}
|
|
346
406
|
}, [effectiveCaptureSource, onCaptureSourceChange]);
|
|
347
|
-
// ── Lens chip availability ──────────────────────────────────────
|
|
348
|
-
// TODO follow-up: probe the device's available physical lenses via
|
|
349
|
-
// vision-camera's `useCameraDevices` and surface in
|
|
350
|
-
// `useCapture().availablePhysicalDevices`. For now we assume the
|
|
351
|
-
// 0.5x ultra-wide exists on modern devices. When it doesn't, the
|
|
352
|
-
// lens chip degenerates to a static 1× indicator (see LensChip).
|
|
353
|
-
const has0_5x = true;
|
|
354
407
|
// ── Capture hooks ───────────────────────────────────────────────
|
|
408
|
+
// v0.13.2 — pass the active `lens` so useCapture uses capability-aware
|
|
409
|
+
// selection (multi-cam zoom-switch where available, standalone-ultra-
|
|
410
|
+
// wide swap otherwise). Replaces the old per-lens
|
|
411
|
+
// `preferredPhysicalDevice` request that mis-selected on some phones.
|
|
355
412
|
const capture = (0, useCapture_1.useCapture)({
|
|
356
413
|
cameraPosition: 'back',
|
|
357
414
|
enableQualityChecks: false,
|
|
358
|
-
|
|
415
|
+
lens,
|
|
359
416
|
});
|
|
417
|
+
// ── Lens chip availability ──────────────────────────────────────
|
|
418
|
+
// v0.13.2 — real device capability from `useCapture` (which uses
|
|
419
|
+
// `selectCaptureDevice`). True only when the device actually exposes
|
|
420
|
+
// an ultra-wide reachable via a multi-cam zoom OR a standalone
|
|
421
|
+
// ultra-wide device; false on wide-only hardware (chip hides).
|
|
422
|
+
const has0_5x = capture.has0_5x;
|
|
360
423
|
const incremental = (0, useIncrementalStitcher_1.useIncrementalStitcher)();
|
|
361
424
|
const visionCameraRef = (0, react_1.useRef)(null);
|
|
362
425
|
const arViewRef = (0, react_1.useRef)(null);
|
|
@@ -867,6 +930,44 @@ function Camera(props) {
|
|
|
867
930
|
const handleARToggle = (0, react_1.useCallback)(() => {
|
|
868
931
|
setArPreference((prev) => !prev);
|
|
869
932
|
}, []);
|
|
933
|
+
// ── v0.13.0 — Flash control ─────────────────────────────────────
|
|
934
|
+
//
|
|
935
|
+
// `flashRequested` is what the host / built-in button asks for.
|
|
936
|
+
// `effectiveFlash` is what we drive into vision-camera (non-AR). AR
|
|
937
|
+
// mode forces 'off' (flash is hidden in AR; ARKit/ARCore own the
|
|
938
|
+
// device) so vision-camera — which isn't the active camera in AR —
|
|
939
|
+
// doesn't fight for it.
|
|
940
|
+
//
|
|
941
|
+
// v0.13.1 — the ACTIVE device's torch capability is the source of
|
|
942
|
+
// truth. The ultra-wide (0.5×) lens has no flash/torch unit on most
|
|
943
|
+
// phones, so vision-camera throws `flash-not-available` if we pass
|
|
944
|
+
// flash="on" while it's selected. `capture.device.hasTorch` (from
|
|
945
|
+
// vision-camera's device list) tells us definitively; we hide the
|
|
946
|
+
// flash control and force 'off' when the device can't flash.
|
|
947
|
+
// v0.13.2 — `capture.deviceHasTorch` reflects the MOUNTED device. In
|
|
948
|
+
// multi-cam mode this is the multi-cam device (has a torch → flash
|
|
949
|
+
// works on both 1× and 0.5× via zoom). In standalone-uw mode on 0.5×
|
|
950
|
+
// the mounted device is the torchless ultra-wide → flash hides.
|
|
951
|
+
const deviceHasTorch = capture.deviceHasTorch;
|
|
952
|
+
const flashRequested = controlledFlash ?? internalFlash;
|
|
953
|
+
const effectiveFlash = isAR || !deviceHasTorch ? 'off' : flashRequested;
|
|
954
|
+
const toggleFlash = (0, react_1.useCallback)(() => {
|
|
955
|
+
const next = flashRequested === 'on' ? 'off' : 'on';
|
|
956
|
+
if (controlledFlash == null)
|
|
957
|
+
setInternalFlash(next);
|
|
958
|
+
onFlashChange?.(next);
|
|
959
|
+
}, [flashRequested, controlledFlash, onFlashChange]);
|
|
960
|
+
// v0.13.1 — top-right control pills (flash + AR) stack vertically
|
|
961
|
+
// UNDER the settings affordance. Anchor depends on what's above:
|
|
962
|
+
// - headerTitle set → pills clear the CaptureHeader bar
|
|
963
|
+
// (title row ≈ topInset + ~36; guidance pill adds ~28 when present)
|
|
964
|
+
// - standalone gear → pills clear the 40px gear at topInset + 8
|
|
965
|
+
// - neither → pills start where the gear would be
|
|
966
|
+
const pillStackTop = headerTitle != null
|
|
967
|
+
? insets.top + (headerGuidance != null ? 72 : 40)
|
|
968
|
+
: showSettingsButton
|
|
969
|
+
? insets.top + 8 + 44
|
|
970
|
+
: insets.top + 8;
|
|
870
971
|
// ── JSX ─────────────────────────────────────────────────────────
|
|
871
972
|
return (react_1.default.createElement(react_native_1.View, { style: [styles.container, style] },
|
|
872
973
|
inFlightTransition ? (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] },
|
|
@@ -878,7 +979,12 @@ function Camera(props) {
|
|
|
878
979
|
// the very first buffered preview frame. Android takeSnapshot
|
|
879
980
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
880
981
|
// which has run on `video` (true) for months without issue.
|
|
881
|
-
video: true, flash:
|
|
982
|
+
video: true, flash: effectiveFlash,
|
|
983
|
+
// v0.13.2 — in multi-cam mode the lens is switched via zoom
|
|
984
|
+
// on a single mounted device (0.5× → ultra-wide end, 1× →
|
|
985
|
+
// wide baseline). undefined in standalone/wide-only modes
|
|
986
|
+
// (lens = device identity, no zoom).
|
|
987
|
+
zoom: capture.deviceZoom, style: react_native_1.StyleSheet.absoluteFill,
|
|
882
988
|
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
883
989
|
// the camera producer thread for every frame. Only wired
|
|
884
990
|
// in non-AR mode; AR mode uses ARCameraView which doesn't
|
|
@@ -905,18 +1011,43 @@ function Camera(props) {
|
|
|
905
1011
|
react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { topInset: insets.top }),
|
|
906
1012
|
react_1.default.createElement(CaptureDebugOverlay_1.CaptureDebugOverlay, { incrementalState: incrementalState, imuTranslationMetres: isNonAR ? imuGate.getTranslationMetres() : null, captureSource: effectiveCaptureSource, frameSelectionMode: settings.frameSelection.mode, stitchMode: settings.stitcher.stitchMode }))),
|
|
907
1013
|
react_1.default.createElement(CaptureStitchStatsToast_1.CaptureStitchStatsToast, { message: stitchToast.message, topInset: insets.top }),
|
|
908
|
-
|
|
1014
|
+
headerTitle != null ? (react_1.default.createElement(react_native_1.View, { style: styles.headerWrap, pointerEvents: "box-none" },
|
|
1015
|
+
react_1.default.createElement(CaptureHeader_1.CaptureHeader, { title: headerTitle, onBack: onHeaderBack, backLabel: headerBackLabel, guidance: headerGuidance, colors: headerColors, topInset: insets.top, onSettingsPress: showSettingsButton
|
|
1016
|
+
? () => setSettingsModalVisible(true)
|
|
1017
|
+
: undefined }))) : (showSettingsButton && (react_1.default.createElement(SettingsButton, { topInset: insets.top, onPress: () => setSettingsModalVisible(true) }))),
|
|
909
1018
|
react_1.default.createElement(react_native_1.View, { pointerEvents: "box-none", style: bottomAreaStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation), insets.bottom + 12, insets.top + 12) },
|
|
910
1019
|
statusPhase === 'recording' && (react_1.default.createElement(PanoramaBandOverlay_1.PanoramaBandOverlay, { state: incrementalState, frameUris: batchKeyframeThumbnails, captureOrientation: deviceOrientation, vertical: isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) })),
|
|
1020
|
+
thumbnails != null && statusPhase !== 'recording' && (react_1.default.createElement(CaptureThumbnailStrip_1.CaptureThumbnailStrip, { items: thumbnails, minPhotos: thumbnailsMin, maxPhotos: thumbnailsMax, onItemPress: onThumbnailPress,
|
|
1021
|
+
// v0.13.1 — stack the idle strip vertically when the
|
|
1022
|
+
// home-indicator anchor is on a side edge (non-locked host
|
|
1023
|
+
// in landscape), matching PanoramaBandOverlay's `vertical`
|
|
1024
|
+
// so the strip rides the home-indicator edge instead of
|
|
1025
|
+
// running horizontally across the rotated screen.
|
|
1026
|
+
vertical: isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)),
|
|
1027
|
+
// v0.13.1 — counter-rotate the thumbnail images so the
|
|
1028
|
+
// captured scene reads upright in portrait-locked landscape.
|
|
1029
|
+
contentRotation: contentRotation })),
|
|
911
1030
|
react_1.default.createElement(react_native_1.View, { style: bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) },
|
|
912
1031
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft }),
|
|
913
1032
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarCenter },
|
|
914
|
-
react_1.default.createElement(LensChip, { lens: lens, onChange: handleLensChange, has0_5x: has0_5x }),
|
|
1033
|
+
!arOnly && (react_1.default.createElement(LensChip, { lens: lens, onChange: handleLensChange, has0_5x: has0_5x, contentRotation: contentRotation })),
|
|
915
1034
|
react_1.default.createElement(react_native_1.View, { style: styles.shutterWrap },
|
|
916
1035
|
react_1.default.createElement(CameraShutter_1.CameraShutter, { onTap: handleTap, onHoldStart: enablePanoramaMode ? handleHoldStart : noop, onHoldComplete: enablePanoramaMode ? handleHoldEnd : noop, isProcessing: statusPhase === 'stitching', disabled: statusPhase === 'stitching' }))),
|
|
917
|
-
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarRight }
|
|
1036
|
+
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarRight }))),
|
|
1037
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.pillStack, { top: pillStackTop }], pointerEvents: "box-none" },
|
|
1038
|
+
arAllowed && nonArAllowed && lens === '1x' && isARSupportedOnDevice && (react_1.default.createElement(ARToggle, { arEnabled: arPreference, onToggle: handleARToggle, contentRotation: contentRotation })),
|
|
1039
|
+
showFlashButton && !isAR && deviceHasTorch && (react_1.default.createElement(react_native_1.Pressable, { onPress: toggleFlash, accessibilityRole: "button", accessibilityLabel: `Flash ${flashRequested === 'on' ? 'on' : 'off'}`, accessibilityState: { selected: flashRequested === 'on' }, hitSlop: 8, style: [
|
|
1040
|
+
pillStyles.pill,
|
|
1041
|
+
flashRequested === 'on' && pillStyles.pillActive,
|
|
1042
|
+
] },
|
|
1043
|
+
react_1.default.createElement(react_native_1.Text, { style: [
|
|
1044
|
+
pillStyles.flashGlyph,
|
|
1045
|
+
flashRequested === 'on' && pillStyles.glyphActive,
|
|
1046
|
+
contentRotation,
|
|
1047
|
+
] }, "\u26A1")))),
|
|
918
1048
|
react_1.default.createElement(PanoramaSettingsModal_1.PanoramaSettingsModal, { visible: settingsModalVisible, settings: settings, onChange: setSettings, onClose: () => setSettingsModalVisible(false) }),
|
|
919
|
-
react_1.default.createElement(OrientationDriftModal_1.OrientationDriftModal, { visible: drift.drifted && !driftModalDismissed, captureOrientation: drift.captureOrientation, currentOrientation: drift.currentOrientation, onAcknowledge: () => setDriftModalDismissed(true) })
|
|
1049
|
+
react_1.default.createElement(OrientationDriftModal_1.OrientationDriftModal, { visible: drift.drifted && !driftModalDismissed, captureOrientation: drift.captureOrientation, currentOrientation: drift.currentOrientation, onAcknowledge: () => setDriftModalDismissed(true) }),
|
|
1050
|
+
react_1.default.createElement(CapturePreview_1.CapturePreview, { visible: capturePreview != null, imageUri: capturePreview?.imageUri ?? '', imageWidth: capturePreview?.imageWidth, imageHeight: capturePreview?.imageHeight, title: capturePreview?.title, actions: capturePreviewActions, onClose: onCapturePreviewClose ?? noop })));
|
|
920
1051
|
}
|
|
921
1052
|
function noop() {
|
|
922
1053
|
/* no-op handler used when panorama mode is disabled */
|
|
@@ -938,6 +1069,16 @@ function homeIndicatorEdge(jsLandscape, deviceOrient) {
|
|
|
938
1069
|
function isSideEdge(edge) {
|
|
939
1070
|
return edge === 'left' || edge === 'right';
|
|
940
1071
|
}
|
|
1072
|
+
// v0.13.1 — test-only exports of the pure orientation-decision
|
|
1073
|
+
// functions. `homeIndicatorEdge` + `isSideEdge` together produce the
|
|
1074
|
+
// `vertical` flag that drives PanoramaBandOverlay and
|
|
1075
|
+
// CaptureThumbnailStrip layout, so they carry the orientation contract.
|
|
1076
|
+
// Unit-tested via these handles (the lib's jest config is pure-TS and
|
|
1077
|
+
// can't mount <Camera>; see jest.config.js).
|
|
1078
|
+
/** @internal test-only — see `homeIndicatorEdge`. */
|
|
1079
|
+
exports._homeIndicatorEdgeForTests = homeIndicatorEdge;
|
|
1080
|
+
/** @internal test-only — see `isSideEdge`. */
|
|
1081
|
+
exports._isSideEdgeForTests = isSideEdge;
|
|
941
1082
|
/**
|
|
942
1083
|
* v0.12.0 — bottom-controls outer container positioning. Anchors
|
|
943
1084
|
* to the home-indicator JS edge with the appropriate flex direction
|
|
@@ -1045,6 +1186,8 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1045
1186
|
},
|
|
1046
1187
|
bottomBarLeft: {
|
|
1047
1188
|
flex: 1,
|
|
1189
|
+
alignItems: 'flex-start',
|
|
1190
|
+
justifyContent: 'flex-end',
|
|
1048
1191
|
},
|
|
1049
1192
|
bottomBarCenter: {
|
|
1050
1193
|
flex: 1,
|
|
@@ -1058,5 +1201,50 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1058
1201
|
shutterWrap: {
|
|
1059
1202
|
marginTop: 12,
|
|
1060
1203
|
},
|
|
1204
|
+
headerWrap: {
|
|
1205
|
+
position: 'absolute',
|
|
1206
|
+
top: 0,
|
|
1207
|
+
left: 0,
|
|
1208
|
+
right: 0,
|
|
1209
|
+
},
|
|
1210
|
+
// v0.13.1 — `thumbnailStripWrap` removed. The strip now renders
|
|
1211
|
+
// inside the orientation-aware bottomArea container (alongside
|
|
1212
|
+
// PanoramaBandOverlay and the bottom bar) rather than as a
|
|
1213
|
+
// position-absolute overlay at hard-coded `bottom: 160`.
|
|
1214
|
+
//
|
|
1215
|
+
// v0.13.1 — top-right control pill stack (flash + AR). Absolute,
|
|
1216
|
+
// pinned to the right edge under the settings affordance; `top` is
|
|
1217
|
+
// set inline from `pillStackTop`. Column so the pills stack
|
|
1218
|
+
// vertically; gap keeps them from touching.
|
|
1219
|
+
pillStack: {
|
|
1220
|
+
position: 'absolute',
|
|
1221
|
+
right: 14,
|
|
1222
|
+
alignItems: 'flex-end',
|
|
1223
|
+
gap: 10,
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
// v0.13.1 — shared pill style for the top-right control stack. The
|
|
1227
|
+
// flash pill matches the AR toggle's shape (same padding / radius /
|
|
1228
|
+
// background) so the two read as a set.
|
|
1229
|
+
const pillStyles = react_native_1.StyleSheet.create({
|
|
1230
|
+
pill: {
|
|
1231
|
+
paddingHorizontal: 14,
|
|
1232
|
+
paddingVertical: 8,
|
|
1233
|
+
borderRadius: 16,
|
|
1234
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
1235
|
+
minWidth: 56,
|
|
1236
|
+
alignItems: 'center',
|
|
1237
|
+
justifyContent: 'center',
|
|
1238
|
+
},
|
|
1239
|
+
pillActive: {
|
|
1240
|
+
backgroundColor: '#ffd34d',
|
|
1241
|
+
},
|
|
1242
|
+
flashGlyph: {
|
|
1243
|
+
color: '#ffffff',
|
|
1244
|
+
fontSize: 18,
|
|
1245
|
+
},
|
|
1246
|
+
glyphActive: {
|
|
1247
|
+
color: '#1a1a1a',
|
|
1248
|
+
},
|
|
1061
1249
|
});
|
|
1062
1250
|
//# sourceMappingURL=Camera.js.map
|
|
@@ -25,6 +25,12 @@ export interface CameraViewProps {
|
|
|
25
25
|
device: CameraDevice | null | undefined;
|
|
26
26
|
/** Flash / torch state from ``useCapture().flash``. */
|
|
27
27
|
flash?: 'off' | 'on';
|
|
28
|
+
/**
|
|
29
|
+
* v0.13.2 — zoom factor for the mounted device. Used in multi-cam
|
|
30
|
+
* mode to switch lenses (0.5× ultra-wide ↔ 1× wide) on a single
|
|
31
|
+
* device. `undefined` leaves vision-camera at its default zoom.
|
|
32
|
+
*/
|
|
33
|
+
zoom?: number;
|
|
28
34
|
/** Whether the preview is actively rendering. Defaults to true. */
|
|
29
35
|
isActive?: boolean;
|
|
30
36
|
/**
|
|
@@ -73,7 +73,7 @@ const VC_LIFECYCLE_ERROR_CODES = new Set([
|
|
|
73
73
|
'system/camera-has-been-disconnected', // another app grabbed the camera
|
|
74
74
|
'device/camera-already-in-use', // same class as above
|
|
75
75
|
]);
|
|
76
|
-
exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', isActive = true, video = false, guidance, style, cameraProps, onError, }, ref) {
|
|
76
|
+
exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash = 'off', zoom, isActive = true, video = false, guidance, style, cameraProps, onError, }, ref) {
|
|
77
77
|
// Error filter — see `VC_LIFECYCLE_ERROR_CODES` for the swallow
|
|
78
78
|
// list rationale. `code` on vision-camera's `CameraRuntimeError`
|
|
79
79
|
// is typed as a string; treat any non-string defensively as a
|
|
@@ -97,7 +97,7 @@ exports.CameraView = (0, react_1.forwardRef)(function CameraView({ device, flash
|
|
|
97
97
|
react_1.default.createElement(react_native_1.Text, { style: styles.placeholderText }, "Initialising camera\u2026")));
|
|
98
98
|
}
|
|
99
99
|
return (react_1.default.createElement(react_native_1.View, { style: [styles.root, style] },
|
|
100
|
-
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive, photo: true, video: video,
|
|
100
|
+
react_1.default.createElement(react_native_vision_camera_1.Camera, { ref: innerRef, style: react_native_1.StyleSheet.absoluteFill, device: device, isActive: isActive, photo: true, video: video, ...(zoom != null ? { zoom } : {}),
|
|
101
101
|
// Bake the device orientation into the captured pixels.
|
|
102
102
|
// Without this, vision-camera writes the file in the camera
|
|
103
103
|
// sensor's native landscape and relies on EXIF metadata to
|