react-native-image-stitcher 0.13.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +33 -17
  3. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +33 -5
  4. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +73 -1
  5. package/dist/camera/Camera.d.ts +71 -16
  6. package/dist/camera/Camera.js +167 -51
  7. package/dist/camera/CameraView.d.ts +6 -0
  8. package/dist/camera/CameraView.js +2 -2
  9. package/dist/camera/CaptureHeader.js +39 -16
  10. package/dist/camera/CapturePreview.js +13 -1
  11. package/dist/camera/CaptureThumbnailStrip.d.ts +25 -1
  12. package/dist/camera/CaptureThumbnailStrip.js +17 -4
  13. package/dist/camera/PanoramaBandOverlay.d.ts +76 -0
  14. package/dist/camera/PanoramaBandOverlay.js +90 -33
  15. package/dist/camera/PanoramaConfirmModal.js +11 -1
  16. package/dist/camera/selectCaptureDevice.d.ts +93 -0
  17. package/dist/camera/selectCaptureDevice.js +131 -0
  18. package/dist/camera/useCapture.d.ts +40 -0
  19. package/dist/camera/useCapture.js +50 -12
  20. package/dist/camera/useContentRotation.d.ts +99 -0
  21. package/dist/camera/useContentRotation.js +124 -0
  22. package/dist/index.d.ts +1 -3
  23. package/dist/index.js +6 -5
  24. package/package.json +1 -1
  25. package/src/camera/Camera.tsx +281 -118
  26. package/src/camera/CameraView.tsx +9 -0
  27. package/src/camera/CaptureHeader.tsx +39 -16
  28. package/src/camera/CapturePreview.tsx +12 -0
  29. package/src/camera/CaptureThumbnailStrip.tsx +44 -4
  30. package/src/camera/PanoramaBandOverlay.tsx +97 -35
  31. package/src/camera/PanoramaConfirmModal.tsx +10 -0
  32. package/src/camera/__tests__/bandThumbRotation.test.ts +120 -0
  33. package/src/camera/__tests__/homeIndicatorEdge.test.ts +116 -0
  34. package/src/camera/__tests__/selectCaptureDevice.test.ts +177 -0
  35. package/src/camera/__tests__/useContentRotation.test.ts +89 -0
  36. package/src/camera/selectCaptureDevice.ts +187 -0
  37. package/src/camera/useCapture.ts +99 -11
  38. package/src/camera/useContentRotation.ts +149 -0
  39. package/src/index.ts +6 -2
