react-native-image-stitcher 0.12.0 → 0.13.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 +76 -0
- package/dist/camera/Camera.d.ts +171 -0
- package/dist/camera/Camera.js +77 -5
- package/package.json +1 -1
- package/src/camera/Camera.tsx +359 -8
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,82 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [0.13.0] — 2026-05-29
|
|
20
|
+
|
|
21
|
+
### Added — Layer-2 components absorbed into `<Camera>` (opt-out)
|
|
22
|
+
|
|
23
|
+
The flagship `<Camera>` now ships built-in defaults for every UX
|
|
24
|
+
chrome piece previously exposed only as a Layer-2 component. Hosts
|
|
25
|
+
adopting `<Camera>` directly get a complete capture surface — flash
|
|
26
|
+
button, pan-speed pill, drift-marker guide, header chrome,
|
|
27
|
+
capture-history strip, and post-stitch preview — without having to
|
|
28
|
+
import and wire each piece by hand.
|
|
29
|
+
|
|
30
|
+
All built-ins use the opt-out pattern: enabled by default, disabled
|
|
31
|
+
by setting the corresponding boolean to `false` or by omitting the
|
|
32
|
+
corresponding payload prop. Hosts that want their own chrome can
|
|
33
|
+
opt out per piece and layer custom UI on top of `<Camera>` (the
|
|
34
|
+
Layer-2 components remain exported and are unchanged).
|
|
35
|
+
|
|
36
|
+
#### Flash control
|
|
37
|
+
|
|
38
|
+
- `flash?: 'on' | 'off'` — controlled torch state. Omit to let
|
|
39
|
+
`<Camera>` own it internally.
|
|
40
|
+
- `onFlashChange?` — fires on tap (controlled and uncontrolled both).
|
|
41
|
+
- `showFlashButton?: boolean` (default `true`) — built-in flash button
|
|
42
|
+
in the bottom-left slot. AR mode auto-disables (ARKit / ARCore own
|
|
43
|
+
the device's torch; surfaces "Flash unavailable in AR mode" a11y
|
|
44
|
+
label and greyed styling).
|
|
45
|
+
|
|
46
|
+
#### Pan guidance
|
|
47
|
+
|
|
48
|
+
- `panGuide?: boolean` (default `true`) — built-in
|
|
49
|
+
`IncrementalPanGuide` ("keep the arrow on the line" drift marker).
|
|
50
|
+
- `panoramaGuidance?: boolean` (default `true`) — built-in
|
|
51
|
+
`PanoramaGuidance` pan-speed pill.
|
|
52
|
+
- Both are gyroscope-driven and only subscribe to the sensor while
|
|
53
|
+
recording — no idle cost.
|
|
54
|
+
|
|
55
|
+
#### Header
|
|
56
|
+
|
|
57
|
+
- `headerTitle?: string` — when set, renders a built-in
|
|
58
|
+
`CaptureHeader` at the top of the screen. The existing settings
|
|
59
|
+
gear is absorbed into the header's right side (no duplicate gear).
|
|
60
|
+
- `onHeaderBack?`, `headerBackLabel?`, `headerGuidance?`,
|
|
61
|
+
`headerColors?` — pass-through to `CaptureHeader`.
|
|
62
|
+
|
|
63
|
+
#### Capture history + preview
|
|
64
|
+
|
|
65
|
+
- `thumbnails?: CaptureThumbnailItem[]` — when supplied (even `[]`),
|
|
66
|
+
renders the built-in `CaptureThumbnailStrip` above the bottom
|
|
67
|
+
controls. Hidden during recording so it doesn't overlap the
|
|
68
|
+
panorama band overlay.
|
|
69
|
+
- `thumbnailsMin?`, `thumbnailsMax?` — count-line hints.
|
|
70
|
+
- `onThumbnailPress?` — replaces the strip's built-in
|
|
71
|
+
tap-to-preview modal with a host handler.
|
|
72
|
+
- `capturePreview?` — when set, renders a built-in `CapturePreview`
|
|
73
|
+
modal showing the supplied image. Use for post-stitch
|
|
74
|
+
confirmation; the host clears the prop on dismiss via
|
|
75
|
+
`onCapturePreviewClose`.
|
|
76
|
+
- `capturePreviewActions?` — pass-through action buttons for the
|
|
77
|
+
preview modal.
|
|
78
|
+
|
|
79
|
+
### Migration
|
|
80
|
+
|
|
81
|
+
- Hosts that were importing Layer-2 components (`CaptureHeader`,
|
|
82
|
+
`CaptureControlsBar`, `IncrementalPanGuide`, `PanoramaGuidance`,
|
|
83
|
+
`CaptureThumbnailStrip`, `CapturePreview`) directly can now drop
|
|
84
|
+
those imports and use the corresponding `<Camera>` props.
|
|
85
|
+
- The Layer-2 components remain exported and unchanged in v0.13 for
|
|
86
|
+
backward compatibility. Deprecation of those exports is targeted
|
|
87
|
+
for v0.14.
|
|
88
|
+
- No behaviour change for hosts that already use `<Camera>` and
|
|
89
|
+
don't supply any of the new props — every new built-in defaults
|
|
90
|
+
to the previous (omitted) UX, except the flash button which
|
|
91
|
+
appears in the now-occupied bottom-left slot. Hosts that previously
|
|
92
|
+
rendered chrome in that slot above `<Camera>` can pass
|
|
93
|
+
`showFlashButton={false}`.
|
|
94
|
+
|
|
19
95
|
## [0.12.0] — 2026-05-28
|
|
20
96
|
|
|
21
97
|
### Added — Orientation-aware `<Camera>` (R2-lite)
|
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -41,6 +41,9 @@
|
|
|
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';
|
|
44
47
|
export type CaptureSource = 'ar' | 'non-ar';
|
|
45
48
|
export type CameraLens = '1x' | '0.5x';
|
|
46
49
|
export type StitchMode = 'auto' | 'panorama' | 'scans';
|
|
@@ -211,6 +214,174 @@ export interface CameraProps {
|
|
|
211
214
|
* for an abandoned capture.
|
|
212
215
|
*/
|
|
213
216
|
onCaptureAbandoned?: (reason: 'orientation-drift') => void;
|
|
217
|
+
/**
|
|
218
|
+
* v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
|
|
219
|
+
*
|
|
220
|
+
* - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
|
|
221
|
+
* state internally. Tapping the built-in flash button toggles
|
|
222
|
+
* it on/off. `onFlashChange` (if supplied) fires for telemetry.
|
|
223
|
+
* - **Controlled** (supply `flash`): the parent owns the state.
|
|
224
|
+
* The built-in button still renders and fires `onFlashChange`
|
|
225
|
+
* on press, but it's a no-op unless the parent updates `flash`
|
|
226
|
+
* in response.
|
|
227
|
+
*
|
|
228
|
+
* Both shapes coexist with the v0.13 "flash button is on by default"
|
|
229
|
+
* built-in (see the bottom-left bar slot in the JSX). Hosts that
|
|
230
|
+
* want their own flash chrome can opt out via `showFlashButton={false}`
|
|
231
|
+
* and drive the underlying torch by controlling `flash` directly.
|
|
232
|
+
*
|
|
233
|
+
* ## AR-mode behaviour
|
|
234
|
+
*
|
|
235
|
+
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
|
|
236
|
+
* ARKit / ARCore own the `AVCaptureDevice` and don't expose the
|
|
237
|
+
* torch through vision-camera's pipeline. The built-in flash
|
|
238
|
+
* button renders as visibly disabled (a11y label "Flash unavailable
|
|
239
|
+
* in AR mode") and `flash` is forced to `'off'` regardless of
|
|
240
|
+
* controlled/uncontrolled state. Hosts that need flash should
|
|
241
|
+
* toggle to non-AR before enabling.
|
|
242
|
+
*/
|
|
243
|
+
flash?: 'on' | 'off';
|
|
244
|
+
/**
|
|
245
|
+
* v0.13.0 — fires when the user taps the built-in flash button.
|
|
246
|
+
* In uncontrolled mode, the internal state has already flipped
|
|
247
|
+
* (single render delay). In controlled mode, the parent must
|
|
248
|
+
* update the `flash` prop in response or the visual toggle is
|
|
249
|
+
* a no-op. Useful in either mode for telemetry.
|
|
250
|
+
*/
|
|
251
|
+
onFlashChange?: (next: 'on' | 'off') => void;
|
|
252
|
+
/**
|
|
253
|
+
* v0.13.0 — show the built-in flash button in the bottom-left
|
|
254
|
+
* slot. Defaults to `true`. Hosts that render their own flash
|
|
255
|
+
* chrome (and drive the underlying torch via the controlled
|
|
256
|
+
* `flash` prop) can opt out by setting this to `false`.
|
|
257
|
+
*/
|
|
258
|
+
showFlashButton?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* v0.13.0 — show the built-in IncrementalPanGuide ("keep the
|
|
261
|
+
* arrow on the line" drift marker) while recording. Defaults
|
|
262
|
+
* to `true`. The guide is gyroscope-driven and only active
|
|
263
|
+
* during the recording phase (no idle sensor cost). Hosts that
|
|
264
|
+
* want their own pan-guide chrome can opt out via `false`.
|
|
265
|
+
*/
|
|
266
|
+
panGuide?: boolean;
|
|
267
|
+
/**
|
|
268
|
+
* v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
|
|
269
|
+
* ("Pan slowly" / "Slow down" / "Too fast") while recording.
|
|
270
|
+
* Defaults to `true`. Gyroscope-driven, only active during
|
|
271
|
+
* recording. Hosts that want their own speed chrome can opt
|
|
272
|
+
* out via `false`.
|
|
273
|
+
*/
|
|
274
|
+
panoramaGuidance?: boolean;
|
|
275
|
+
/**
|
|
276
|
+
* v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
|
|
277
|
+
* renders a top-of-screen header showing this title (centred)
|
|
278
|
+
* with an optional back affordance + guidance subtitle + the
|
|
279
|
+
* existing settings gear absorbed into the header's right side.
|
|
280
|
+
*
|
|
281
|
+
* When `headerTitle` is undefined the header is not rendered
|
|
282
|
+
* (matches pre-v0.13 behaviour: top of preview is bare except
|
|
283
|
+
* for the standalone settings gear gated on `showSettingsButton`).
|
|
284
|
+
*
|
|
285
|
+
* Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
|
|
286
|
+
* and `headerColors` to customise the rest of the header. Hosts
|
|
287
|
+
* that need richer header chrome can omit `headerTitle` and
|
|
288
|
+
* compose their own `<CaptureHeader>` above `<Camera>`.
|
|
289
|
+
*/
|
|
290
|
+
headerTitle?: string;
|
|
291
|
+
/**
|
|
292
|
+
* v0.13.0 — header back-button callback. When supplied (and
|
|
293
|
+
* `headerTitle` is set), the header renders a back affordance
|
|
294
|
+
* on the left. Omitted ⇒ no back button (the title stays
|
|
295
|
+
* centred).
|
|
296
|
+
*/
|
|
297
|
+
onHeaderBack?: () => void;
|
|
298
|
+
/**
|
|
299
|
+
* v0.13.0 — header back-button label. Defaults to "‹ Back".
|
|
300
|
+
* No effect unless `headerTitle` and `onHeaderBack` are both set.
|
|
301
|
+
*/
|
|
302
|
+
headerBackLabel?: string;
|
|
303
|
+
/**
|
|
304
|
+
* v0.13.0 — optional second-line subtitle shown below the
|
|
305
|
+
* header title. E.g. "Photograph the promotional cola end cap."
|
|
306
|
+
* Renders nothing when undefined. No effect unless `headerTitle`
|
|
307
|
+
* is set.
|
|
308
|
+
*/
|
|
309
|
+
headerGuidance?: string;
|
|
310
|
+
/**
|
|
311
|
+
* v0.13.0 — colour overrides for the built-in header. Defaults
|
|
312
|
+
* are white-on-black to stay legible over the camera preview.
|
|
313
|
+
* No effect unless `headerTitle` is set.
|
|
314
|
+
*/
|
|
315
|
+
headerColors?: CaptureHeaderProps['colors'];
|
|
316
|
+
/**
|
|
317
|
+
* v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
|
|
318
|
+
* built-in `CaptureThumbnailStrip` above the bottom controls
|
|
319
|
+
* showing the host's capture history. Each item is a plain
|
|
320
|
+
* `{ id, uri, width?, height? }` object; the strip handles
|
|
321
|
+
* aspect-ratio rendering, tap-to-preview, and the count line.
|
|
322
|
+
*
|
|
323
|
+
* Omit (`undefined`) to skip the strip entirely. Hosts using
|
|
324
|
+
* the strip independently (e.g. on a non-camera screen) can keep
|
|
325
|
+
* importing `CaptureThumbnailStrip` directly from the library —
|
|
326
|
+
* the prop here is the convenience wiring for in-`<Camera>` use.
|
|
327
|
+
*
|
|
328
|
+
* Captures emitted by `<Camera>`'s `onCapture` are NOT added to
|
|
329
|
+
* this array automatically — the host owns the canonical list
|
|
330
|
+
* (typically persisted to its own DB) and updates the prop in
|
|
331
|
+
* response. This matches the SDK's "Camera owns runtime state,
|
|
332
|
+
* host persists" pattern.
|
|
333
|
+
*/
|
|
334
|
+
thumbnails?: CaptureThumbnailItem[];
|
|
335
|
+
/**
|
|
336
|
+
* v0.13.0 — minimum-photos hint for the count line. Renders
|
|
337
|
+
* "n / minPhotos min" with the success colour when reached,
|
|
338
|
+
* warning colour otherwise.
|
|
339
|
+
*/
|
|
340
|
+
thumbnailsMin?: number;
|
|
341
|
+
/**
|
|
342
|
+
* v0.13.0 — maximum-photos hint for the count line. Renders
|
|
343
|
+
* "· maxPhotos max" suffix. No enforcement — the host decides
|
|
344
|
+
* what to do at the cap.
|
|
345
|
+
*/
|
|
346
|
+
thumbnailsMax?: number;
|
|
347
|
+
/**
|
|
348
|
+
* v0.13.0 — tap handler for thumbnails. When set, replaces the
|
|
349
|
+
* strip's built-in tap-to-preview modal; the host shows its own
|
|
350
|
+
* preview UI (e.g. with delete / recapture buttons gated on
|
|
351
|
+
* sync state). Omit to use the built-in preview.
|
|
352
|
+
*/
|
|
353
|
+
onThumbnailPress?: (item: CaptureThumbnailItem) => void;
|
|
354
|
+
/**
|
|
355
|
+
* v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
|
|
356
|
+
* modal as `visible`. Use this for post-stitch confirmation:
|
|
357
|
+
* after `onCapture` emits, the host stores the result and sets
|
|
358
|
+
* `capturePreview` to the new image, with `capturePreviewActions`
|
|
359
|
+
* = `[Discard, Save]` (or similar). Setting `undefined` hides
|
|
360
|
+
* the modal.
|
|
361
|
+
*
|
|
362
|
+
* Hosts using the modal for thumbnail tap-to-preview can leave
|
|
363
|
+
* this undefined and let the built-in strip's preview handle
|
|
364
|
+
* that case.
|
|
365
|
+
*/
|
|
366
|
+
capturePreview?: {
|
|
367
|
+
imageUri: string;
|
|
368
|
+
imageWidth?: number;
|
|
369
|
+
imageHeight?: number;
|
|
370
|
+
title?: string;
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* v0.13.0 — action buttons rendered along the bottom of the
|
|
374
|
+
* `CapturePreview` modal. Empty array (or undefined) renders
|
|
375
|
+
* no buttons, only the close affordance.
|
|
376
|
+
*/
|
|
377
|
+
capturePreviewActions?: CapturePreviewAction[];
|
|
378
|
+
/**
|
|
379
|
+
* v0.13.0 — fires when the user dismisses the `capturePreview`
|
|
380
|
+
* modal (tap close, backdrop tap, hardware back on Android).
|
|
381
|
+
* The host is expected to clear the `capturePreview` prop in
|
|
382
|
+
* response.
|
|
383
|
+
*/
|
|
384
|
+
onCapturePreviewClose?: () => void;
|
|
214
385
|
/**
|
|
215
386
|
* Optional host-supplied vision-camera frame processor.
|
|
216
387
|
*
|
package/dist/camera/Camera.js
CHANGED
|
@@ -83,13 +83,18 @@ 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");
|
|
89
92
|
const CaptureKeyframePill_1 = require("./CaptureKeyframePill");
|
|
90
93
|
const CaptureOrientationPill_1 = require("./CaptureOrientationPill");
|
|
91
94
|
const CaptureStitchStatsToast_1 = require("./CaptureStitchStatsToast");
|
|
95
|
+
const IncrementalPanGuide_1 = require("./IncrementalPanGuide");
|
|
92
96
|
const PanoramaBandOverlay_1 = require("./PanoramaBandOverlay");
|
|
97
|
+
const PanoramaGuidance_1 = require("./PanoramaGuidance");
|
|
93
98
|
const PanoramaSettingsBridge_1 = require("./PanoramaSettingsBridge");
|
|
94
99
|
const PanoramaSettingsModal_1 = require("./PanoramaSettingsModal");
|
|
95
100
|
const buildPanoramaInitialSettings_1 = require("./buildPanoramaInitialSettings");
|
|
@@ -272,7 +277,7 @@ function extractPanoramaOverrides(props) {
|
|
|
272
277
|
* The public `<Camera>` component.
|
|
273
278
|
*/
|
|
274
279
|
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;
|
|
280
|
+
const { defaultCaptureSource = 'ar', defaultLens = '1x', enablePhotoMode = true, enablePanoramaMode = true, showSettingsButton = false, style, outputDir, onCapture, onCaptureSourceChange, onLensChange, onFramesDropped, onError, onCaptureAbandoned, flash: controlledFlash, onFlashChange, showFlashButton = true, panGuide = true, panoramaGuidance = true, headerTitle, onHeaderBack, headerBackLabel, headerGuidance, headerColors, thumbnails, thumbnailsMin, thumbnailsMax, onThumbnailPress, capturePreview, capturePreviewActions, onCapturePreviewClose, frameProcessor: hostFrameProcessor, engine = 'batch-keyframe', } = props;
|
|
276
281
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
277
282
|
// v0.12.0 — JS-layout orientation independent of device-physical.
|
|
278
283
|
// `useWindowDimensions().width > height` tells us if the OS
|
|
@@ -285,6 +290,12 @@ function Camera(props) {
|
|
|
285
290
|
// ── State ───────────────────────────────────────────────────────
|
|
286
291
|
const [arPreference, setArPreference] = (0, react_1.useState)(defaultCaptureSource === 'ar');
|
|
287
292
|
const [lens, setLens] = (0, react_1.useState)(defaultLens);
|
|
293
|
+
// v0.13.0 — flash state. Controlled by `controlledFlash` when the
|
|
294
|
+
// host supplies the `flash` prop; otherwise owned internally and
|
|
295
|
+
// toggled by the built-in flash button. `effectiveFlash` below
|
|
296
|
+
// also forces 'off' in AR mode (ARKit / ARCore own the device's
|
|
297
|
+
// torch and don't surface it through vision-camera's pipeline).
|
|
298
|
+
const [internalFlash, setInternalFlash] = (0, react_1.useState)('off');
|
|
288
299
|
const [settings, setSettings] = (0, react_1.useState)(() => (0, buildPanoramaInitialSettings_1.buildPanoramaInitialSettings)(extractPanoramaOverrides(props), (0, lowMemDevice_1.isLowMemDevice)()));
|
|
289
300
|
const [settingsModalVisible, setSettingsModalVisible] = (0, react_1.useState)(false);
|
|
290
301
|
const [statusPhase, setStatusPhase] = (0, react_1.useState)('idle');
|
|
@@ -867,6 +878,22 @@ function Camera(props) {
|
|
|
867
878
|
const handleARToggle = (0, react_1.useCallback)(() => {
|
|
868
879
|
setArPreference((prev) => !prev);
|
|
869
880
|
}, []);
|
|
881
|
+
// ── v0.13.0 — Flash control ─────────────────────────────────────
|
|
882
|
+
//
|
|
883
|
+
// `flashRequested` is what the host / built-in button asks for.
|
|
884
|
+
// `effectiveFlash` is what we actually drive into vision-camera —
|
|
885
|
+
// AR mode forces 'off' because ARKit / ARCore own AVCaptureDevice
|
|
886
|
+
// and the torch isn't exposed. This way the button's visual state
|
|
887
|
+
// (a11y, styling) tracks `flashRequested` while the underlying
|
|
888
|
+
// camera always sees the correct value.
|
|
889
|
+
const flashRequested = controlledFlash ?? internalFlash;
|
|
890
|
+
const effectiveFlash = isAR ? 'off' : flashRequested;
|
|
891
|
+
const toggleFlash = (0, react_1.useCallback)(() => {
|
|
892
|
+
const next = flashRequested === 'on' ? 'off' : 'on';
|
|
893
|
+
if (controlledFlash == null)
|
|
894
|
+
setInternalFlash(next);
|
|
895
|
+
onFlashChange?.(next);
|
|
896
|
+
}, [flashRequested, controlledFlash, onFlashChange]);
|
|
870
897
|
// ── JSX ─────────────────────────────────────────────────────────
|
|
871
898
|
return (react_1.default.createElement(react_native_1.View, { style: [styles.container, style] },
|
|
872
899
|
inFlightTransition ? (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.transitionPlaceholder] },
|
|
@@ -878,7 +905,7 @@ function Camera(props) {
|
|
|
878
905
|
// the very first buffered preview frame. Android takeSnapshot
|
|
879
906
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
880
907
|
// which has run on `video` (true) for months without issue.
|
|
881
|
-
video: true, flash:
|
|
908
|
+
video: true, flash: effectiveFlash, style: react_native_1.StyleSheet.absoluteFill,
|
|
882
909
|
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
883
910
|
// the camera producer thread for every frame. Only wired
|
|
884
911
|
// in non-AR mode; AR mode uses ARCameraView which doesn't
|
|
@@ -899,24 +926,37 @@ function Camera(props) {
|
|
|
899
926
|
onError?.(new CameraError('VISION_CAMERA_RUNTIME', `${codeStr}: ${msg}`, err));
|
|
900
927
|
} })),
|
|
901
928
|
react_1.default.createElement(CaptureStatusOverlay_1.CaptureStatusOverlay, { phase: statusPhase, topInset: insets.top, recordingStartedAt: recordingStartedAt ?? undefined }),
|
|
929
|
+
panGuide && (react_1.default.createElement(IncrementalPanGuide_1.IncrementalPanGuide, { active: statusPhase === 'recording' })),
|
|
930
|
+
panoramaGuidance && (react_1.default.createElement(PanoramaGuidance_1.PanoramaGuidance, { active: statusPhase === 'recording' })),
|
|
902
931
|
settings.debug && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
903
932
|
react_1.default.createElement(CaptureOrientationPill_1.CaptureOrientationPill, { orientation: deviceOrientation, topInset: insets.top }),
|
|
904
933
|
react_1.default.createElement(CaptureKeyframePill_1.CaptureKeyframePill, { state: incrementalState, topInset: insets.top }),
|
|
905
934
|
react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { topInset: insets.top }),
|
|
906
935
|
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
936
|
react_1.default.createElement(CaptureStitchStatsToast_1.CaptureStitchStatsToast, { message: stitchToast.message, topInset: insets.top }),
|
|
908
|
-
|
|
937
|
+
headerTitle != null ? (react_1.default.createElement(react_native_1.View, { style: styles.headerWrap, pointerEvents: "box-none" },
|
|
938
|
+
react_1.default.createElement(CaptureHeader_1.CaptureHeader, { title: headerTitle, onBack: onHeaderBack, backLabel: headerBackLabel, guidance: headerGuidance, colors: headerColors, topInset: insets.top, onSettingsPress: showSettingsButton
|
|
939
|
+
? () => setSettingsModalVisible(true)
|
|
940
|
+
: undefined }))) : (showSettingsButton && (react_1.default.createElement(SettingsButton, { topInset: insets.top, onPress: () => setSettingsModalVisible(true) }))),
|
|
941
|
+
thumbnails != null && statusPhase !== 'recording' && (react_1.default.createElement(react_native_1.View, { style: styles.thumbnailStripWrap, pointerEvents: "box-none" },
|
|
942
|
+
react_1.default.createElement(CaptureThumbnailStrip_1.CaptureThumbnailStrip, { items: thumbnails, minPhotos: thumbnailsMin, maxPhotos: thumbnailsMax, onItemPress: onThumbnailPress }))),
|
|
909
943
|
react_1.default.createElement(react_native_1.View, { pointerEvents: "box-none", style: bottomAreaStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation), insets.bottom + 12, insets.top + 12) },
|
|
910
944
|
statusPhase === 'recording' && (react_1.default.createElement(PanoramaBandOverlay_1.PanoramaBandOverlay, { state: incrementalState, frameUris: batchKeyframeThumbnails, captureOrientation: deviceOrientation, vertical: isSideEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) })),
|
|
911
945
|
react_1.default.createElement(react_native_1.View, { style: bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation)) },
|
|
912
|
-
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft }
|
|
946
|
+
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarLeft }, showFlashButton && (react_1.default.createElement(react_native_1.Pressable, { onPress: isAR ? undefined : toggleFlash, accessibilityRole: "button", accessibilityLabel: isAR ? 'Flash unavailable in AR mode' : `Flash ${flashRequested === 'on' ? 'on' : 'off'}`, accessibilityState: { selected: flashRequested === 'on', disabled: isAR }, disabled: isAR, hitSlop: 8, style: [
|
|
947
|
+
styles.flashButton,
|
|
948
|
+
flashRequested === 'on' && !isAR && styles.flashButtonActive,
|
|
949
|
+
isAR && styles.flashButtonDisabled,
|
|
950
|
+
] },
|
|
951
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.flashIcon }, "\u26A1")))),
|
|
913
952
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarCenter },
|
|
914
953
|
react_1.default.createElement(LensChip, { lens: lens, onChange: handleLensChange, has0_5x: has0_5x }),
|
|
915
954
|
react_1.default.createElement(react_native_1.View, { style: styles.shutterWrap },
|
|
916
955
|
react_1.default.createElement(CameraShutter_1.CameraShutter, { onTap: handleTap, onHoldStart: enablePanoramaMode ? handleHoldStart : noop, onHoldComplete: enablePanoramaMode ? handleHoldEnd : noop, isProcessing: statusPhase === 'stitching', disabled: statusPhase === 'stitching' }))),
|
|
917
956
|
react_1.default.createElement(react_native_1.View, { style: styles.bottomBarRight }, lens === '1x' && isARSupportedOnDevice && (react_1.default.createElement(ARToggle, { arEnabled: arPreference, onToggle: handleARToggle }))))),
|
|
918
957
|
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) })
|
|
958
|
+
react_1.default.createElement(OrientationDriftModal_1.OrientationDriftModal, { visible: drift.drifted && !driftModalDismissed, captureOrientation: drift.captureOrientation, currentOrientation: drift.currentOrientation, onAcknowledge: () => setDriftModalDismissed(true) }),
|
|
959
|
+
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
960
|
}
|
|
921
961
|
function noop() {
|
|
922
962
|
/* no-op handler used when panorama mode is disabled */
|
|
@@ -1045,6 +1085,8 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1045
1085
|
},
|
|
1046
1086
|
bottomBarLeft: {
|
|
1047
1087
|
flex: 1,
|
|
1088
|
+
alignItems: 'flex-start',
|
|
1089
|
+
justifyContent: 'flex-end',
|
|
1048
1090
|
},
|
|
1049
1091
|
bottomBarCenter: {
|
|
1050
1092
|
flex: 1,
|
|
@@ -1058,5 +1100,35 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
1058
1100
|
shutterWrap: {
|
|
1059
1101
|
marginTop: 12,
|
|
1060
1102
|
},
|
|
1103
|
+
headerWrap: {
|
|
1104
|
+
position: 'absolute',
|
|
1105
|
+
top: 0,
|
|
1106
|
+
left: 0,
|
|
1107
|
+
right: 0,
|
|
1108
|
+
},
|
|
1109
|
+
thumbnailStripWrap: {
|
|
1110
|
+
position: 'absolute',
|
|
1111
|
+
left: 0,
|
|
1112
|
+
right: 0,
|
|
1113
|
+
bottom: 160,
|
|
1114
|
+
},
|
|
1115
|
+
flashButton: {
|
|
1116
|
+
width: 44,
|
|
1117
|
+
height: 44,
|
|
1118
|
+
borderRadius: 22,
|
|
1119
|
+
alignItems: 'center',
|
|
1120
|
+
justifyContent: 'center',
|
|
1121
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
1122
|
+
},
|
|
1123
|
+
flashButtonActive: {
|
|
1124
|
+
backgroundColor: '#ffd34d',
|
|
1125
|
+
},
|
|
1126
|
+
flashButtonDisabled: {
|
|
1127
|
+
opacity: 0.35,
|
|
1128
|
+
},
|
|
1129
|
+
flashIcon: {
|
|
1130
|
+
fontSize: 20,
|
|
1131
|
+
color: '#ffffff',
|
|
1132
|
+
},
|
|
1061
1133
|
});
|
|
1062
1134
|
//# sourceMappingURL=Camera.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Pose-aware panorama capture + stitching for React Native. One <Camera> component, both tap-to-photo and hold-to-pan modes, both AR-backed and IMU-fallback capture paths.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -67,13 +67,21 @@ import { useARSession } from '../ar/useARSession';
|
|
|
67
67
|
import { ARCameraView, type ARCameraViewHandle } from './ARCameraView';
|
|
68
68
|
import { CameraShutter } from './CameraShutter';
|
|
69
69
|
import { CameraView } from './CameraView';
|
|
70
|
+
import { CaptureHeader, type CaptureHeaderProps } from './CaptureHeader';
|
|
71
|
+
import { CapturePreview, type CapturePreviewAction } from './CapturePreview';
|
|
72
|
+
import {
|
|
73
|
+
CaptureThumbnailStrip,
|
|
74
|
+
type CaptureThumbnailItem,
|
|
75
|
+
} from './CaptureThumbnailStrip';
|
|
70
76
|
import { CaptureStatusOverlay, type CaptureStatusPhase } from './CaptureStatusOverlay';
|
|
71
77
|
import { CaptureDebugOverlay } from './CaptureDebugOverlay';
|
|
72
78
|
import { CaptureMemoryPill } from './CaptureMemoryPill';
|
|
73
79
|
import { CaptureKeyframePill } from './CaptureKeyframePill';
|
|
74
80
|
import { CaptureOrientationPill } from './CaptureOrientationPill';
|
|
75
81
|
import { CaptureStitchStatsToast, useStitchStatsToast } from './CaptureStitchStatsToast';
|
|
82
|
+
import { IncrementalPanGuide } from './IncrementalPanGuide';
|
|
76
83
|
import { PanoramaBandOverlay } from './PanoramaBandOverlay';
|
|
84
|
+
import { PanoramaGuidance } from './PanoramaGuidance';
|
|
77
85
|
import { type PanoramaSettings } from './PanoramaSettings';
|
|
78
86
|
import { panoramaSettingsToNativeConfig } from './PanoramaSettingsBridge';
|
|
79
87
|
import { PanoramaSettingsModal } from './PanoramaSettingsModal';
|
|
@@ -317,6 +325,191 @@ export interface CameraProps {
|
|
|
317
325
|
*/
|
|
318
326
|
onCaptureAbandoned?: (reason: 'orientation-drift') => void;
|
|
319
327
|
|
|
328
|
+
/**
|
|
329
|
+
* v0.13.0 — flash (torch) state. Controlled-or-uncontrolled.
|
|
330
|
+
*
|
|
331
|
+
* - **Uncontrolled** (omit `flash`): `<Camera>` owns the flash
|
|
332
|
+
* state internally. Tapping the built-in flash button toggles
|
|
333
|
+
* it on/off. `onFlashChange` (if supplied) fires for telemetry.
|
|
334
|
+
* - **Controlled** (supply `flash`): the parent owns the state.
|
|
335
|
+
* The built-in button still renders and fires `onFlashChange`
|
|
336
|
+
* on press, but it's a no-op unless the parent updates `flash`
|
|
337
|
+
* in response.
|
|
338
|
+
*
|
|
339
|
+
* Both shapes coexist with the v0.13 "flash button is on by default"
|
|
340
|
+
* built-in (see the bottom-left bar slot in the JSX). Hosts that
|
|
341
|
+
* want their own flash chrome can opt out via `showFlashButton={false}`
|
|
342
|
+
* and drive the underlying torch by controlling `flash` directly.
|
|
343
|
+
*
|
|
344
|
+
* ## AR-mode behaviour
|
|
345
|
+
*
|
|
346
|
+
* In AR mode (`defaultCaptureSource="ar"` or runtime-toggled),
|
|
347
|
+
* ARKit / ARCore own the `AVCaptureDevice` and don't expose the
|
|
348
|
+
* torch through vision-camera's pipeline. The built-in flash
|
|
349
|
+
* button renders as visibly disabled (a11y label "Flash unavailable
|
|
350
|
+
* in AR mode") and `flash` is forced to `'off'` regardless of
|
|
351
|
+
* controlled/uncontrolled state. Hosts that need flash should
|
|
352
|
+
* toggle to non-AR before enabling.
|
|
353
|
+
*/
|
|
354
|
+
flash?: 'on' | 'off';
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* v0.13.0 — fires when the user taps the built-in flash button.
|
|
358
|
+
* In uncontrolled mode, the internal state has already flipped
|
|
359
|
+
* (single render delay). In controlled mode, the parent must
|
|
360
|
+
* update the `flash` prop in response or the visual toggle is
|
|
361
|
+
* a no-op. Useful in either mode for telemetry.
|
|
362
|
+
*/
|
|
363
|
+
onFlashChange?: (next: 'on' | 'off') => void;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* v0.13.0 — show the built-in flash button in the bottom-left
|
|
367
|
+
* slot. Defaults to `true`. Hosts that render their own flash
|
|
368
|
+
* chrome (and drive the underlying torch via the controlled
|
|
369
|
+
* `flash` prop) can opt out by setting this to `false`.
|
|
370
|
+
*/
|
|
371
|
+
showFlashButton?: boolean;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* v0.13.0 — show the built-in IncrementalPanGuide ("keep the
|
|
375
|
+
* arrow on the line" drift marker) while recording. Defaults
|
|
376
|
+
* to `true`. The guide is gyroscope-driven and only active
|
|
377
|
+
* during the recording phase (no idle sensor cost). Hosts that
|
|
378
|
+
* want their own pan-guide chrome can opt out via `false`.
|
|
379
|
+
*/
|
|
380
|
+
panGuide?: boolean;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* v0.13.0 — show the built-in PanoramaGuidance pan-speed pill
|
|
384
|
+
* ("Pan slowly" / "Slow down" / "Too fast") while recording.
|
|
385
|
+
* Defaults to `true`. Gyroscope-driven, only active during
|
|
386
|
+
* recording. Hosts that want their own speed chrome can opt
|
|
387
|
+
* out via `false`.
|
|
388
|
+
*/
|
|
389
|
+
panoramaGuidance?: boolean;
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* v0.13.0 — built-in CaptureHeader title. When set, `<Camera>`
|
|
393
|
+
* renders a top-of-screen header showing this title (centred)
|
|
394
|
+
* with an optional back affordance + guidance subtitle + the
|
|
395
|
+
* existing settings gear absorbed into the header's right side.
|
|
396
|
+
*
|
|
397
|
+
* When `headerTitle` is undefined the header is not rendered
|
|
398
|
+
* (matches pre-v0.13 behaviour: top of preview is bare except
|
|
399
|
+
* for the standalone settings gear gated on `showSettingsButton`).
|
|
400
|
+
*
|
|
401
|
+
* Combine with `onHeaderBack`, `headerBackLabel`, `headerGuidance`,
|
|
402
|
+
* and `headerColors` to customise the rest of the header. Hosts
|
|
403
|
+
* that need richer header chrome can omit `headerTitle` and
|
|
404
|
+
* compose their own `<CaptureHeader>` above `<Camera>`.
|
|
405
|
+
*/
|
|
406
|
+
headerTitle?: string;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* v0.13.0 — header back-button callback. When supplied (and
|
|
410
|
+
* `headerTitle` is set), the header renders a back affordance
|
|
411
|
+
* on the left. Omitted ⇒ no back button (the title stays
|
|
412
|
+
* centred).
|
|
413
|
+
*/
|
|
414
|
+
onHeaderBack?: () => void;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* v0.13.0 — header back-button label. Defaults to "‹ Back".
|
|
418
|
+
* No effect unless `headerTitle` and `onHeaderBack` are both set.
|
|
419
|
+
*/
|
|
420
|
+
headerBackLabel?: string;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* v0.13.0 — optional second-line subtitle shown below the
|
|
424
|
+
* header title. E.g. "Photograph the promotional cola end cap."
|
|
425
|
+
* Renders nothing when undefined. No effect unless `headerTitle`
|
|
426
|
+
* is set.
|
|
427
|
+
*/
|
|
428
|
+
headerGuidance?: string;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* v0.13.0 — colour overrides for the built-in header. Defaults
|
|
432
|
+
* are white-on-black to stay legible over the camera preview.
|
|
433
|
+
* No effect unless `headerTitle` is set.
|
|
434
|
+
*/
|
|
435
|
+
headerColors?: CaptureHeaderProps['colors'];
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* v0.13.0 — when provided (even as `[]`), `<Camera>` renders a
|
|
439
|
+
* built-in `CaptureThumbnailStrip` above the bottom controls
|
|
440
|
+
* showing the host's capture history. Each item is a plain
|
|
441
|
+
* `{ id, uri, width?, height? }` object; the strip handles
|
|
442
|
+
* aspect-ratio rendering, tap-to-preview, and the count line.
|
|
443
|
+
*
|
|
444
|
+
* Omit (`undefined`) to skip the strip entirely. Hosts using
|
|
445
|
+
* the strip independently (e.g. on a non-camera screen) can keep
|
|
446
|
+
* importing `CaptureThumbnailStrip` directly from the library —
|
|
447
|
+
* the prop here is the convenience wiring for in-`<Camera>` use.
|
|
448
|
+
*
|
|
449
|
+
* Captures emitted by `<Camera>`'s `onCapture` are NOT added to
|
|
450
|
+
* this array automatically — the host owns the canonical list
|
|
451
|
+
* (typically persisted to its own DB) and updates the prop in
|
|
452
|
+
* response. This matches the SDK's "Camera owns runtime state,
|
|
453
|
+
* host persists" pattern.
|
|
454
|
+
*/
|
|
455
|
+
thumbnails?: CaptureThumbnailItem[];
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* v0.13.0 — minimum-photos hint for the count line. Renders
|
|
459
|
+
* "n / minPhotos min" with the success colour when reached,
|
|
460
|
+
* warning colour otherwise.
|
|
461
|
+
*/
|
|
462
|
+
thumbnailsMin?: number;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* v0.13.0 — maximum-photos hint for the count line. Renders
|
|
466
|
+
* "· maxPhotos max" suffix. No enforcement — the host decides
|
|
467
|
+
* what to do at the cap.
|
|
468
|
+
*/
|
|
469
|
+
thumbnailsMax?: number;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* v0.13.0 — tap handler for thumbnails. When set, replaces the
|
|
473
|
+
* strip's built-in tap-to-preview modal; the host shows its own
|
|
474
|
+
* preview UI (e.g. with delete / recapture buttons gated on
|
|
475
|
+
* sync state). Omit to use the built-in preview.
|
|
476
|
+
*/
|
|
477
|
+
onThumbnailPress?: (item: CaptureThumbnailItem) => void;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* v0.13.0 — when set, `<Camera>` renders a built-in `CapturePreview`
|
|
481
|
+
* modal as `visible`. Use this for post-stitch confirmation:
|
|
482
|
+
* after `onCapture` emits, the host stores the result and sets
|
|
483
|
+
* `capturePreview` to the new image, with `capturePreviewActions`
|
|
484
|
+
* = `[Discard, Save]` (or similar). Setting `undefined` hides
|
|
485
|
+
* the modal.
|
|
486
|
+
*
|
|
487
|
+
* Hosts using the modal for thumbnail tap-to-preview can leave
|
|
488
|
+
* this undefined and let the built-in strip's preview handle
|
|
489
|
+
* that case.
|
|
490
|
+
*/
|
|
491
|
+
capturePreview?: {
|
|
492
|
+
imageUri: string;
|
|
493
|
+
imageWidth?: number;
|
|
494
|
+
imageHeight?: number;
|
|
495
|
+
title?: string;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* v0.13.0 — action buttons rendered along the bottom of the
|
|
500
|
+
* `CapturePreview` modal. Empty array (or undefined) renders
|
|
501
|
+
* no buttons, only the close affordance.
|
|
502
|
+
*/
|
|
503
|
+
capturePreviewActions?: CapturePreviewAction[];
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* v0.13.0 — fires when the user dismisses the `capturePreview`
|
|
507
|
+
* modal (tap close, backdrop tap, hardware back on Android).
|
|
508
|
+
* The host is expected to clear the `capturePreview` prop in
|
|
509
|
+
* response.
|
|
510
|
+
*/
|
|
511
|
+
onCapturePreviewClose?: () => void;
|
|
512
|
+
|
|
320
513
|
/**
|
|
321
514
|
* Optional host-supplied vision-camera frame processor.
|
|
322
515
|
*
|
|
@@ -681,6 +874,23 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
681
874
|
onFramesDropped,
|
|
682
875
|
onError,
|
|
683
876
|
onCaptureAbandoned,
|
|
877
|
+
flash: controlledFlash,
|
|
878
|
+
onFlashChange,
|
|
879
|
+
showFlashButton = true,
|
|
880
|
+
panGuide = true,
|
|
881
|
+
panoramaGuidance = true,
|
|
882
|
+
headerTitle,
|
|
883
|
+
onHeaderBack,
|
|
884
|
+
headerBackLabel,
|
|
885
|
+
headerGuidance,
|
|
886
|
+
headerColors,
|
|
887
|
+
thumbnails,
|
|
888
|
+
thumbnailsMin,
|
|
889
|
+
thumbnailsMax,
|
|
890
|
+
onThumbnailPress,
|
|
891
|
+
capturePreview,
|
|
892
|
+
capturePreviewActions,
|
|
893
|
+
onCapturePreviewClose,
|
|
684
894
|
frameProcessor: hostFrameProcessor,
|
|
685
895
|
engine = 'batch-keyframe',
|
|
686
896
|
} = props;
|
|
@@ -700,6 +910,12 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
700
910
|
defaultCaptureSource === 'ar',
|
|
701
911
|
);
|
|
702
912
|
const [lens, setLens] = useState<CameraLens>(defaultLens);
|
|
913
|
+
// v0.13.0 — flash state. Controlled by `controlledFlash` when the
|
|
914
|
+
// host supplies the `flash` prop; otherwise owned internally and
|
|
915
|
+
// toggled by the built-in flash button. `effectiveFlash` below
|
|
916
|
+
// also forces 'off' in AR mode (ARKit / ARCore own the device's
|
|
917
|
+
// torch and don't surface it through vision-camera's pipeline).
|
|
918
|
+
const [internalFlash, setInternalFlash] = useState<'on' | 'off'>('off');
|
|
703
919
|
const [settings, setSettings] = useState<PanoramaSettings>(() =>
|
|
704
920
|
buildPanoramaInitialSettings(
|
|
705
921
|
extractPanoramaOverrides(props),
|
|
@@ -1349,6 +1565,22 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1349
1565
|
setArPreference((prev) => !prev);
|
|
1350
1566
|
}, []);
|
|
1351
1567
|
|
|
1568
|
+
// ── v0.13.0 — Flash control ─────────────────────────────────────
|
|
1569
|
+
//
|
|
1570
|
+
// `flashRequested` is what the host / built-in button asks for.
|
|
1571
|
+
// `effectiveFlash` is what we actually drive into vision-camera —
|
|
1572
|
+
// AR mode forces 'off' because ARKit / ARCore own AVCaptureDevice
|
|
1573
|
+
// and the torch isn't exposed. This way the button's visual state
|
|
1574
|
+
// (a11y, styling) tracks `flashRequested` while the underlying
|
|
1575
|
+
// camera always sees the correct value.
|
|
1576
|
+
const flashRequested: 'on' | 'off' = controlledFlash ?? internalFlash;
|
|
1577
|
+
const effectiveFlash: 'on' | 'off' = isAR ? 'off' : flashRequested;
|
|
1578
|
+
const toggleFlash = useCallback(() => {
|
|
1579
|
+
const next: 'on' | 'off' = flashRequested === 'on' ? 'off' : 'on';
|
|
1580
|
+
if (controlledFlash == null) setInternalFlash(next);
|
|
1581
|
+
onFlashChange?.(next);
|
|
1582
|
+
}, [flashRequested, controlledFlash, onFlashChange]);
|
|
1583
|
+
|
|
1352
1584
|
// ── JSX ─────────────────────────────────────────────────────────
|
|
1353
1585
|
|
|
1354
1586
|
return (
|
|
@@ -1380,7 +1612,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1380
1612
|
// works either way. Pattern matches AuditCaptureScreen.tsx
|
|
1381
1613
|
// which has run on `video` (true) for months without issue.
|
|
1382
1614
|
video
|
|
1383
|
-
flash=
|
|
1615
|
+
flash={effectiveFlash}
|
|
1384
1616
|
style={StyleSheet.absoluteFill}
|
|
1385
1617
|
// F8 (FrameProcessor port) — host-supplied worklet runs on
|
|
1386
1618
|
// the camera producer thread for every frame. Only wired
|
|
@@ -1416,6 +1648,20 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1416
1648
|
recordingStartedAt={recordingStartedAt ?? undefined}
|
|
1417
1649
|
/>
|
|
1418
1650
|
|
|
1651
|
+
{/* v0.13.0 — built-in pan guidance overlays. Both sit on top
|
|
1652
|
+
of the camera preview but under the controls. Each is
|
|
1653
|
+
gyroscope-driven and only subscribes while `active` is
|
|
1654
|
+
true — flipping `active` false on capture-end tears the
|
|
1655
|
+
subscription down so the sensor isn't running idle. Hosts
|
|
1656
|
+
can opt out per overlay via the `panGuide` / `panoramaGuidance`
|
|
1657
|
+
boolean props (both default true). */}
|
|
1658
|
+
{panGuide && (
|
|
1659
|
+
<IncrementalPanGuide active={statusPhase === 'recording'} />
|
|
1660
|
+
)}
|
|
1661
|
+
{panoramaGuidance && (
|
|
1662
|
+
<PanoramaGuidance active={statusPhase === 'recording'} />
|
|
1663
|
+
)}
|
|
1664
|
+
|
|
1419
1665
|
{/*
|
|
1420
1666
|
2026-05-22 (audit F9 + F3) — debug UI suite, all gated on
|
|
1421
1667
|
settings.debug. Mounts in <Camera> automatically; Layer-2
|
|
@@ -1459,12 +1705,52 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1459
1705
|
topInset={insets.top}
|
|
1460
1706
|
/>
|
|
1461
1707
|
|
|
1462
|
-
{/*
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1708
|
+
{/* v0.13.0 — built-in CaptureHeader, gated on `headerTitle`.
|
|
1709
|
+
When the header is mounted, it absorbs the settings gear
|
|
1710
|
+
on its right side (avoids stacking with the standalone
|
|
1711
|
+
gear). Hosts that DON'T set `headerTitle` get the legacy
|
|
1712
|
+
standalone gear, still gated on `showSettingsButton`. */}
|
|
1713
|
+
{headerTitle != null ? (
|
|
1714
|
+
<View style={styles.headerWrap} pointerEvents="box-none">
|
|
1715
|
+
<CaptureHeader
|
|
1716
|
+
title={headerTitle}
|
|
1717
|
+
onBack={onHeaderBack}
|
|
1718
|
+
backLabel={headerBackLabel}
|
|
1719
|
+
guidance={headerGuidance}
|
|
1720
|
+
colors={headerColors}
|
|
1721
|
+
topInset={insets.top}
|
|
1722
|
+
onSettingsPress={
|
|
1723
|
+
showSettingsButton
|
|
1724
|
+
? () => setSettingsModalVisible(true)
|
|
1725
|
+
: undefined
|
|
1726
|
+
}
|
|
1727
|
+
/>
|
|
1728
|
+
</View>
|
|
1729
|
+
) : (
|
|
1730
|
+
showSettingsButton && (
|
|
1731
|
+
<SettingsButton
|
|
1732
|
+
topInset={insets.top}
|
|
1733
|
+
onPress={() => setSettingsModalVisible(true)}
|
|
1734
|
+
/>
|
|
1735
|
+
)
|
|
1736
|
+
)}
|
|
1737
|
+
|
|
1738
|
+
{/* v0.13.0 — built-in capture-history thumbnail strip. Renders
|
|
1739
|
+
when the host supplies a `thumbnails` array (even empty),
|
|
1740
|
+
hidden during recording so it doesn't overlap the band
|
|
1741
|
+
overlay. Sits above the bottom controls in JS-bottom
|
|
1742
|
+
coordinates; landscape/non-locked layouts get the strip in
|
|
1743
|
+
the same place (no orientation-aware repositioning for now —
|
|
1744
|
+
the strip is intrinsically horizontal). */}
|
|
1745
|
+
{thumbnails != null && statusPhase !== 'recording' && (
|
|
1746
|
+
<View style={styles.thumbnailStripWrap} pointerEvents="box-none">
|
|
1747
|
+
<CaptureThumbnailStrip
|
|
1748
|
+
items={thumbnails}
|
|
1749
|
+
minPhotos={thumbnailsMin}
|
|
1750
|
+
maxPhotos={thumbnailsMax}
|
|
1751
|
+
onItemPress={onThumbnailPress}
|
|
1752
|
+
/>
|
|
1753
|
+
</View>
|
|
1468
1754
|
)}
|
|
1469
1755
|
|
|
1470
1756
|
{/*
|
|
@@ -1505,7 +1791,25 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1505
1791
|
vertical column when on left/right (slots stack along
|
|
1506
1792
|
the narrow strip). Touch targets stay axis-aligned. */}
|
|
1507
1793
|
<View style={bottomBarStyleForEdge(homeIndicatorEdge(jsLandscape, deviceOrientation))}>
|
|
1508
|
-
<View style={styles.bottomBarLeft}
|
|
1794
|
+
<View style={styles.bottomBarLeft}>
|
|
1795
|
+
{showFlashButton && (
|
|
1796
|
+
<Pressable
|
|
1797
|
+
onPress={isAR ? undefined : toggleFlash}
|
|
1798
|
+
accessibilityRole="button"
|
|
1799
|
+
accessibilityLabel={isAR ? 'Flash unavailable in AR mode' : `Flash ${flashRequested === 'on' ? 'on' : 'off'}`}
|
|
1800
|
+
accessibilityState={{ selected: flashRequested === 'on', disabled: isAR }}
|
|
1801
|
+
disabled={isAR}
|
|
1802
|
+
hitSlop={8}
|
|
1803
|
+
style={[
|
|
1804
|
+
styles.flashButton,
|
|
1805
|
+
flashRequested === 'on' && !isAR && styles.flashButtonActive,
|
|
1806
|
+
isAR && styles.flashButtonDisabled,
|
|
1807
|
+
]}
|
|
1808
|
+
>
|
|
1809
|
+
<Text style={styles.flashIcon}>⚡</Text>
|
|
1810
|
+
</Pressable>
|
|
1811
|
+
)}
|
|
1812
|
+
</View>
|
|
1509
1813
|
<View style={styles.bottomBarCenter}>
|
|
1510
1814
|
<LensChip
|
|
1511
1815
|
lens={lens}
|
|
@@ -1550,6 +1854,21 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1550
1854
|
currentOrientation={drift.currentOrientation}
|
|
1551
1855
|
onAcknowledge={() => setDriftModalDismissed(true)}
|
|
1552
1856
|
/>
|
|
1857
|
+
|
|
1858
|
+
{/* v0.13.0 — built-in post-stitch / tap-to-preview modal.
|
|
1859
|
+
Visible when the host supplies `capturePreview`. When
|
|
1860
|
+
undefined the modal stays hidden (visible=false) so it
|
|
1861
|
+
doesn't intercept touches. Host is expected to clear
|
|
1862
|
+
`capturePreview` via `onCapturePreviewClose` on dismiss. */}
|
|
1863
|
+
<CapturePreview
|
|
1864
|
+
visible={capturePreview != null}
|
|
1865
|
+
imageUri={capturePreview?.imageUri ?? ''}
|
|
1866
|
+
imageWidth={capturePreview?.imageWidth}
|
|
1867
|
+
imageHeight={capturePreview?.imageHeight}
|
|
1868
|
+
title={capturePreview?.title}
|
|
1869
|
+
actions={capturePreviewActions}
|
|
1870
|
+
onClose={onCapturePreviewClose ?? noop}
|
|
1871
|
+
/>
|
|
1553
1872
|
</View>
|
|
1554
1873
|
);
|
|
1555
1874
|
}
|
|
@@ -1731,6 +2050,8 @@ const styles = StyleSheet.create({
|
|
|
1731
2050
|
},
|
|
1732
2051
|
bottomBarLeft: {
|
|
1733
2052
|
flex: 1,
|
|
2053
|
+
alignItems: 'flex-start',
|
|
2054
|
+
justifyContent: 'flex-end',
|
|
1734
2055
|
},
|
|
1735
2056
|
bottomBarCenter: {
|
|
1736
2057
|
flex: 1,
|
|
@@ -1744,4 +2065,34 @@ const styles = StyleSheet.create({
|
|
|
1744
2065
|
shutterWrap: {
|
|
1745
2066
|
marginTop: 12,
|
|
1746
2067
|
},
|
|
2068
|
+
headerWrap: {
|
|
2069
|
+
position: 'absolute',
|
|
2070
|
+
top: 0,
|
|
2071
|
+
left: 0,
|
|
2072
|
+
right: 0,
|
|
2073
|
+
},
|
|
2074
|
+
thumbnailStripWrap: {
|
|
2075
|
+
position: 'absolute',
|
|
2076
|
+
left: 0,
|
|
2077
|
+
right: 0,
|
|
2078
|
+
bottom: 160,
|
|
2079
|
+
},
|
|
2080
|
+
flashButton: {
|
|
2081
|
+
width: 44,
|
|
2082
|
+
height: 44,
|
|
2083
|
+
borderRadius: 22,
|
|
2084
|
+
alignItems: 'center',
|
|
2085
|
+
justifyContent: 'center',
|
|
2086
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
2087
|
+
},
|
|
2088
|
+
flashButtonActive: {
|
|
2089
|
+
backgroundColor: '#ffd34d',
|
|
2090
|
+
},
|
|
2091
|
+
flashButtonDisabled: {
|
|
2092
|
+
opacity: 0.35,
|
|
2093
|
+
},
|
|
2094
|
+
flashIcon: {
|
|
2095
|
+
fontSize: 20,
|
|
2096
|
+
color: '#ffffff',
|
|
2097
|
+
},
|
|
1747
2098
|
});
|