react-native-image-stitcher 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +181 -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 +226 -0
  6. package/dist/camera/Camera.js +208 -20
  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 +546 -32
  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,149 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * useContentRotation — returns a CSS transform that rotates control
4
+ * content so labels stay upright relative to device gravity,
5
+ * regardless of whether the OS rotated the framebuffer.
6
+ *
7
+ * ## Why this exists
8
+ *
9
+ * v0.12 anchored `<Camera>`'s bottom controls to the home-indicator
10
+ * edge so they stay in thumb reach on phones in landscape on
11
+ * non-locked iOS hosts. The anchoring works because the OS rotates
12
+ * the framebuffer to match the device, so a JS-bottom view in
13
+ * landscape is the device's actual landscape-bottom edge.
14
+ *
15
+ * On locked-portrait hosts (the most common production
16
+ * configuration) the OS does NOT rotate the framebuffer when the
17
+ * device tilts to landscape. v0.12 still anchored controls to
18
+ * "JS-bottom" — which is now the device's side edge — so the
19
+ * shutter sits where the thumb expects, BUT the labels inside
20
+ * each control (`AR`, `1×`, `0.5×`, the lens chip pills, the gear)
21
+ * render at their JS-portrait baseline, so the user holding the
22
+ * device sideways reads them at 90°.
23
+ *
24
+ * This hook fixes that by applying a `transform: rotate(±90°)` to
25
+ * the control's *content* so it appears upright relative to actual
26
+ * gravity, while the control container itself stays in place.
27
+ *
28
+ * ## How the rotation is computed
29
+ *
30
+ * Two signals:
31
+ * - **Framebuffer rotation** — what rotation has the OS already
32
+ * applied to the JS layout? Read from
33
+ * `useWindowDimensions().width > height` — non-locked +
34
+ * device-landscape is the only case where the OS rotates,
35
+ * and that's exactly when `jsLandscape === true`.
36
+ * - **Device-physical rotation** — what rotation does the device
37
+ * have relative to gravity? Read from `useDeviceOrientation()`
38
+ * (accelerometer-derived).
39
+ *
40
+ * The content rotation we apply is the *difference* between
41
+ * device-physical and framebuffer rotation, so the net rotation
42
+ * (content × framebuffer) equals device-physical → labels are
43
+ * upright in the world.
44
+ *
45
+ * ## Truth table
46
+ *
47
+ * | Host config | Device | jsLandscape | Net rot |
48
+ * |--- |--- |--- |--- |
49
+ * | Locked-portrait | portrait | false | 0° |
50
+ * | Locked-portrait | landscape-left | false | 90° |
51
+ * | Locked-portrait | landscape-right | false | -90° |
52
+ * | Locked-portrait | upside-down | false | 180° |
53
+ * | Non-locked | portrait | false | 0° |
54
+ * | Non-locked | landscape-left | true | 0° |
55
+ * | Non-locked | landscape-right | true | 0° |
56
+ *
57
+ * The 0° case is the common one (locked-portrait + device-portrait
58
+ * OR non-locked + framebuffer-already-rotated); we return an empty
59
+ * style object so React skips the layout work.
60
+ *
61
+ * ## Caveats
62
+ *
63
+ * - Rotation transforms preserve hit-testing in RN 0.84 (verified
64
+ * on iOS + Android), but historical RN versions had bugs in this
65
+ * area. If support for older RN is added, retest pressables.
66
+ * - Containers whose sized layouts depend on un-rotated content
67
+ * (e.g. a 100px-wide pill containing text that's now rotated 90°)
68
+ * may overflow. Fixed-size pills (the lens chip, AR toggle,
69
+ * flash button) are fine; the header title's `flex: 1 + textAlign:
70
+ * center` may need tuning when rotated — see `CaptureHeader`'s
71
+ * own rotation handling.
72
+ */
73
+
74
+ import { useWindowDimensions, type ViewStyle } from 'react-native';
75
+
76
+ import {
77
+ useDeviceOrientation,
78
+ type DeviceOrientation,
79
+ } from './useDeviceOrientation';
80
+
81
+
82
+ export type ContentRotationDeg = 0 | 90 | -90 | 180;
83
+
84
+
85
+ /**
86
+ * Return type for `useContentRotation`. Typed structurally on just
87
+ * the `transform` property so it spreads cleanly into ViewStyle,
88
+ * TextStyle, AND ImageStyle — all three accept identical transform
89
+ * shapes in RN 0.84. Returning the more specific `ViewStyle` would
90
+ * collide with ImageStyle's stricter `overflow` enum at <Image>
91
+ * call sites.
92
+ */
93
+ export type ContentRotationStyle = {
94
+ transform?: ViewStyle['transform'];
95
+ };
96
+
97
+
98
+ /**
99
+ * Pure rotation computation. Exported so tests can exercise the
100
+ * full truth table without booting a React render.
101
+ */
102
+ export function contentRotationDeg(
103
+ jsLandscape: boolean,
104
+ deviceOrient: DeviceOrientation,
105
+ ): ContentRotationDeg {
106
+ // Framebuffer rotation relative to device-physical. Only the
107
+ // non-locked + device-landscape cases see a rotated framebuffer.
108
+ // jsLandscape can briefly be true mid-rotation on devices that
109
+ // aren't a clean landscape orientation; the device-orientation
110
+ // check below catches those and falls through to 0.
111
+ const fbRot: 0 | 90 | -90 =
112
+ !jsLandscape ? 0
113
+ : deviceOrient === 'landscape-left' ? 90
114
+ : deviceOrient === 'landscape-right' ? -90
115
+ : 0;
116
+
117
+ // Device-physical rotation relative to gravity.
118
+ const deviceRot: 0 | 90 | -90 | 180 =
119
+ deviceOrient === 'portrait' ? 0
120
+ : deviceOrient === 'landscape-left' ? 90
121
+ : deviceOrient === 'landscape-right' ? -90
122
+ : 180;
123
+
124
+ // Net rotation we need to apply to content so that
125
+ // content + framebuffer = device-physical (upright in the world).
126
+ // Normalise to [-180, 180] so transform values stay canonical.
127
+ let net = deviceRot - fbRot;
128
+ if (net > 180) net -= 360;
129
+ if (net < -180) net += 360;
130
+ return net as ContentRotationDeg;
131
+ }
132
+
133
+
134
+ /**
135
+ * Returns the rotation as a ready-to-spread style object. Empty
136
+ * object in the common 0° case so React skips the layout work.
137
+ * Type `ContentRotationStyle` is structurally just `{ transform? }`
138
+ * so call sites can spread it into ViewStyle, TextStyle, or
139
+ * ImageStyle interchangeably.
140
+ */
141
+ export function useContentRotation(): ContentRotationStyle {
142
+ const orient = useDeviceOrientation();
143
+ const { width, height } = useWindowDimensions();
144
+ const jsLandscape = width > height;
145
+ const deg = contentRotationDeg(jsLandscape, orient);
146
+ return deg === 0
147
+ ? {}
148
+ : { transform: [{ rotate: `${deg}deg` }] };
149
+ }
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export type {
30
30
  CameraCaptureResult,
31
31
  CameraErrorCode,
32
32
  CaptureSource,
33
+ CaptureSourcesMode,
33
34
  CameraLens,
34
35
  StitchMode,
35
36
  Blender,
@@ -106,9 +107,12 @@ export type {
106
107
  } from './camera/CaptureStitchStatsToast';
107
108
  export { CaptureThumbnailStrip } from './camera/CaptureThumbnailStrip';
108
109
  export type { CaptureThumbnailItem } from './camera/CaptureThumbnailStrip';
109
- export { IncrementalPanGuide } from './camera/IncrementalPanGuide';
110
+ // v0.13.1 IncrementalPanGuide (drift marker) and PanoramaGuidance
111
+ // (pan-speed pill) are no longer part of the public API. They remain
112
+ // in the tree as internal-only components but are not exported and not
113
+ // rendered by <Camera> (the `panGuide` / `panoramaGuidance` props were
114
+ // removed). Re-introduce here if a host need resurfaces.
110
115
  export { PanoramaBandOverlay } from './camera/PanoramaBandOverlay';
111
- export { PanoramaGuidance } from './camera/PanoramaGuidance';
112
116
  // Settings modal — the modal is in `PanoramaSettingsModal.tsx`, but
113
117
  // the type tree + defaults + JS↔native bridge live in dedicated
114
118
  // files since v0.4 (F10). The modal is now a thin presentational