react-native-image-stitcher 0.3.0 → 0.4.1
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 +220 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +137 -124
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +212 -119
- package/dist/camera/Camera.js +70 -58
- package/dist/camera/PanoramaSettings.d.ts +478 -0
- package/dist/camera/PanoramaSettings.js +120 -0
- package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
- package/dist/camera/PanoramaSettingsBridge.js +208 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +50 -299
- package/dist/camera/PanoramaSettingsModal.js +189 -354
- package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
- package/dist/camera/buildPanoramaInitialSettings.js +97 -0
- package/dist/camera/lowMemDevice.d.ts +24 -0
- package/dist/camera/lowMemDevice.js +69 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +23 -2
- package/package.json +6 -2
- package/src/camera/Camera.tsx +79 -71
- package/src/camera/PanoramaSettings.ts +605 -0
- package/src/camera/PanoramaSettingsBridge.ts +238 -0
- package/src/camera/PanoramaSettingsModal.tsx +296 -989
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
- package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
- package/src/camera/buildPanoramaInitialSettings.ts +139 -0
- package/src/camera/lowMemDevice.ts +71 -0
- package/src/index.ts +42 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildPanoramaInitialSettings — pure helper that materialises the
|
|
3
|
+
* initial `PanoramaSettings` snapshot from <Camera>'s `default*` props
|
|
4
|
+
* and a device-capability hint.
|
|
5
|
+
*
|
|
6
|
+
* Why a separate file?
|
|
7
|
+
* ────────────────────
|
|
8
|
+
*
|
|
9
|
+
* The settings tree lives in `PanoramaSettings.ts`; <Camera> consumes
|
|
10
|
+
* it and writes it into React state. The translation FROM the prop
|
|
11
|
+
* surface (flat names like `defaultStitchMode`) INTO the hierarchical
|
|
12
|
+
* settings tree is the part that:
|
|
13
|
+
*
|
|
14
|
+
* • is non-trivial enough to deserve direct unit-test coverage
|
|
15
|
+
* (covers the prop→sub-tree path mapping, which is easy to drift),
|
|
16
|
+
* • is pure TS — no React, no React Native — so the test runs in
|
|
17
|
+
* jest's `node` environment without needing the `react-native`
|
|
18
|
+
* preset (the rest of <Camera> is unmockable in pure TS).
|
|
19
|
+
*
|
|
20
|
+
* Living alongside `Camera.tsx` (vs. burying it as a private function
|
|
21
|
+
* inside) is the only way to get those two properties without taking
|
|
22
|
+
* on full React-Native jest setup just for this one helper.
|
|
23
|
+
*
|
|
24
|
+
* The exported `PanoramaPropOverrides` type is the prop-fragment
|
|
25
|
+
* <Camera> uses; `CameraProps` extends it. Keeping it explicit here
|
|
26
|
+
* means future Camera prop additions don't accidentally widen the
|
|
27
|
+
* settings-translation surface — every consumer of the helper sees
|
|
28
|
+
* exactly the prop fields that drive the settings tree.
|
|
29
|
+
*/
|
|
30
|
+
import { type PanoramaSettings } from './PanoramaSettings';
|
|
31
|
+
/**
|
|
32
|
+
* Subset of <Camera>'s props that map onto fields of the initial
|
|
33
|
+
* `PanoramaSettings` snapshot. Anything outside this interface
|
|
34
|
+
* (e.g. `defaultLens`, `enablePhotoMode`, callbacks) is irrelevant
|
|
35
|
+
* to the settings shape and stays in `CameraProps` only.
|
|
36
|
+
*
|
|
37
|
+
* Forward-looking `default*ResolMP` props are documented here but
|
|
38
|
+
* intentionally not translated yet — the new `PanoramaSettings` tree
|
|
39
|
+
* has no home for them (the v0.3 audit found cv::Stitcher's resol
|
|
40
|
+
* knobs aren't reached by the current native bridges).
|
|
41
|
+
*/
|
|
42
|
+
export interface PanoramaPropOverrides {
|
|
43
|
+
defaultCaptureSource?: 'ar' | 'non-ar';
|
|
44
|
+
defaultStitchMode?: 'auto' | 'panorama' | 'scans';
|
|
45
|
+
defaultBlender?: 'multiband' | 'feather';
|
|
46
|
+
defaultSeamFinder?: 'graphcut' | 'skip';
|
|
47
|
+
defaultWarper?: 'plane' | 'cylindrical' | 'spherical';
|
|
48
|
+
defaultFlowNoveltyPercentile?: number;
|
|
49
|
+
defaultFlowEvalEveryNFrames?: number;
|
|
50
|
+
defaultFlowMaxTranslationCm?: number;
|
|
51
|
+
defaultKeyframeMaxCount?: number;
|
|
52
|
+
defaultKeyframeOverlapThreshold?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether this device is low-memory enough to benefit from the
|
|
56
|
+
* feather+skip blender/seam fallback (vs. the heavier multiband+
|
|
57
|
+
* graphcut default). <Camera> derives this from
|
|
58
|
+
* `NativeModules.BatchStitcher.physicalMemoryBytes` at module load
|
|
59
|
+
* (RN-only — see `getIsLowMemDevice` in Camera.tsx); tests pass
|
|
60
|
+
* `false` explicitly to keep the prop-translation path the unit of
|
|
61
|
+
* the unit test.
|
|
62
|
+
*
|
|
63
|
+
* Why a parameter and not a constant import?
|
|
64
|
+
* The pre-v0.4 `DEFAULT_PANORAMA_SETTINGS` was a `let` mutated at
|
|
65
|
+
* module load — side-effect-heavy, untestable. v0.4 keeps the
|
|
66
|
+
* defaults static + side-effect-free; the device adaptation lives
|
|
67
|
+
* exactly where it needs to (Camera's mount-time `useState`).
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildPanoramaInitialSettings(overrides: PanoramaPropOverrides, isLowMemDevice: boolean): PanoramaSettings;
|
|
70
|
+
//# sourceMappingURL=buildPanoramaInitialSettings.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* buildPanoramaInitialSettings — pure helper that materialises the
|
|
5
|
+
* initial `PanoramaSettings` snapshot from <Camera>'s `default*` props
|
|
6
|
+
* and a device-capability hint.
|
|
7
|
+
*
|
|
8
|
+
* Why a separate file?
|
|
9
|
+
* ────────────────────
|
|
10
|
+
*
|
|
11
|
+
* The settings tree lives in `PanoramaSettings.ts`; <Camera> consumes
|
|
12
|
+
* it and writes it into React state. The translation FROM the prop
|
|
13
|
+
* surface (flat names like `defaultStitchMode`) INTO the hierarchical
|
|
14
|
+
* settings tree is the part that:
|
|
15
|
+
*
|
|
16
|
+
* • is non-trivial enough to deserve direct unit-test coverage
|
|
17
|
+
* (covers the prop→sub-tree path mapping, which is easy to drift),
|
|
18
|
+
* • is pure TS — no React, no React Native — so the test runs in
|
|
19
|
+
* jest's `node` environment without needing the `react-native`
|
|
20
|
+
* preset (the rest of <Camera> is unmockable in pure TS).
|
|
21
|
+
*
|
|
22
|
+
* Living alongside `Camera.tsx` (vs. burying it as a private function
|
|
23
|
+
* inside) is the only way to get those two properties without taking
|
|
24
|
+
* on full React-Native jest setup just for this one helper.
|
|
25
|
+
*
|
|
26
|
+
* The exported `PanoramaPropOverrides` type is the prop-fragment
|
|
27
|
+
* <Camera> uses; `CameraProps` extends it. Keeping it explicit here
|
|
28
|
+
* means future Camera prop additions don't accidentally widen the
|
|
29
|
+
* settings-translation surface — every consumer of the helper sees
|
|
30
|
+
* exactly the prop fields that drive the settings tree.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.buildPanoramaInitialSettings = buildPanoramaInitialSettings;
|
|
34
|
+
const PanoramaSettings_1 = require("./PanoramaSettings");
|
|
35
|
+
/**
|
|
36
|
+
* Whether this device is low-memory enough to benefit from the
|
|
37
|
+
* feather+skip blender/seam fallback (vs. the heavier multiband+
|
|
38
|
+
* graphcut default). <Camera> derives this from
|
|
39
|
+
* `NativeModules.BatchStitcher.physicalMemoryBytes` at module load
|
|
40
|
+
* (RN-only — see `getIsLowMemDevice` in Camera.tsx); tests pass
|
|
41
|
+
* `false` explicitly to keep the prop-translation path the unit of
|
|
42
|
+
* the unit test.
|
|
43
|
+
*
|
|
44
|
+
* Why a parameter and not a constant import?
|
|
45
|
+
* The pre-v0.4 `DEFAULT_PANORAMA_SETTINGS` was a `let` mutated at
|
|
46
|
+
* module load — side-effect-heavy, untestable. v0.4 keeps the
|
|
47
|
+
* defaults static + side-effect-free; the device adaptation lives
|
|
48
|
+
* exactly where it needs to (Camera's mount-time `useState`).
|
|
49
|
+
*/
|
|
50
|
+
function buildPanoramaInitialSettings(overrides, isLowMemDevice) {
|
|
51
|
+
// Start from the static, side-effect-free defaults.
|
|
52
|
+
const base = PanoramaSettings_1.DEFAULT_PANORAMA_SETTINGS;
|
|
53
|
+
// Apply the low-memory device adaptation:
|
|
54
|
+
// - feather blender (streams warped frames, no peak-memory spike)
|
|
55
|
+
// - skip seam finder (no graphcut working set)
|
|
56
|
+
// Replaces the v0.3 module-load-time mutation; same semantics.
|
|
57
|
+
const stitcherDefaults = isLowMemDevice
|
|
58
|
+
? {
|
|
59
|
+
...base.stitcher,
|
|
60
|
+
blenderType: 'feather',
|
|
61
|
+
seamFinderType: 'skip',
|
|
62
|
+
}
|
|
63
|
+
: base.stitcher;
|
|
64
|
+
// Use the standalone DEFAULT_FLOW_GATE_SETTINGS constant rather
|
|
65
|
+
// than `base.frameSelection.flow!` — the non-null assertion would
|
|
66
|
+
// crash silently if a future refactor un-defines the default's
|
|
67
|
+
// flow sub-tree, but the constant lives at the same level as the
|
|
68
|
+
// type and is type-checked. See F10 Phase 2 review (NIT-4).
|
|
69
|
+
const flowDefaults = PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS;
|
|
70
|
+
return {
|
|
71
|
+
captureSource: overrides.defaultCaptureSource ?? base.captureSource,
|
|
72
|
+
debug: base.debug,
|
|
73
|
+
stitcher: {
|
|
74
|
+
...stitcherDefaults,
|
|
75
|
+
stitchMode: overrides.defaultStitchMode ?? stitcherDefaults.stitchMode,
|
|
76
|
+
warperType: overrides.defaultWarper ?? stitcherDefaults.warperType,
|
|
77
|
+
blenderType: overrides.defaultBlender ?? stitcherDefaults.blenderType,
|
|
78
|
+
seamFinderType: overrides.defaultSeamFinder ?? stitcherDefaults.seamFinderType,
|
|
79
|
+
},
|
|
80
|
+
frameSelection: {
|
|
81
|
+
...base.frameSelection,
|
|
82
|
+
maxKeyframes: overrides.defaultKeyframeMaxCount ?? base.frameSelection.maxKeyframes,
|
|
83
|
+
overlapThreshold: overrides.defaultKeyframeOverlapThreshold
|
|
84
|
+
?? base.frameSelection.overlapThreshold,
|
|
85
|
+
flow: {
|
|
86
|
+
...flowDefaults,
|
|
87
|
+
noveltyPercentile: overrides.defaultFlowNoveltyPercentile
|
|
88
|
+
?? flowDefaults.noveltyPercentile,
|
|
89
|
+
evalEveryNFrames: overrides.defaultFlowEvalEveryNFrames
|
|
90
|
+
?? flowDefaults.evalEveryNFrames,
|
|
91
|
+
maxTranslationCm: overrides.defaultFlowMaxTranslationCm
|
|
92
|
+
?? flowDefaults.maxTranslationCm,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=buildPanoramaInitialSettings.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** 2 GB in bytes — the cutoff below which `<Camera>` falls back
|
|
2
|
+
* to the feather+skip blender/seam combo for safer peak memory. */
|
|
3
|
+
export declare const LOW_MEM_THRESHOLD_BYTES: number;
|
|
4
|
+
/**
|
|
5
|
+
* Pure classifier. Returns `true` when `bytes` is a positive
|
|
6
|
+
* number strictly below the threshold. Zero / NaN / undefined-shaped
|
|
7
|
+
* inputs return `false` — the safe choice when the native bridge
|
|
8
|
+
* hasn't surfaced the value (assume the device has enough memory
|
|
9
|
+
* for the higher-quality combo; the operator can still flip
|
|
10
|
+
* blender / seamFinder in the modal).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isBelowMemThreshold(bytes: number): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Read the device's physical memory from the native bridge.
|
|
15
|
+
* Returns 0 when the bridge isn't loaded or the constant is missing
|
|
16
|
+
* — caller should treat 0 as "unknown".
|
|
17
|
+
*/
|
|
18
|
+
export declare function getPhysicalMemoryBytes(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Composed `getPhysicalMemoryBytes()` + `isBelowMemThreshold()`.
|
|
21
|
+
* Convenience for the common consumer pattern.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isLowMemDevice(): boolean;
|
|
24
|
+
//# sourceMappingURL=lowMemDevice.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOW_MEM_THRESHOLD_BYTES = void 0;
|
|
4
|
+
exports.isBelowMemThreshold = isBelowMemThreshold;
|
|
5
|
+
exports.getPhysicalMemoryBytes = getPhysicalMemoryBytes;
|
|
6
|
+
exports.isLowMemDevice = isLowMemDevice;
|
|
7
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
8
|
+
/**
|
|
9
|
+
* lowMemDevice — shared helpers around the iOS BatchStitcher
|
|
10
|
+
* `physicalMemoryBytes` constant.
|
|
11
|
+
*
|
|
12
|
+
* Two consumers (Camera.tsx's `useState` initialiser + the modal's
|
|
13
|
+
* device-mem debug line) had near-identical implementations of the
|
|
14
|
+
* same "read physical memory from NativeModules, classify as
|
|
15
|
+
* low-mem" logic. The F10 Phase 2 review (N2) flagged this as a
|
|
16
|
+
* drift hazard — exactly the kind of subtle duplication that audit
|
|
17
|
+
* fix F1 chased on the native side.
|
|
18
|
+
*
|
|
19
|
+
* Layered for testability:
|
|
20
|
+
*
|
|
21
|
+
* - `isBelowMemThreshold(bytes)` is pure (in: number, out:
|
|
22
|
+
* boolean) — unit-testable.
|
|
23
|
+
* - `getPhysicalMemoryBytes()` reads the RN bridge module — must
|
|
24
|
+
* run on a real device.
|
|
25
|
+
* - `isLowMemDevice()` composes the two for the common case.
|
|
26
|
+
*
|
|
27
|
+
* The 2 GB threshold corresponds to iPhone X / 8 Plus / iPhone 6s
|
|
28
|
+
* era devices; below that, multiband blend + graphcut seam-finder
|
|
29
|
+
* peak memory risks the jetsam threshold mid-stitch. Static value
|
|
30
|
+
* (no runtime config); revisit if the SDK ever targets a wider
|
|
31
|
+
* device range.
|
|
32
|
+
*/
|
|
33
|
+
const react_native_1 = require("react-native");
|
|
34
|
+
/** 2 GB in bytes — the cutoff below which `<Camera>` falls back
|
|
35
|
+
* to the feather+skip blender/seam combo for safer peak memory. */
|
|
36
|
+
exports.LOW_MEM_THRESHOLD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* Pure classifier. Returns `true` when `bytes` is a positive
|
|
39
|
+
* number strictly below the threshold. Zero / NaN / undefined-shaped
|
|
40
|
+
* inputs return `false` — the safe choice when the native bridge
|
|
41
|
+
* hasn't surfaced the value (assume the device has enough memory
|
|
42
|
+
* for the higher-quality combo; the operator can still flip
|
|
43
|
+
* blender / seamFinder in the modal).
|
|
44
|
+
*/
|
|
45
|
+
function isBelowMemThreshold(bytes) {
|
|
46
|
+
return Number.isFinite(bytes)
|
|
47
|
+
&& bytes > 0
|
|
48
|
+
&& bytes < exports.LOW_MEM_THRESHOLD_BYTES;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read the device's physical memory from the native bridge.
|
|
52
|
+
* Returns 0 when the bridge isn't loaded or the constant is missing
|
|
53
|
+
* — caller should treat 0 as "unknown".
|
|
54
|
+
*/
|
|
55
|
+
function getPhysicalMemoryBytes() {
|
|
56
|
+
const m = react_native_1.NativeModules.BatchStitcher;
|
|
57
|
+
const bytes = m && typeof m === 'object'
|
|
58
|
+
? m.physicalMemoryBytes
|
|
59
|
+
: undefined;
|
|
60
|
+
return typeof bytes === 'number' ? bytes : 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Composed `getPhysicalMemoryBytes()` + `isBelowMemThreshold()`.
|
|
64
|
+
* Convenience for the common consumer pattern.
|
|
65
|
+
*/
|
|
66
|
+
function isLowMemDevice() {
|
|
67
|
+
return isBelowMemThreshold(getPhysicalMemoryBytes());
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=lowMemDevice.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -50,8 +50,12 @@ export type { CaptureThumbnailItem } from './camera/CaptureThumbnailStrip';
|
|
|
50
50
|
export { IncrementalPanGuide } from './camera/IncrementalPanGuide';
|
|
51
51
|
export { PanoramaBandOverlay } from './camera/PanoramaBandOverlay';
|
|
52
52
|
export { PanoramaGuidance } from './camera/PanoramaGuidance';
|
|
53
|
-
export { PanoramaSettingsModal
|
|
54
|
-
export type {
|
|
53
|
+
export { PanoramaSettingsModal } from './camera/PanoramaSettingsModal';
|
|
54
|
+
export type { PanoramaSettingsModalProps } from './camera/PanoramaSettingsModal';
|
|
55
|
+
export { DEFAULT_PANORAMA_SETTINGS, DEFAULT_FLOW_GATE_SETTINGS, DEFAULT_SLITSCAN_SETTINGS, DEFAULT_HYBRID_SETTINGS, } from './camera/PanoramaSettings';
|
|
56
|
+
export type { CaptureBaseSettings, PanoramaSettings, BatchStitcherSettings, FrameSelectionSettings, FlowGateSettings, SlitscanSettings, SlitscanPaintingSettings, SlitscanRegistrationSettings, SlitscanAdvancedSettings, Ncc1dSettings, Ncc2dSettings, PlaneProjectionSettings, HybridSettings, } from './camera/PanoramaSettings';
|
|
57
|
+
export { panoramaSettingsToNativeConfig, slitscanSettingsToNativeConfig, hybridSettingsToNativeConfig, } from './camera/PanoramaSettingsBridge';
|
|
58
|
+
export type { NativeConfigDict } from './camera/PanoramaSettingsBridge';
|
|
55
59
|
export { ViewportCropOverlay } from './camera/ViewportCropOverlay';
|
|
56
60
|
export { useCapture } from './camera/useCapture';
|
|
57
61
|
export type { TakePhotoCallOptions } from './camera/useCapture';
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* adds RetaiLens-specific features on top.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.stitchVideo = exports.useIncrementalJSDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
25
|
+
exports.stitchVideo = exports.useIncrementalJSDriver = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.useDeviceOrientation = exports.useVideoCapture = exports.useCapture = exports.ViewportCropOverlay = exports.hybridSettingsToNativeConfig = exports.slitscanSettingsToNativeConfig = exports.panoramaSettingsToNativeConfig = exports.DEFAULT_HYBRID_SETTINGS = exports.DEFAULT_SLITSCAN_SETTINGS = exports.DEFAULT_FLOW_GATE_SETTINGS = exports.DEFAULT_PANORAMA_SETTINGS = exports.PanoramaSettingsModal = exports.PanoramaGuidance = exports.PanoramaBandOverlay = exports.IncrementalPanGuide = exports.CaptureThumbnailStrip = exports.useStitchStatsToast = exports.CaptureStitchStatsToast = exports.CaptureOrientationPill = exports.CaptureKeyframePill = exports.CaptureMemoryPill = exports.CaptureDebugOverlay = exports.CaptureStatusOverlay = exports.CapturePreview = exports.CaptureControlsBar = exports.CaptureHeader = exports.CameraView = exports.ARCameraView = exports.useIMUTranslationGate = exports.ARTrackingState = exports.useARSession = exports.CameraError = exports.Camera = void 0;
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────
|
|
27
27
|
// Layer 1 — the high-level <Camera> component
|
|
28
28
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -91,9 +91,30 @@ var PanoramaBandOverlay_1 = require("./camera/PanoramaBandOverlay");
|
|
|
91
91
|
Object.defineProperty(exports, "PanoramaBandOverlay", { enumerable: true, get: function () { return PanoramaBandOverlay_1.PanoramaBandOverlay; } });
|
|
92
92
|
var PanoramaGuidance_1 = require("./camera/PanoramaGuidance");
|
|
93
93
|
Object.defineProperty(exports, "PanoramaGuidance", { enumerable: true, get: function () { return PanoramaGuidance_1.PanoramaGuidance; } });
|
|
94
|
+
// Settings modal — the modal is in `PanoramaSettingsModal.tsx`, but
|
|
95
|
+
// the type tree + defaults + JS↔native bridge live in dedicated
|
|
96
|
+
// files since v0.4 (F10). The modal is now a thin presentational
|
|
97
|
+
// component over the typed structure.
|
|
94
98
|
var PanoramaSettingsModal_1 = require("./camera/PanoramaSettingsModal");
|
|
95
99
|
Object.defineProperty(exports, "PanoramaSettingsModal", { enumerable: true, get: function () { return PanoramaSettingsModal_1.PanoramaSettingsModal; } });
|
|
96
|
-
|
|
100
|
+
// Settings types — the v0.4 engine-discriminated structures. Three
|
|
101
|
+
// disjoint top-level types (one per stitching engine), each composed
|
|
102
|
+
// of named sub-trees the corresponding native engine actually reads.
|
|
103
|
+
// See `./camera/PanoramaSettings.ts` for the rationale and the
|
|
104
|
+
// field-by-field native-consumer references.
|
|
105
|
+
var PanoramaSettings_1 = require("./camera/PanoramaSettings");
|
|
106
|
+
Object.defineProperty(exports, "DEFAULT_PANORAMA_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_PANORAMA_SETTINGS; } });
|
|
107
|
+
Object.defineProperty(exports, "DEFAULT_FLOW_GATE_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS; } });
|
|
108
|
+
Object.defineProperty(exports, "DEFAULT_SLITSCAN_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_SLITSCAN_SETTINGS; } });
|
|
109
|
+
Object.defineProperty(exports, "DEFAULT_HYBRID_SETTINGS", { enumerable: true, get: function () { return PanoramaSettings_1.DEFAULT_HYBRID_SETTINGS; } });
|
|
110
|
+
// Settings → native config adapters. Layer 2 hosts building their
|
|
111
|
+
// own capture flow on top of `incremental.start()` should always
|
|
112
|
+
// pass the result of the matching adapter as `config`; the bridge is
|
|
113
|
+
// the single source of truth for the JS↔native wire format.
|
|
114
|
+
var PanoramaSettingsBridge_1 = require("./camera/PanoramaSettingsBridge");
|
|
115
|
+
Object.defineProperty(exports, "panoramaSettingsToNativeConfig", { enumerable: true, get: function () { return PanoramaSettingsBridge_1.panoramaSettingsToNativeConfig; } });
|
|
116
|
+
Object.defineProperty(exports, "slitscanSettingsToNativeConfig", { enumerable: true, get: function () { return PanoramaSettingsBridge_1.slitscanSettingsToNativeConfig; } });
|
|
117
|
+
Object.defineProperty(exports, "hybridSettingsToNativeConfig", { enumerable: true, get: function () { return PanoramaSettingsBridge_1.hybridSettingsToNativeConfig; } });
|
|
97
118
|
var ViewportCropOverlay_1 = require("./camera/ViewportCropOverlay");
|
|
98
119
|
Object.defineProperty(exports, "ViewportCropOverlay", { enumerable: true, get: function () { return ViewportCropOverlay_1.ViewportCropOverlay; } });
|
|
99
120
|
// ── Capture hooks ─────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-image-stitcher",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
"CHANGELOG.md"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
28
|
+
"build": "tsc -p tsconfig.build.json",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "jest",
|
|
30
31
|
"clean": "rm -rf dist",
|
|
31
32
|
"postinstall": "node scripts/postinstall-fetch-binaries.js"
|
|
32
33
|
},
|
|
@@ -52,13 +53,16 @@
|
|
|
52
53
|
},
|
|
53
54
|
"homepage": "https://github.com/bhargavkanda/react-native-image-stitcher#readme",
|
|
54
55
|
"devDependencies": {
|
|
56
|
+
"@types/jest": "^29.5.0",
|
|
55
57
|
"@types/react": "^19.0.0",
|
|
58
|
+
"jest": "^29.7.0",
|
|
56
59
|
"react": "^19.0.0",
|
|
57
60
|
"react-native": "^0.84.0",
|
|
58
61
|
"react-native-safe-area-context": "^4.0.0",
|
|
59
62
|
"react-native-sensors": "^7.0.0",
|
|
60
63
|
"react-native-vision-camera": "^4.0.0",
|
|
61
64
|
"rxjs": "^7.0.0",
|
|
65
|
+
"ts-jest": "^29.1.0",
|
|
62
66
|
"typescript": "^5.5.0"
|
|
63
67
|
},
|
|
64
68
|
"peerDependencies": {
|
package/src/camera/Camera.tsx
CHANGED
|
@@ -69,11 +69,14 @@ import { CaptureKeyframePill } from './CaptureKeyframePill';
|
|
|
69
69
|
import { CaptureOrientationPill } from './CaptureOrientationPill';
|
|
70
70
|
import { CaptureStitchStatsToast, useStitchStatsToast } from './CaptureStitchStatsToast';
|
|
71
71
|
import { PanoramaBandOverlay } from './PanoramaBandOverlay';
|
|
72
|
+
import { type PanoramaSettings } from './PanoramaSettings';
|
|
73
|
+
import { panoramaSettingsToNativeConfig } from './PanoramaSettingsBridge';
|
|
74
|
+
import { PanoramaSettingsModal } from './PanoramaSettingsModal';
|
|
72
75
|
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} from './
|
|
76
|
+
buildPanoramaInitialSettings,
|
|
77
|
+
type PanoramaPropOverrides,
|
|
78
|
+
} from './buildPanoramaInitialSettings';
|
|
79
|
+
import { isLowMemDevice } from './lowMemDevice';
|
|
77
80
|
import { useCapture } from './useCapture';
|
|
78
81
|
import { useDeviceOrientation } from './useDeviceOrientation';
|
|
79
82
|
import {
|
|
@@ -471,40 +474,31 @@ function deriveEffectiveCaptureSource(
|
|
|
471
474
|
|
|
472
475
|
|
|
473
476
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
+
* Pluck the props that influence the initial PanoramaSettings tree.
|
|
478
|
+
* Kept inline (vs. a wide structural type) so future Camera prop
|
|
479
|
+
* additions don't accidentally widen the settings-translation
|
|
480
|
+
* surface — the pure builder in `./buildPanoramaInitialSettings.ts`
|
|
481
|
+
* has the canonical interface; this just forwards the relevant
|
|
482
|
+
* fields.
|
|
477
483
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
484
|
+
* The `default*ResolMP` props on `CameraProps` are documented as
|
|
485
|
+
* forward-looking no-ops; the new PanoramaSettings tree has no home
|
|
486
|
+
* for them yet (the v0.3 audit found cv::Stitcher's resol knobs
|
|
487
|
+
* aren't reached by either platform's bridge). They're accepted on
|
|
488
|
+
* the prop interface for API stability and ignored here.
|
|
482
489
|
*/
|
|
483
|
-
function
|
|
490
|
+
function extractPanoramaOverrides(props: CameraProps): PanoramaPropOverrides {
|
|
484
491
|
return {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
DEFAULT_PANORAMA_SETTINGS.flowNoveltyPercentile,
|
|
496
|
-
flowEvalEveryNFrames:
|
|
497
|
-
props.defaultFlowEvalEveryNFrames ??
|
|
498
|
-
DEFAULT_PANORAMA_SETTINGS.flowEvalEveryNFrames,
|
|
499
|
-
flowMaxTranslationCm:
|
|
500
|
-
props.defaultFlowMaxTranslationCm ??
|
|
501
|
-
DEFAULT_PANORAMA_SETTINGS.flowMaxTranslationCm,
|
|
502
|
-
keyframeMaxCount:
|
|
503
|
-
props.defaultKeyframeMaxCount ??
|
|
504
|
-
DEFAULT_PANORAMA_SETTINGS.keyframeMaxCount,
|
|
505
|
-
keyframeOverlapThreshold:
|
|
506
|
-
props.defaultKeyframeOverlapThreshold ??
|
|
507
|
-
DEFAULT_PANORAMA_SETTINGS.keyframeOverlapThreshold,
|
|
492
|
+
defaultCaptureSource: props.defaultCaptureSource,
|
|
493
|
+
defaultStitchMode: props.defaultStitchMode,
|
|
494
|
+
defaultBlender: props.defaultBlender,
|
|
495
|
+
defaultSeamFinder: props.defaultSeamFinder,
|
|
496
|
+
defaultWarper: props.defaultWarper,
|
|
497
|
+
defaultFlowNoveltyPercentile: props.defaultFlowNoveltyPercentile,
|
|
498
|
+
defaultFlowEvalEveryNFrames: props.defaultFlowEvalEveryNFrames,
|
|
499
|
+
defaultFlowMaxTranslationCm: props.defaultFlowMaxTranslationCm,
|
|
500
|
+
defaultKeyframeMaxCount: props.defaultKeyframeMaxCount,
|
|
501
|
+
defaultKeyframeOverlapThreshold: props.defaultKeyframeOverlapThreshold,
|
|
508
502
|
};
|
|
509
503
|
}
|
|
510
504
|
|
|
@@ -546,7 +540,10 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
546
540
|
);
|
|
547
541
|
const [lens, setLens] = useState<CameraLens>(defaultLens);
|
|
548
542
|
const [settings, setSettings] = useState<PanoramaSettings>(() =>
|
|
549
|
-
|
|
543
|
+
buildPanoramaInitialSettings(
|
|
544
|
+
extractPanoramaOverrides(props),
|
|
545
|
+
isLowMemDevice(),
|
|
546
|
+
),
|
|
550
547
|
);
|
|
551
548
|
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
|
552
549
|
const [statusPhase, setStatusPhase] = useState<CaptureStatusPhase>('idle');
|
|
@@ -694,12 +691,18 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
694
691
|
// |residual|` — which undercounted any time a non-IMU accept
|
|
695
692
|
// (flow novelty, force-last) reset the integrator before the
|
|
696
693
|
// budget threshold was reached.
|
|
694
|
+
// The translation budget lives at `frameSelection.flow.maxTranslationCm`
|
|
695
|
+
// in the new hierarchical settings shape. When `flow` is undefined
|
|
696
|
+
// (the consumer opted out of the flow strategy entirely), the gate
|
|
697
|
+
// stays disabled — same observable behaviour as v0.3's `0` default.
|
|
698
|
+
const flowMaxTranslationCm =
|
|
699
|
+
settings.frameSelection.flow?.maxTranslationCm ?? 0;
|
|
697
700
|
const imuGate = useIMUTranslationGate({
|
|
698
701
|
enabled:
|
|
699
702
|
isNonAR
|
|
700
703
|
&& statusPhase === 'recording'
|
|
701
|
-
&&
|
|
702
|
-
budgetMeters: Math.max(0.001,
|
|
704
|
+
&& flowMaxTranslationCm > 0,
|
|
705
|
+
budgetMeters: Math.max(0.001, flowMaxTranslationCm / 100.0),
|
|
703
706
|
onBudgetExceeded: () => {
|
|
704
707
|
const mod = getIncrementalNativeModule();
|
|
705
708
|
mod?.markNextFrameAsLastKeyframe?.().catch(() => undefined);
|
|
@@ -890,6 +893,25 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
890
893
|
deviceOrientation === 'portrait' ? 90
|
|
891
894
|
: deviceOrientation === 'portrait-upside-down' ? 270
|
|
892
895
|
: 0;
|
|
896
|
+
// v0.4 — the inline-flat config dict that v0.3 maintained here
|
|
897
|
+
// moved into `panoramaSettingsToNativeConfig` (see
|
|
898
|
+
// PanoramaSettingsBridge.ts). That adapter is the single source
|
|
899
|
+
// of truth for the JS→native wire format; both this call site
|
|
900
|
+
// AND the modal's reset-to-defaults preview agree on the same
|
|
901
|
+
// mapping. Audit fixes F1 / F4 / F6 from v0.3 are now properties
|
|
902
|
+
// of the bridge (verified by the unit tests in
|
|
903
|
+
// src/camera/__tests__/PanoramaSettingsBridge.test.ts).
|
|
904
|
+
//
|
|
905
|
+
// 2026-05-23 — override `captureSource` with the runtime-derived
|
|
906
|
+
// `effectiveCaptureSource` (from `arPreference + lens +
|
|
907
|
+
// AR-device-support`). Pre-this change the camera-screen AR
|
|
908
|
+
// toggle wrote ONLY to local `arPreference` state while the
|
|
909
|
+
// bridge read `settings.captureSource` — so native could think
|
|
910
|
+
// the capture was AR while the operator had toggled it off (or
|
|
911
|
+
// vice-versa). Single source of truth now: whatever camera the
|
|
912
|
+
// operator can see is what native is told it is. The settings
|
|
913
|
+
// modal's `captureSource` control has been removed for the same
|
|
914
|
+
// reason — see PanoramaSettingsModal.tsx for the rationale.
|
|
893
915
|
await incremental.start({
|
|
894
916
|
snapshotJpegQuality: 75,
|
|
895
917
|
snapshotEveryNAccepts: 1,
|
|
@@ -901,37 +923,10 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
901
923
|
canvasWidth: 5000,
|
|
902
924
|
canvasHeight: 5000,
|
|
903
925
|
engine: 'batch-keyframe',
|
|
904
|
-
config: {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
blenderType: settings.blenderType,
|
|
909
|
-
seamFinderType: settings.seamFinderType,
|
|
910
|
-
enableMaxInscribedRectCrop: settings.enableMaxInscribedRectCrop,
|
|
911
|
-
// ── KeyframeGate (per-frame selection) ────────────────────
|
|
912
|
-
// F6 audit fix: pass settings.frameSelectionMode through
|
|
913
|
-
// instead of hardcoding 'flow-based' (which silently made the
|
|
914
|
-
// time-based / pose-based modal options no-ops).
|
|
915
|
-
frameSelectionMode: settings.frameSelectionMode,
|
|
916
|
-
keyframeMaxCount: settings.keyframeMaxCount,
|
|
917
|
-
keyframeOverlapThreshold: settings.keyframeOverlapThreshold,
|
|
918
|
-
// ── Flow-strategy tunables ────────────────────────────────
|
|
919
|
-
// F4 audit fix: previously omitted, which made the modal
|
|
920
|
-
// sliders for these three a complete no-op (only iOS native
|
|
921
|
-
// even read them, and only when JS sent them).
|
|
922
|
-
flowNoveltyPercentile: settings.flowNoveltyPercentile,
|
|
923
|
-
flowEvalEveryNFrames: settings.flowEvalEveryNFrames,
|
|
924
|
-
flowMaxTranslationCm: settings.flowMaxTranslationCm,
|
|
925
|
-
flowMaxCorners: settings.flowMaxCorners,
|
|
926
|
-
flowQualityLevel: settings.flowQualityLevel,
|
|
927
|
-
flowMinDistance: settings.flowMinDistance,
|
|
928
|
-
// ── Engine-routing flags consumed by native ───────────────
|
|
929
|
-
// F1 audit fix: Android keyframe gate's disableAngularFallback
|
|
930
|
-
// opt-out reads this to decide whether to skip the angular
|
|
931
|
-
// fallback (gyro pose is too noisy for the FoV-overlap calc
|
|
932
|
-
// in non-AR mode, causing degenerate cv::Stitcher params).
|
|
933
|
-
captureSource: settings.captureSource,
|
|
934
|
-
},
|
|
926
|
+
config: panoramaSettingsToNativeConfig({
|
|
927
|
+
...settings,
|
|
928
|
+
captureSource: effectiveCaptureSource,
|
|
929
|
+
}),
|
|
935
930
|
});
|
|
936
931
|
imuGate.resetAnchor();
|
|
937
932
|
// Start pumping vision-camera snapshots into the engine for
|
|
@@ -959,6 +954,7 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
959
954
|
isNonAR,
|
|
960
955
|
deviceOrientation,
|
|
961
956
|
settings,
|
|
957
|
+
effectiveCaptureSource,
|
|
962
958
|
imuGate,
|
|
963
959
|
jsDriver,
|
|
964
960
|
onError,
|
|
@@ -1048,6 +1044,18 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1048
1044
|
onError,
|
|
1049
1045
|
recordingStartedAt,
|
|
1050
1046
|
jsDriver,
|
|
1047
|
+
// F10 Phase 2 review N1 — these four were missing pre-fix. The
|
|
1048
|
+
// callback reads `settings.debug` (to gate the stitchToast),
|
|
1049
|
+
// `isNonAR` (to decide whether to read IMU totalAbs translation),
|
|
1050
|
+
// `imuGate` (the read itself), and `stitchToast` (the toast hook
|
|
1051
|
+
// object). If any of those identities change between the user
|
|
1052
|
+
// pressing-and-holding the shutter and the release, the stale-
|
|
1053
|
+
// closure read could disagree with the actual current state.
|
|
1054
|
+
// Pre-existing v0.3 bug; v0.4 was the natural time to address it.
|
|
1055
|
+
settings,
|
|
1056
|
+
isNonAR,
|
|
1057
|
+
imuGate,
|
|
1058
|
+
stitchToast,
|
|
1051
1059
|
]);
|
|
1052
1060
|
|
|
1053
1061
|
// ── Lens / AR-toggle handlers ───────────────────────────────────
|
|
@@ -1132,8 +1140,8 @@ export function Camera(props: CameraProps): React.JSX.Element {
|
|
|
1132
1140
|
isNonAR ? imuGate.getTranslationMetres() : null
|
|
1133
1141
|
}
|
|
1134
1142
|
captureSource={effectiveCaptureSource}
|
|
1135
|
-
frameSelectionMode={settings.
|
|
1136
|
-
stitchMode={settings.stitchMode}
|
|
1143
|
+
frameSelectionMode={settings.frameSelection.mode}
|
|
1144
|
+
stitchMode={settings.stitcher.stitchMode}
|
|
1137
1145
|
/>
|
|
1138
1146
|
</>
|
|
1139
1147
|
)}
|