@@ -0,0 +1,99 @@
1
+ /**
2
+ * useContentRotation — returns a CSS transform that rotates control
3
+ * content so labels stay upright relative to device gravity,
4
+ * regardless of whether the OS rotated the framebuffer.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * v0.12 anchored `<Camera>`'s bottom controls to the home-indicator
9
+ * edge so they stay in thumb reach on phones in landscape on
10
+ * non-locked iOS hosts. The anchoring works because the OS rotates
11
+ * the framebuffer to match the device, so a JS-bottom view in
12
+ * landscape is the device's actual landscape-bottom edge.
13
+ *
14
+ * On locked-portrait hosts (the most common production
15
+ * configuration) the OS does NOT rotate the framebuffer when the
16
+ * device tilts to landscape. v0.12 still anchored controls to
17
+ * "JS-bottom" — which is now the device's side edge — so the
18
+ * shutter sits where the thumb expects, BUT the labels inside
19
+ * each control (`AR`, `1×`, `0.5×`, the lens chip pills, the gear)
20
+ * render at their JS-portrait baseline, so the user holding the
21
+ * device sideways reads them at 90°.
22
+ *
23
+ * This hook fixes that by applying a `transform: rotate(±90°)` to
24
+ * the control's *content* so it appears upright relative to actual
25
+ * gravity, while the control container itself stays in place.
26
+ *
27
+ * ## How the rotation is computed
28
+ *
29
+ * Two signals:
30
+ * - **Framebuffer rotation** — what rotation has the OS already
31
+ * applied to the JS layout? Read from
32
+ * `useWindowDimensions().width > height` — non-locked +
33
+ * device-landscape is the only case where the OS rotates,
34
+ * and that's exactly when `jsLandscape === true`.
35
+ * - **Device-physical rotation** — what rotation does the device
36
+ * have relative to gravity? Read from `useDeviceOrientation()`
37
+ * (accelerometer-derived).
38
+ *
39
+ * The content rotation we apply is the *difference* between
40
+ * device-physical and framebuffer rotation, so the net rotation
41
+ * (content × framebuffer) equals device-physical → labels are
42
+ * upright in the world.
43
+ *
44
+ * ## Truth table
45
+ *
46
+ * | Host config | Device | jsLandscape | Net rot |
47
+ * |--- |--- |--- |--- |
48
+ * | Locked-portrait | portrait | false | 0° |
49
+ * | Locked-portrait | landscape-left | false | 90° |
50
+ * | Locked-portrait | landscape-right | false | -90° |
51
+ * | Locked-portrait | upside-down | false | 180° |
52
+ * | Non-locked | portrait | false | 0° |
53
+ * | Non-locked | landscape-left | true | 0° |
54
+ * | Non-locked | landscape-right | true | 0° |
55
+ *
56
+ * The 0° case is the common one (locked-portrait + device-portrait
57
+ * OR non-locked + framebuffer-already-rotated); we return an empty
58
+ * style object so React skips the layout work.
59
+ *
60
+ * ## Caveats
61
+ *
62
+ * - Rotation transforms preserve hit-testing in RN 0.84 (verified
63
+ * on iOS + Android), but historical RN versions had bugs in this
64
+ * area. If support for older RN is added, retest pressables.
65
+ * - Containers whose sized layouts depend on un-rotated content
66
+ * (e.g. a 100px-wide pill containing text that's now rotated 90°)
67
+ * may overflow. Fixed-size pills (the lens chip, AR toggle,
68
+ * flash button) are fine; the header title's `flex: 1 + textAlign:
69
+ * center` may need tuning when rotated — see `CaptureHeader`'s
70
+ * own rotation handling.
71
+ */
72
+ import { type ViewStyle } from 'react-native';
73
+ import { type DeviceOrientation } from './useDeviceOrientation';
74
+ export type ContentRotationDeg = 0 | 90 | -90 | 180;
75
+ /**
76
+ * Return type for `useContentRotation`. Typed structurally on just
77
+ * the `transform` property so it spreads cleanly into ViewStyle,
78
+ * TextStyle, AND ImageStyle — all three accept identical transform
79
+ * shapes in RN 0.84. Returning the more specific `ViewStyle` would
80
+ * collide with ImageStyle's stricter `overflow` enum at <Image>
81
+ * call sites.
82
+ */
83
+ export type ContentRotationStyle = {
84
+ transform?: ViewStyle['transform'];
85
+ };
86
+ /**
87
+ * Pure rotation computation. Exported so tests can exercise the
88
+ * full truth table without booting a React render.
89
+ */
90
+ export declare function contentRotationDeg(jsLandscape: boolean, deviceOrient: DeviceOrientation): ContentRotationDeg;
91
+ /**
92
+ * Returns the rotation as a ready-to-spread style object. Empty
93
+ * object in the common 0° case so React skips the layout work.
94
+ * Type `ContentRotationStyle` is structurally just `{ transform? }`
95
+ * so call sites can spread it into ViewStyle, TextStyle, or
96
+ * ImageStyle interchangeably.
97
+ */
98
+ export declare function useContentRotation(): ContentRotationStyle;
99
+ //# sourceMappingURL=useContentRotation.d.ts.map
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * useContentRotation — returns a CSS transform that rotates control
5
+ * content so labels stay upright relative to device gravity,
6
+ * regardless of whether the OS rotated the framebuffer.
7
+ *
8
+ * ## Why this exists
9
+ *
10
+ * v0.12 anchored `<Camera>`'s bottom controls to the home-indicator
11
+ * edge so they stay in thumb reach on phones in landscape on
12
+ * non-locked iOS hosts. The anchoring works because the OS rotates
13
+ * the framebuffer to match the device, so a JS-bottom view in
14
+ * landscape is the device's actual landscape-bottom edge.
15
+ *
16
+ * On locked-portrait hosts (the most common production
17
+ * configuration) the OS does NOT rotate the framebuffer when the
18
+ * device tilts to landscape. v0.12 still anchored controls to
19
+ * "JS-bottom" — which is now the device's side edge — so the
20
+ * shutter sits where the thumb expects, BUT the labels inside
21
+ * each control (`AR`, `1×`, `0.5×`, the lens chip pills, the gear)
22
+ * render at their JS-portrait baseline, so the user holding the
23
+ * device sideways reads them at 90°.
24
+ *
25
+ * This hook fixes that by applying a `transform: rotate(±90°)` to
26
+ * the control's *content* so it appears upright relative to actual
27
+ * gravity, while the control container itself stays in place.
28
+ *
29
+ * ## How the rotation is computed
30
+ *
31
+ * Two signals:
32
+ * - **Framebuffer rotation** — what rotation has the OS already
33
+ * applied to the JS layout? Read from
34
+ * `useWindowDimensions().width > height` — non-locked +
35
+ * device-landscape is the only case where the OS rotates,
36
+ * and that's exactly when `jsLandscape === true`.
37
+ * - **Device-physical rotation** — what rotation does the device
38
+ * have relative to gravity? Read from `useDeviceOrientation()`
39
+ * (accelerometer-derived).
40
+ *
41
+ * The content rotation we apply is the *difference* between
42
+ * device-physical and framebuffer rotation, so the net rotation
43
+ * (content × framebuffer) equals device-physical → labels are
44
+ * upright in the world.
45
+ *
46
+ * ## Truth table
47
+ *
48
+ * | Host config | Device | jsLandscape | Net rot |
49
+ * |--- |--- |--- |--- |
50
+ * | Locked-portrait | portrait | false | 0° |
51
+ * | Locked-portrait | landscape-left | false | 90° |
52
+ * | Locked-portrait | landscape-right | false | -90° |
53
+ * | Locked-portrait | upside-down | false | 180° |
54
+ * | Non-locked | portrait | false | 0° |
55
+ * | Non-locked | landscape-left | true | 0° |
56
+ * | Non-locked | landscape-right | true | 0° |
57
+ *
58
+ * The 0° case is the common one (locked-portrait + device-portrait
59
+ * OR non-locked + framebuffer-already-rotated); we return an empty
60
+ * style object so React skips the layout work.
61
+ *
62
+ * ## Caveats
63
+ *
64
+ * - Rotation transforms preserve hit-testing in RN 0.84 (verified
65
+ * on iOS + Android), but historical RN versions had bugs in this
66
+ * area. If support for older RN is added, retest pressables.
67
+ * - Containers whose sized layouts depend on un-rotated content
68
+ * (e.g. a 100px-wide pill containing text that's now rotated 90°)
69
+ * may overflow. Fixed-size pills (the lens chip, AR toggle,
70
+ * flash button) are fine; the header title's `flex: 1 + textAlign:
71
+ * center` may need tuning when rotated — see `CaptureHeader`'s
72
+ * own rotation handling.
73
+ */
74
+ Object.defineProperty(exports, "__esModule", { value: true });
75
+ exports.contentRotationDeg = contentRotationDeg;
76
+ exports.useContentRotation = useContentRotation;
77
+ const react_native_1 = require("react-native");
78
+ const useDeviceOrientation_1 = require("./useDeviceOrientation");
79
+ /**
80
+ * Pure rotation computation. Exported so tests can exercise the
81
+ * full truth table without booting a React render.
82
+ */
83
+ function contentRotationDeg(jsLandscape, deviceOrient) {
84
+ // Framebuffer rotation relative to device-physical. Only the
85
+ // non-locked + device-landscape cases see a rotated framebuffer.
86
+ // jsLandscape can briefly be true mid-rotation on devices that
87
+ // aren't a clean landscape orientation; the device-orientation
88
+ // check below catches those and falls through to 0.
89
+ const fbRot = !jsLandscape ? 0
90
+ : deviceOrient === 'landscape-left' ? 90
91
+ : deviceOrient === 'landscape-right' ? -90
92
+ : 0;
93
+ // Device-physical rotation relative to gravity.
94
+ const deviceRot = deviceOrient === 'portrait' ? 0
95
+ : deviceOrient === 'landscape-left' ? 90
96
+ : deviceOrient === 'landscape-right' ? -90
97
+ : 180;
98
+ // Net rotation we need to apply to content so that
99
+ // content + framebuffer = device-physical (upright in the world).
100
+ // Normalise to [-180, 180] so transform values stay canonical.
101
+ let net = deviceRot - fbRot;
102
+ if (net > 180)
103
+ net -= 360;
104
+ if (net < -180)
105
+ net += 360;
106
+ return net;
107
+ }
108
+ /**
109
+ * Returns the rotation as a ready-to-spread style object. Empty
110
+ * object in the common 0° case so React skips the layout work.
111
+ * Type `ContentRotationStyle` is structurally just `{ transform? }`
112
+ * so call sites can spread it into ViewStyle, TextStyle, or
113
+ * ImageStyle interchangeably.
114
+ */
115
+ function useContentRotation() {
116
+ const orient = (0, useDeviceOrientation_1.useDeviceOrientation)();
117
+ const { width, height } = (0, react_native_1.useWindowDimensions)();
118
+ const jsLandscape = width > height;
119
+ const deg = contentRotationDeg(jsLandscape, orient);
120
+ return deg === 0
121
+ ? {}
122
+ : { transform: [{ rotate: `${deg}deg` }] };
123
+ }
124
+ //# sourceMappingURL=useContentRotation.js.map
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * adds RetaiLens-specific features on top.
21
21
  */
22
22
  export { Camera, CameraError } from './camera/Camera';
23
- export type { CameraProps, CameraCaptureResult, CameraErrorCode, CaptureSource, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
23
+ export type { CameraProps, CameraCaptureResult, CameraErrorCode, CaptureSource, CaptureSourcesMode, CameraLens, StitchMode, Blender, SeamFinder, Warper, FramesDroppedInfo, } from './camera/Camera';
24
24
  export { useARSession, ARTrackingState } from './ar/useARSession';
25
25
  export type { UseARSessionReturn, FramePose, } from './ar/useARSession';
26
26
  export { useIMUTranslationGate } from './sensors/useIMUTranslationGate';
@@ -47,9 +47,7 @@ export { CaptureStitchStatsToast, useStitchStatsToast, } from './camera/CaptureS
47
47
  export type { CaptureStitchStatsToastProps, UseStitchStatsToastReturn, } from './camera/CaptureStitchStatsToast';
48
48
  export { CaptureThumbnailStrip } from './camera/CaptureThumbnailStrip';
49
49
  export type { CaptureThumbnailItem } from './camera/CaptureThumbnailStrip';
50
- export { IncrementalPanGuide } from './camera/IncrementalPanGuide';
51
50
  export { PanoramaBandOverlay } from './camera/PanoramaBandOverlay';
52
- export { PanoramaGuidance } from './camera/PanoramaGuidance';
53
51
  export { PanoramaSettingsModal } from './camera/PanoramaSettingsModal';
54
52
  export type { PanoramaSettingsModalProps } from './camera/PanoramaSettingsModal';
55
53
  export { DEFAULT_PANORAMA_SETTINGS, DEFAULT_FLOW_GATE_SETTINGS, DEFAULT_SLITSCAN_SETTINGS, DEFAULT_HYBRID_SETTINGS, } from './camera/PanoramaSettings';
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.useStitcherWorklet = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.OrientationDriftModal = exports.useOrientationDrift = 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;
25
+ exports.stitchVideo = exports.useStitcherWorklet = exports.useFrameProcessorDriver = exports.useFrameStream = exports.useThrottledFrameProcessor = exports.useFrameProcessor = exports.useKeyframeStream = exports.useIncrementalStitcher = exports.cleanupOldKeyframes = exports.getIncrementalNativeModule = exports.subscribeIncrementalState = exports.incrementalStitcherIsAvailable = exports.IncrementalOutcome = exports.OrientationDriftModal = exports.useOrientationDrift = 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.PanoramaBandOverlay = 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
  // ─────────────────────────────────────────────────────────────────────
@@ -85,12 +85,13 @@ Object.defineProperty(exports, "CaptureStitchStatsToast", { enumerable: true, ge
85
85
  Object.defineProperty(exports, "useStitchStatsToast", { enumerable: true, get: function () { return CaptureStitchStatsToast_1.useStitchStatsToast; } });
86
86
  var CaptureThumbnailStrip_1 = require("./camera/CaptureThumbnailStrip");
87
87
  Object.defineProperty(exports, "CaptureThumbnailStrip", { enumerable: true, get: function () { return CaptureThumbnailStrip_1.CaptureThumbnailStrip; } });
88
- var IncrementalPanGuide_1 = require("./camera/IncrementalPanGuide");
89
- Object.defineProperty(exports, "IncrementalPanGuide", { enumerable: true, get: function () { return IncrementalPanGuide_1.IncrementalPanGuide; } });
88
+ // v0.13.1 IncrementalPanGuide (drift marker) and PanoramaGuidance
89
+ // (pan-speed pill) are no longer part of the public API. They remain
90
+ // in the tree as internal-only components but are not exported and not
91
+ // rendered by <Camera> (the `panGuide` / `panoramaGuidance` props were
92
+ // removed). Re-introduce here if a host need resurfaces.
90
93
  var PanoramaBandOverlay_1 = require("./camera/PanoramaBandOverlay");
91
94
  Object.defineProperty(exports, "PanoramaBandOverlay", { enumerable: true, get: function () { return PanoramaBandOverlay_1.PanoramaBandOverlay; } });
92
- var PanoramaGuidance_1 = require("./camera/PanoramaGuidance");
93
- Object.defineProperty(exports, "PanoramaGuidance", { enumerable: true, get: function () { return PanoramaGuidance_1.PanoramaGuidance; } });
94
95
  // Settings modal — the modal is in `PanoramaSettingsModal.tsx`, but
95
96
  // the type tree + defaults + JS↔native bridge live in dedicated
96
97
  // files since v0.4 (F10). The modal is now a thin presentational
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-image-stitcher",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",