react-native-image-stitcher 0.15.2 → 0.16.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 (133) hide show
  1. package/CHANGELOG.md +124 -1
  2. package/README.md +116 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
  4. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
  6. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  8. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  9. package/cpp/crop_quad.cpp +162 -0
  10. package/cpp/crop_quad.hpp +163 -0
  11. package/cpp/stitcher.cpp +651 -55
  12. package/cpp/stitcher.hpp +10 -0
  13. package/cpp/warp_guard.hpp +212 -0
  14. package/dist/camera/Camera.d.ts +196 -12
  15. package/dist/camera/Camera.js +629 -35
  16. package/dist/camera/CameraView.js +35 -16
  17. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  18. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  19. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  20. package/dist/camera/CaptureFrameCounterOverlay.js +142 -0
  21. package/dist/camera/CaptureMemoryPill.d.ts +9 -1
  22. package/dist/camera/CaptureMemoryPill.js +3 -3
  23. package/dist/camera/CapturePreview.js +2 -1
  24. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  25. package/dist/camera/CaptureStatusOverlay.js +22 -5
  26. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  27. package/dist/camera/LateralMotionModal.d.ts +85 -0
  28. package/dist/camera/LateralMotionModal.js +134 -0
  29. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  30. package/dist/camera/PanHowToOverlay.js +222 -0
  31. package/dist/camera/PanoramaSettings.d.ts +8 -6
  32. package/dist/camera/PanoramaSettings.js +26 -5
  33. package/dist/camera/PanoramaSettingsModal.js +4 -4
  34. package/dist/camera/RectCropPreview.d.ts +161 -0
  35. package/dist/camera/RectCropPreview.js +480 -0
  36. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  37. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  38. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  39. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  40. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  41. package/dist/camera/cameraErrorMessages.js +26 -10
  42. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  43. package/dist/camera/cameraGuidanceCopy.js +80 -0
  44. package/dist/camera/captureCountdown.d.ts +52 -0
  45. package/dist/camera/captureCountdown.js +76 -0
  46. package/dist/camera/captureWarnings.d.ts +90 -0
  47. package/dist/camera/captureWarnings.js +108 -0
  48. package/dist/camera/classifyStitchError.d.ts +30 -0
  49. package/dist/camera/classifyStitchError.js +42 -0
  50. package/dist/camera/cropGeometry.d.ts +136 -0
  51. package/dist/camera/cropGeometry.js +223 -0
  52. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  53. package/dist/camera/displayDecodeImageProps.js +29 -0
  54. package/dist/camera/guidanceGraphics.d.ts +58 -0
  55. package/dist/camera/guidanceGraphics.js +280 -0
  56. package/dist/camera/guidanceTokens.d.ts +54 -0
  57. package/dist/camera/guidanceTokens.js +58 -0
  58. package/dist/camera/panModeGate.d.ts +54 -0
  59. package/dist/camera/panModeGate.js +62 -0
  60. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  61. package/dist/camera/pickCaptureFormat.js +85 -0
  62. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  63. package/dist/camera/stitchDebugInfo.js +55 -0
  64. package/dist/camera/usePanMotion.d.ts +250 -0
  65. package/dist/camera/usePanMotion.js +451 -0
  66. package/dist/index.d.ts +24 -3
  67. package/dist/index.js +33 -2
  68. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  69. package/dist/stitching/computeInscribedRect.js +55 -0
  70. package/dist/stitching/cropQuad.d.ts +78 -0
  71. package/dist/stitching/cropQuad.js +116 -0
  72. package/dist/stitching/incremental.d.ts +45 -0
  73. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
  74. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  75. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  76. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  77. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +191 -7
  78. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  79. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  80. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  81. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  82. package/package.json +5 -1
  83. package/src/camera/Camera.tsx +994 -47
  84. package/src/camera/CameraView.tsx +48 -16
  85. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  86. package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
  87. package/src/camera/CaptureMemoryPill.tsx +17 -3
  88. package/src/camera/CapturePreview.tsx +5 -0
  89. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  90. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  91. package/src/camera/LateralMotionModal.tsx +199 -0
  92. package/src/camera/PanHowToOverlay.tsx +246 -0
  93. package/src/camera/PanoramaSettings.ts +34 -11
  94. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  95. package/src/camera/RectCropPreview.tsx +820 -0
  96. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  97. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  98. package/src/camera/cameraErrorMessages.ts +39 -2
  99. package/src/camera/cameraGuidanceCopy.ts +145 -0
  100. package/src/camera/captureCountdown.ts +83 -0
  101. package/src/camera/captureWarnings.ts +190 -0
  102. package/src/camera/classifyStitchError.ts +68 -0
  103. package/src/camera/cropGeometry.ts +268 -0
  104. package/src/camera/displayDecodeImageProps.ts +25 -0
  105. package/src/camera/guidanceGraphics.tsx +347 -0
  106. package/src/camera/guidanceTokens.ts +57 -0
  107. package/src/camera/panModeGate.ts +81 -0
  108. package/src/camera/pickCaptureFormat.ts +130 -0
  109. package/src/camera/stitchDebugInfo.ts +71 -0
  110. package/src/camera/usePanMotion.ts +667 -0
  111. package/src/index.ts +66 -3
  112. package/src/stitching/computeInscribedRect.ts +81 -0
  113. package/src/stitching/cropQuad.ts +167 -0
  114. package/src/stitching/incremental.ts +45 -0
  115. package/cpp/tests/CMakeLists.txt +0 -104
  116. package/cpp/tests/README.md +0 -86
  117. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  118. package/cpp/tests/pose_test.cpp +0 -74
  119. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  120. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  121. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  122. package/cpp/tests/warp_guard_test.cpp +0 -48
  123. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  124. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  125. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  126. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  127. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  128. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  129. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  130. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  131. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  132. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  133. package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
@@ -32,9 +32,9 @@ export type CaptureStatusPhase = 'idle' | 'recording' | 'stitching';
32
32
  export interface CaptureStatusOverlayProps {
33
33
  /**
34
34
  * Current phase. `idle` renders nothing. `recording` shows the
35
- * REC banner + red glowing border. `stitching` swaps to a neutral
36
- * "Stitching..." banner with no border (recording is over; UI
37
- * cue should de-escalate).
35
+ * REC banner + glowing border (GREEN normally, RED when `tooFast`).
36
+ * `stitching` swaps to a neutral "Stitching..." banner with no border
37
+ * (recording is over; UI cue should de-escalate).
38
38
  */
39
39
  phase: CaptureStatusPhase;
40
40
  /**
@@ -44,6 +44,13 @@ export interface CaptureStatusOverlayProps {
44
44
  * type.
45
45
  */
46
46
  recordingMessage?: string;
47
+ /**
48
+ * v0.16 — speed feedback. When `false` (default) the recording banner +
49
+ * border are GREEN ("your pace is fine"); when `true` they turn RED to
50
+ * signal the pan is too fast. This consolidates the old always-red border
51
+ * + separate amber "slow down" pill into one calm-by-default cue.
52
+ */
53
+ tooFast?: boolean;
47
54
  /**
48
55
  * Optional override for the stitching-phase message. Defaults to
49
56
  * "Stitching panorama…".
@@ -71,5 +78,5 @@ export interface CaptureStatusOverlayProps {
71
78
  /** Outer style passthrough. */
72
79
  style?: StyleProp<ViewStyle>;
73
80
  }
74
- export declare function CaptureStatusOverlay({ phase, recordingMessage, stitchingMessage, countdownMs, recordingStartedAt, topInset, style, }: CaptureStatusOverlayProps): React.JSX.Element | null;
81
+ export declare function CaptureStatusOverlay({ phase, recordingMessage, tooFast, stitchingMessage, countdownMs, recordingStartedAt, topInset, style, }: CaptureStatusOverlayProps): React.JSX.Element | null;
75
82
  //# sourceMappingURL=CaptureStatusOverlay.d.ts.map
@@ -66,7 +66,7 @@ exports.CaptureStatusOverlay = CaptureStatusOverlay;
66
66
  const react_1 = __importStar(require("react"));
67
67
  const react_native_1 = require("react-native");
68
68
  const useDeviceOrientation_1 = require("./useDeviceOrientation");
69
- function CaptureStatusOverlay({ phase, recordingMessage = 'Hold steady — pan slowly', stitchingMessage = 'Stitching panorama…', countdownMs, recordingStartedAt, topInset = 0, style, }) {
69
+ function CaptureStatusOverlay({ phase, recordingMessage = 'Hold steady — pan slowly', tooFast = false, stitchingMessage = 'Stitching panorama…', countdownMs, recordingStartedAt, topInset = 0, style, }) {
70
70
  // Countdown ticker — re-renders every 250 ms while recording so
71
71
  // the "REC 4s left" text stays current without flooding render
72
72
  // calls. Disabled (no interval) when not in recording phase or
@@ -161,10 +161,15 @@ function CaptureStatusOverlay({ phase, recordingMessage = 'Hold steady — pan s
161
161
  // the underlying camera / shutter / preview. The banner and
162
162
  // border are read-only.
163
163
  pointerEvents: "box-none", style: [react_native_1.StyleSheet.absoluteFill, style], accessibilityLiveRegion: "polite" },
164
- showBorder ? (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: styles.recordBorder })) : null,
164
+ showBorder ? (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
165
+ styles.recordBorder,
166
+ tooFast ? styles.recordBorderTooFast : styles.recordBorderOk,
167
+ ] })) : null,
165
168
  react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
166
169
  styles.banner,
167
- phase === 'recording' ? styles.bannerRecording : styles.bannerStitching,
170
+ phase === 'recording'
171
+ ? (tooFast ? styles.bannerTooFast : styles.bannerRecording)
172
+ : styles.bannerStitching,
168
173
  bannerOrientationStyle,
169
174
  ] },
170
175
  phase === 'recording' ? (react_1.default.createElement(react_native_1.Animated.View, { style: [
@@ -287,7 +292,12 @@ const styles = react_native_1.StyleSheet.create({
287
292
  minHeight: 36,
288
293
  },
289
294
  bannerRecording: {
290
- backgroundColor: 'rgba(255,59,48,0.92)',
295
+ // Green by default — "you're recording and your pace is fine".
296
+ backgroundColor: 'rgba(52,199,89,0.92)',
297
+ },
298
+ bannerTooFast: {
299
+ // Red only when the pan is too fast (consolidates the old amber pill).
300
+ backgroundColor: 'rgba(255,59,48,0.94)',
291
301
  },
292
302
  bannerStitching: {
293
303
  // Neutral grey while we wait for the stitcher; communicates
@@ -320,7 +330,14 @@ const styles = react_native_1.StyleSheet.create({
320
330
  recordBorder: {
321
331
  ...react_native_1.StyleSheet.absoluteFillObject,
322
332
  borderWidth: 4,
323
- borderColor: 'rgba(255,59,48,0.9)',
333
+ },
334
+ recordBorderOk: {
335
+ // Green by default (calm — the pan pace is fine).
336
+ borderColor: 'rgba(52,199,89,0.9)',
337
+ },
338
+ recordBorderTooFast: {
339
+ // Red only when too fast.
340
+ borderColor: 'rgba(255,59,48,0.95)',
324
341
  },
325
342
  });
326
343
  //# sourceMappingURL=CaptureStatusOverlay.js.map
@@ -74,6 +74,7 @@ exports.CaptureThumbnailStrip = CaptureThumbnailStrip;
74
74
  const react_1 = __importStar(require("react"));
75
75
  const react_native_1 = require("react-native");
76
76
  const CapturePreview_1 = require("./CapturePreview");
77
+ const displayDecodeImageProps_1 = require("./displayDecodeImageProps");
77
78
  /// Fixed thumbnail height — width varies with aspect ratio.
78
79
  const THUMB_HEIGHT = 60;
79
80
  /// Width clamps protect the strip from extreme aspect ratios (very
@@ -136,7 +137,7 @@ function CaptureThumbnailStrip({ items, minPhotos, maxPhotos, backgroundColor =
136
137
  vertical ? styles.thumbWrapperVertical : styles.thumbWrapperHorizontal,
137
138
  { width: thumbWidth(item), height: THUMB_HEIGHT },
138
139
  ] },
139
- react_1.default.createElement(react_native_1.Image, { source: { uri: item.uri }, style: [styles.thumbImage, contentRotation], resizeMode: "cover" }))) }),
140
+ react_1.default.createElement(react_native_1.Image, { source: { uri: item.uri }, style: [styles.thumbImage, contentRotation], resizeMode: "cover", ...displayDecodeImageProps_1.DISPLAY_DECODE_IMAGE_PROPS }))) }),
140
141
  countLine,
141
142
  react_1.default.createElement(CapturePreview_1.CapturePreview, { visible: previewItem !== null, imageUri: previewItem?.uri ?? '', imageWidth: previewItem?.width, imageHeight: previewItem?.height, onClose: closePreview })));
142
143
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * LateralMotionModal — informational popup shown when the SDK stops an
3
+ * in-progress capture because the user panned sideways (cross-axis /
4
+ * lateral drift) instead of holding the single sweep direction
5
+ * (Mode A: top→bottom; Mode B: left→right).
6
+ *
7
+ * ## When this modal appears
8
+ *
9
+ * This is the item-6 sibling of `OrientationDriftModal` — the "you
10
+ * moved sideways" variant. By the time the modal renders, the capture
11
+ * has ALREADY been finalized by the parent `<Camera>` (the lateral-stop
12
+ * effect calls the engine's `stop()` and keeps whatever was captured up
13
+ * to that point — there is no malformed-output risk, so the copy says
14
+ * "we stitched what you captured"). The modal exists solely to explain
15
+ * to the user what happened and how to avoid it next time; the single
16
+ * dismiss button just clears the latched lateral-stop state so the next
17
+ * capture can start fresh.
18
+ *
19
+ * ## Layer-2 host usage
20
+ *
21
+ * Hosts using `CameraView` directly (rather than the flagship
22
+ * `<Camera>`) can compose this modal with their own lateral-drift
23
+ * detector for the same finalize-and-explain UX:
24
+ *
25
+ * const lateral = useLateralDrift(captureActive);
26
+ * useEffect(() => {
27
+ * if (lateral.stopped) {
28
+ * // host finalizes capture (engine stop + keep captured output)
29
+ * finalizeCapture();
30
+ * }
31
+ * }, [lateral.stopped]);
32
+ *
33
+ * return <>
34
+ * <CameraView ... />
35
+ * <LateralMotionModal
36
+ * visible={lateral.stopped}
37
+ * onDismiss={dismissLateralModal}
38
+ * />
39
+ * </>;
40
+ *
41
+ * ## Copy
42
+ *
43
+ * `title` / `body` / `dismissLabel` default to the centralised
44
+ * `DEFAULT_GUIDANCE_COPY.lateralStop*` strings so hosts can localise or
45
+ * re-word every guidance message in one place via the `guidanceCopy`
46
+ * `<Camera>` prop; pass explicit props to override per-instance.
47
+ *
48
+ * ## Accessibility
49
+ *
50
+ * Modal `role` defaults to RN's native dialog handling. The dismiss
51
+ * button carries an `accessibilityRole='button'` + label. Body text
52
+ * uses `accessibilityRole='text'` so the guidance is read by VoiceOver
53
+ * / TalkBack.
54
+ */
55
+ import React from 'react';
56
+ export interface LateralMotionModalProps {
57
+ /**
58
+ * Show / hide. In the `<Camera>` integration this is driven by the
59
+ * latched lateral-stop flag (capture already finalized when true).
60
+ */
61
+ visible: boolean;
62
+ /**
63
+ * Popup title. Defaults to
64
+ * `DEFAULT_GUIDANCE_COPY.lateralStopTitle`.
65
+ */
66
+ title?: string;
67
+ /**
68
+ * Popup body / guidance copy. Defaults to
69
+ * `DEFAULT_GUIDANCE_COPY.lateralStopBody`.
70
+ */
71
+ body?: string;
72
+ /**
73
+ * Dismiss button label. Defaults to
74
+ * `DEFAULT_GUIDANCE_COPY.lateralStopDismiss`.
75
+ */
76
+ dismissLabel?: string;
77
+ /**
78
+ * Tapped when the user dismisses. By the time the modal renders the
79
+ * capture is already finalized; this callback exists only to clear
80
+ * the latched lateral-stop state so the next capture can start fresh.
81
+ */
82
+ onDismiss: () => void;
83
+ }
84
+ export declare function LateralMotionModal(props: LateralMotionModalProps): React.JSX.Element;
85
+ //# sourceMappingURL=LateralMotionModal.d.ts.map
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * LateralMotionModal — informational popup shown when the SDK stops an
5
+ * in-progress capture because the user panned sideways (cross-axis /
6
+ * lateral drift) instead of holding the single sweep direction
7
+ * (Mode A: top→bottom; Mode B: left→right).
8
+ *
9
+ * ## When this modal appears
10
+ *
11
+ * This is the item-6 sibling of `OrientationDriftModal` — the "you
12
+ * moved sideways" variant. By the time the modal renders, the capture
13
+ * has ALREADY been finalized by the parent `<Camera>` (the lateral-stop
14
+ * effect calls the engine's `stop()` and keeps whatever was captured up
15
+ * to that point — there is no malformed-output risk, so the copy says
16
+ * "we stitched what you captured"). The modal exists solely to explain
17
+ * to the user what happened and how to avoid it next time; the single
18
+ * dismiss button just clears the latched lateral-stop state so the next
19
+ * capture can start fresh.
20
+ *
21
+ * ## Layer-2 host usage
22
+ *
23
+ * Hosts using `CameraView` directly (rather than the flagship
24
+ * `<Camera>`) can compose this modal with their own lateral-drift
25
+ * detector for the same finalize-and-explain UX:
26
+ *
27
+ * const lateral = useLateralDrift(captureActive);
28
+ * useEffect(() => {
29
+ * if (lateral.stopped) {
30
+ * // host finalizes capture (engine stop + keep captured output)
31
+ * finalizeCapture();
32
+ * }
33
+ * }, [lateral.stopped]);
34
+ *
35
+ * return <>
36
+ * <CameraView ... />
37
+ * <LateralMotionModal
38
+ * visible={lateral.stopped}
39
+ * onDismiss={dismissLateralModal}
40
+ * />
41
+ * </>;
42
+ *
43
+ * ## Copy
44
+ *
45
+ * `title` / `body` / `dismissLabel` default to the centralised
46
+ * `DEFAULT_GUIDANCE_COPY.lateralStop*` strings so hosts can localise or
47
+ * re-word every guidance message in one place via the `guidanceCopy`
48
+ * `<Camera>` prop; pass explicit props to override per-instance.
49
+ *
50
+ * ## Accessibility
51
+ *
52
+ * Modal `role` defaults to RN's native dialog handling. The dismiss
53
+ * button carries an `accessibilityRole='button'` + label. Body text
54
+ * uses `accessibilityRole='text'` so the guidance is read by VoiceOver
55
+ * / TalkBack.
56
+ */
57
+ var __importDefault = (this && this.__importDefault) || function (mod) {
58
+ return (mod && mod.__esModule) ? mod : { "default": mod };
59
+ };
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.LateralMotionModal = LateralMotionModal;
62
+ const react_1 = __importDefault(require("react"));
63
+ const react_native_1 = require("react-native");
64
+ const cameraGuidanceCopy_1 = require("./cameraGuidanceCopy");
65
+ function LateralMotionModal(props) {
66
+ const { visible, title = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.lateralStopTitle, body = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.lateralStopBody, dismissLabel = cameraGuidanceCopy_1.DEFAULT_GUIDANCE_COPY.lateralStopDismiss, onDismiss, } = props;
67
+ return (react_1.default.createElement(react_native_1.Modal, { visible: visible, transparent: true, animationType: "fade", onRequestClose: onDismiss, accessibilityLabel: "Capture finalized \u2014 moved sideways",
68
+ // v0.12.0 — see OrientationDriftModal / PanoramaSettingsModal for
69
+ // the same prop's rationale. Declaring all orientations prevents
70
+ // iOS from force-rotating the window to portrait when this modal
71
+ // opens mid-rotation, which would otherwise leave the underlying
72
+ // <Camera>'s ARSession in a stale-orientation state on dismiss.
73
+ supportedOrientations: [
74
+ 'portrait',
75
+ 'portrait-upside-down',
76
+ 'landscape-left',
77
+ 'landscape-right',
78
+ ] },
79
+ react_1.default.createElement(react_native_1.View, { style: styles.backdrop },
80
+ react_1.default.createElement(react_native_1.View, { style: styles.card },
81
+ react_1.default.createElement(react_native_1.Text, { style: styles.title, accessibilityRole: "header" }, title),
82
+ react_1.default.createElement(react_native_1.Text, { style: styles.body, accessibilityRole: "text" }, body),
83
+ react_1.default.createElement(react_native_1.Pressable, { style: ({ pressed }) => [
84
+ styles.button,
85
+ pressed && styles.buttonPressed,
86
+ ], onPress: onDismiss, accessibilityRole: "button", accessibilityLabel: dismissLabel },
87
+ react_1.default.createElement(react_native_1.Text, { style: styles.buttonLabel }, dismissLabel))))));
88
+ }
89
+ const styles = react_native_1.StyleSheet.create({
90
+ backdrop: {
91
+ flex: 1,
92
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ paddingHorizontal: 32,
96
+ },
97
+ card: {
98
+ backgroundColor: '#1c1c1e',
99
+ borderRadius: 14,
100
+ paddingHorizontal: 20,
101
+ paddingVertical: 24,
102
+ width: '100%',
103
+ maxWidth: 340,
104
+ },
105
+ title: {
106
+ color: '#fff',
107
+ fontSize: 18,
108
+ fontWeight: '600',
109
+ marginBottom: 12,
110
+ textAlign: 'center',
111
+ },
112
+ body: {
113
+ color: '#e5e5ea',
114
+ fontSize: 15,
115
+ lineHeight: 21,
116
+ textAlign: 'center',
117
+ marginBottom: 20,
118
+ },
119
+ button: {
120
+ backgroundColor: '#0a84ff',
121
+ borderRadius: 10,
122
+ paddingVertical: 12,
123
+ alignItems: 'center',
124
+ },
125
+ buttonPressed: {
126
+ backgroundColor: '#0860c0',
127
+ },
128
+ buttonLabel: {
129
+ color: '#fff',
130
+ fontSize: 17,
131
+ fontWeight: '600',
132
+ },
133
+ });
134
+ //# sourceMappingURL=LateralMotionModal.js.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * PanHowToOverlay — the "how to pan" coach-mark (guidance item 3).
3
+ *
4
+ * Shown briefly at the START of a capture to teach the panning
5
+ * gesture before the live pan-speed pill (`PanoramaGuidance`) takes
6
+ * over. It pairs the code-drawn `PanPhoneGraphic` (white phone +
7
+ * sweeping amber band) with a code-built bouncing arrow so the
8
+ * direction reads instantly without any copy.
9
+ *
10
+ * ┌──────────────────────────────────────────────────────────┐
11
+ * │ │
12
+ * │ ┌───────────────┐ │
13
+ * │ │ PanPhone │ (240px graphic, the │
14
+ * │ │ Graphic │ white phone + │
15
+ * │ └───────────────┘ amber sweep) │
16
+ * │ ▼ ← amber triangle │
17
+ * │ ▼ bouncing ~12px along the │
18
+ * │ pan axis, back and forth │
19
+ * └──────────────────────────────────────────────────────────┘
20
+ *
21
+ * Direction follows the capture mode (derived from the physical
22
+ * device orientation, sensor-based — works under portrait-lock):
23
+ *
24
+ * Mode A — LANDSCAPE → pan TOP → BOTTOM → arrow points DOWN.
25
+ * Mode B — PORTRAIT → pan LEFT → RIGHT → arrow points RIGHT.
26
+ *
27
+ * Both `landscape-left` and `landscape-right` are valid Mode A.
28
+ *
29
+ * ## Visibility & timing
30
+ *
31
+ * This component is intentionally pure-presentational: the PARENT
32
+ * owns `visible` and the brief auto-fade lifecycle (mount → show →
33
+ * dismiss once recording is under way). We never self-time;
34
+ * `visible === false` renders `null` so the host can mount us
35
+ * unconditionally without layout shift.
36
+ *
37
+ * ## Upright under portrait-lock
38
+ *
39
+ * The app layout is typically portrait-locked, so when the user
40
+ * holds the device in landscape (Mode A) the JS framebuffer is NOT
41
+ * rotated. We counter-rotate the whole coach-mark with
42
+ * `useContentRotation()` (same hook the bottom controls use) so the
43
+ * graphic and arrow read upright relative to gravity. The arrow's
44
+ * bounce axis and triangle point are expressed in that upright frame
45
+ * — i.e. the user's view — so "down" / "right" mean what the user
46
+ * sees, not the layout's raw axes.
47
+ *
48
+ * ## No SVG / no extra deps
49
+ *
50
+ * The arrow is a pure CSS border-width triangle (a zero-size View
51
+ * whose thick coloured border on one edge + transparent borders on
52
+ * the adjacent edges read as a filled triangle). Bounce is a single
53
+ * `Animated.loop` on the native driver — cheap, and only running
54
+ * while `visible`.
55
+ */
56
+ import React from 'react';
57
+ import { type StyleProp, type ViewStyle } from 'react-native';
58
+ import { type DeviceOrientation } from './useDeviceOrientation';
59
+ export interface PanHowToOverlayProps {
60
+ /**
61
+ * Show / hide. `false` renders `null`. The host owns the brief
62
+ * auto-fade lifecycle — this component never self-times.
63
+ */
64
+ visible: boolean;
65
+ /**
66
+ * Physical device orientation (sensor-based, from
67
+ * `useDeviceOrientation`). Selects the pan mode → arrow
68
+ * direction: landscape-* → DOWN (Mode A), portrait-* → RIGHT
69
+ * (Mode B).
70
+ */
71
+ orientation: DeviceOrientation;
72
+ /** Outer style passthrough (positioning / opacity from the host). */
73
+ style?: StyleProp<ViewStyle>;
74
+ }
75
+ export declare function PanHowToOverlay({ visible, orientation, style, }: PanHowToOverlayProps): React.JSX.Element | null;
76
+ //# sourceMappingURL=PanHowToOverlay.d.ts.map
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * PanHowToOverlay — the "how to pan" coach-mark (guidance item 3).
5
+ *
6
+ * Shown briefly at the START of a capture to teach the panning
7
+ * gesture before the live pan-speed pill (`PanoramaGuidance`) takes
8
+ * over. It pairs the code-drawn `PanPhoneGraphic` (white phone +
9
+ * sweeping amber band) with a code-built bouncing arrow so the
10
+ * direction reads instantly without any copy.
11
+ *
12
+ * ┌──────────────────────────────────────────────────────────┐
13
+ * │ │
14
+ * │ ┌───────────────┐ │
15
+ * │ │ PanPhone │ (240px graphic, the │
16
+ * │ │ Graphic │ white phone + │
17
+ * │ └───────────────┘ amber sweep) │
18
+ * │ ▼ ← amber triangle │
19
+ * │ ▼ bouncing ~12px along the │
20
+ * │ pan axis, back and forth │
21
+ * └──────────────────────────────────────────────────────────┘
22
+ *
23
+ * Direction follows the capture mode (derived from the physical
24
+ * device orientation, sensor-based — works under portrait-lock):
25
+ *
26
+ * Mode A — LANDSCAPE → pan TOP → BOTTOM → arrow points DOWN.
27
+ * Mode B — PORTRAIT → pan LEFT → RIGHT → arrow points RIGHT.
28
+ *
29
+ * Both `landscape-left` and `landscape-right` are valid Mode A.
30
+ *
31
+ * ## Visibility & timing
32
+ *
33
+ * This component is intentionally pure-presentational: the PARENT
34
+ * owns `visible` and the brief auto-fade lifecycle (mount → show →
35
+ * dismiss once recording is under way). We never self-time;
36
+ * `visible === false` renders `null` so the host can mount us
37
+ * unconditionally without layout shift.
38
+ *
39
+ * ## Upright under portrait-lock
40
+ *
41
+ * The app layout is typically portrait-locked, so when the user
42
+ * holds the device in landscape (Mode A) the JS framebuffer is NOT
43
+ * rotated. We counter-rotate the whole coach-mark with
44
+ * `useContentRotation()` (same hook the bottom controls use) so the
45
+ * graphic and arrow read upright relative to gravity. The arrow's
46
+ * bounce axis and triangle point are expressed in that upright frame
47
+ * — i.e. the user's view — so "down" / "right" mean what the user
48
+ * sees, not the layout's raw axes.
49
+ *
50
+ * ## No SVG / no extra deps
51
+ *
52
+ * The arrow is a pure CSS border-width triangle (a zero-size View
53
+ * whose thick coloured border on one edge + transparent borders on
54
+ * the adjacent edges read as a filled triangle). Bounce is a single
55
+ * `Animated.loop` on the native driver — cheap, and only running
56
+ * while `visible`.
57
+ */
58
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
59
+ if (k2 === undefined) k2 = k;
60
+ var desc = Object.getOwnPropertyDescriptor(m, k);
61
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
62
+ desc = { enumerable: true, get: function() { return m[k]; } };
63
+ }
64
+ Object.defineProperty(o, k2, desc);
65
+ }) : (function(o, m, k, k2) {
66
+ if (k2 === undefined) k2 = k;
67
+ o[k2] = m[k];
68
+ }));
69
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
70
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
71
+ }) : function(o, v) {
72
+ o["default"] = v;
73
+ });
74
+ var __importStar = (this && this.__importStar) || (function () {
75
+ var ownKeys = function(o) {
76
+ ownKeys = Object.getOwnPropertyNames || function (o) {
77
+ var ar = [];
78
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
79
+ return ar;
80
+ };
81
+ return ownKeys(o);
82
+ };
83
+ return function (mod) {
84
+ if (mod && mod.__esModule) return mod;
85
+ var result = {};
86
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
87
+ __setModuleDefault(result, mod);
88
+ return result;
89
+ };
90
+ })();
91
+ Object.defineProperty(exports, "__esModule", { value: true });
92
+ exports.PanHowToOverlay = PanHowToOverlay;
93
+ const react_1 = __importStar(require("react"));
94
+ const react_native_1 = require("react-native");
95
+ const guidanceGraphics_1 = require("./guidanceGraphics");
96
+ const guidanceTokens_1 = require("./guidanceTokens");
97
+ const useContentRotation_1 = require("./useContentRotation");
98
+ /** Distance (px) the arrow travels along the pan axis each bounce. */
99
+ const BOUNCE_DISTANCE = 12;
100
+ /** Half-period of the bounce (out, then back) — ~700 ms each leg. */
101
+ const BOUNCE_DURATION_MS = 700;
102
+ /** Visual size of the CSS-triangle arrow (base width / height in px). */
103
+ const ARROW_SIZE = 18;
104
+ /**
105
+ * Map a physical orientation to the pan direction the user should
106
+ * sweep. Mode A (either landscape) pans top→bottom (DOWN); Mode B
107
+ * (either portrait variant) pans left→right (RIGHT). Directions are
108
+ * in the user's upright view — the content wrapper is counter-rotated
109
+ * so these read correctly under portrait-lock.
110
+ */
111
+ function directionForOrientation(orientation) {
112
+ switch (orientation) {
113
+ case 'landscape-left':
114
+ case 'landscape-right':
115
+ return 'down';
116
+ case 'portrait':
117
+ case 'portrait-upside-down':
118
+ default:
119
+ return 'right';
120
+ }
121
+ }
122
+ function PanHowToOverlay({ visible, orientation, style, }) {
123
+ // Counter-rotation so the GIF + arrow read upright relative to
124
+ // gravity even when the app is portrait-locked and the device is
125
+ // held in landscape (Mode A). Always called so hook order is
126
+ // stable across the `visible` toggle.
127
+ const contentRotation = (0, useContentRotation_1.useContentRotation)();
128
+ // Single Animated value driving the bounce, 0 → 1 → 0. Native
129
+ // driver (transform-only), so the loop runs off the JS thread.
130
+ const bounce = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
131
+ const direction = directionForOrientation(orientation);
132
+ (0, react_1.useEffect)(() => {
133
+ if (!visible) {
134
+ bounce.setValue(0);
135
+ return;
136
+ }
137
+ const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
138
+ react_native_1.Animated.timing(bounce, {
139
+ toValue: 1,
140
+ duration: BOUNCE_DURATION_MS,
141
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
142
+ useNativeDriver: true,
143
+ }),
144
+ react_native_1.Animated.timing(bounce, {
145
+ toValue: 0,
146
+ duration: BOUNCE_DURATION_MS,
147
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
148
+ useNativeDriver: true,
149
+ }),
150
+ ]));
151
+ loop.start();
152
+ return () => loop.stop();
153
+ }, [visible, bounce]);
154
+ // Translate 0→BOUNCE_DISTANCE along the pan axis. In the upright
155
+ // (counter-rotated) frame, "down" moves +Y and "right" moves +X.
156
+ const travel = bounce.interpolate({
157
+ inputRange: [0, 1],
158
+ outputRange: [0, BOUNCE_DISTANCE],
159
+ });
160
+ const arrowTransform = (0, react_1.useMemo)(() => direction === 'down'
161
+ ? [{ translateY: travel }]
162
+ : [{ translateX: travel }], [direction, travel]);
163
+ if (!visible)
164
+ return null;
165
+ return (react_1.default.createElement(react_native_1.View
166
+ // box-none on the root: never intercept taps anywhere on the
167
+ // full-screen layer. The inner content is also non-interactive.
168
+ , {
169
+ // box-none on the root: never intercept taps anywhere on the
170
+ // full-screen layer. The inner content is also non-interactive.
171
+ pointerEvents: "none", style: [styles.root, style] },
172
+ react_1.default.createElement(react_native_1.View, { style: [styles.content, contentRotation] },
173
+ react_1.default.createElement(guidanceGraphics_1.PanPhoneGraphic, { direction: direction, playing: visible }),
174
+ react_1.default.createElement(react_native_1.Animated.View, { style: [
175
+ styles.arrow,
176
+ direction === 'down' ? styles.arrowDown : styles.arrowRight,
177
+ { transform: arrowTransform },
178
+ ] }))));
179
+ }
180
+ const styles = react_native_1.StyleSheet.create({
181
+ root: {
182
+ ...react_native_1.StyleSheet.absoluteFillObject,
183
+ alignItems: 'center',
184
+ justifyContent: 'center',
185
+ },
186
+ content: {
187
+ alignItems: 'center',
188
+ justifyContent: 'center',
189
+ },
190
+ // CSS-triangle base: a zero-size box whose borders are coloured on
191
+ // one edge and transparent on the two adjacent edges, producing a
192
+ // filled triangle pointing away from the coloured edge. The
193
+ // direction-specific styles below set which edge is amber.
194
+ arrow: {
195
+ width: 0,
196
+ height: 0,
197
+ backgroundColor: 'transparent',
198
+ borderStyle: 'solid',
199
+ marginTop: 8,
200
+ },
201
+ // Triangle pointing DOWN (Mode A): left + right borders transparent,
202
+ // TOP border amber → apex at the bottom.
203
+ arrowDown: {
204
+ borderLeftWidth: ARROW_SIZE / 2,
205
+ borderRightWidth: ARROW_SIZE / 2,
206
+ borderTopWidth: ARROW_SIZE,
207
+ borderLeftColor: 'transparent',
208
+ borderRightColor: 'transparent',
209
+ borderTopColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
210
+ },
211
+ // Triangle pointing RIGHT (Mode B): top + bottom borders
212
+ // transparent, LEFT border amber → apex on the right.
213
+ arrowRight: {
214
+ borderTopWidth: ARROW_SIZE / 2,
215
+ borderBottomWidth: ARROW_SIZE / 2,
216
+ borderLeftWidth: ARROW_SIZE,
217
+ borderTopColor: 'transparent',
218
+ borderBottomColor: 'transparent',
219
+ borderLeftColor: guidanceTokens_1.GUIDANCE_TOKENS.amber,
220
+ },
221
+ });
222
+ //# sourceMappingURL=PanHowToOverlay.js.map
@@ -168,11 +168,11 @@ export interface FrameSelectionSettings {
168
168
  maxKeyframes: number;
169
169
  /**
170
170
  * Required NEW-content fraction (0..1) for a candidate frame to
171
- * be accepted. Default 0.20 = 20% novel content per accept.
172
- * Lower = more frames accepted, larger panoramas. Higher = fewer
173
- * frames, faster captures but more conservative about coverage.
174
- * Clamped to `[0.10, 0.80]` natively
175
- * (`IncrementalStitcher.swift:962`).
171
+ * be accepted. Default 0.15 = 15% novel content per accept (v0.16;
172
+ * was 0.20). Lower = more frames accepted, denser overlap, more
173
+ * robust registration. Higher = fewer frames, faster captures but
174
+ * more conservative about coverage. Clamped to `[0.10, 0.80]`
175
+ * natively (`IncrementalStitcher.swift:962`) — 0.10 is the floor.
176
176
  */
177
177
  overlapThreshold: number;
178
178
  /**
@@ -182,7 +182,9 @@ export interface FrameSelectionSettings {
182
182
  * overlap threshold wasn't met — so a slow or static pan never goes
183
183
  * longer than this without a keyframe. Counts toward `maxKeyframes`
184
184
  * (the cap still finalises the capture). `0` disables it. Default
185
- * `2000` (2 s). Maps to the native gate's `setMaxKeyframeIntervalMs`.
185
+ * `1500` (1.5 s) with `maxKeyframes` 8 this bounds a static/slow
186
+ * capture to ~8×1.5 ≈ 12 s before the keyframe-count auto-finalize.
187
+ * Maps to the native gate's `setMaxKeyframeIntervalMs`.
186
188
  */
187
189
  maxKeyframeIntervalMs: number;
188
190
  /**