react-native-image-stitcher 0.15.2 → 0.16.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.
Files changed (146) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/README.md +131 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
  4. package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
  5. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  6. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
  7. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  8. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
  9. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  10. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  11. package/cpp/crop_quad.cpp +162 -0
  12. package/cpp/crop_quad.hpp +163 -0
  13. package/cpp/keyframe_gate.cpp +54 -15
  14. package/cpp/keyframe_gate.hpp +33 -0
  15. package/cpp/stitcher.cpp +1122 -132
  16. package/cpp/stitcher.hpp +62 -0
  17. package/cpp/warp_guard.hpp +212 -0
  18. package/dist/camera/Camera.d.ts +209 -12
  19. package/dist/camera/Camera.js +575 -36
  20. package/dist/camera/CameraView.js +35 -16
  21. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  22. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  23. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  24. package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
  25. package/dist/camera/CaptureMemoryPill.d.ts +24 -8
  26. package/dist/camera/CaptureMemoryPill.js +37 -12
  27. package/dist/camera/CapturePreview.js +2 -1
  28. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  29. package/dist/camera/CaptureStatusOverlay.js +22 -5
  30. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  31. package/dist/camera/LateralMotionModal.d.ts +85 -0
  32. package/dist/camera/LateralMotionModal.js +134 -0
  33. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  34. package/dist/camera/PanHowToOverlay.js +222 -0
  35. package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
  36. package/dist/camera/PanoramaBandOverlay.js +9 -3
  37. package/dist/camera/PanoramaSettings.d.ts +8 -6
  38. package/dist/camera/PanoramaSettings.js +19 -1
  39. package/dist/camera/PanoramaSettingsModal.js +4 -4
  40. package/dist/camera/RectCropPreview.d.ts +135 -0
  41. package/dist/camera/RectCropPreview.js +370 -0
  42. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  43. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  44. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  45. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  46. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  47. package/dist/camera/cameraErrorMessages.js +26 -10
  48. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  49. package/dist/camera/cameraGuidanceCopy.js +80 -0
  50. package/dist/camera/captureCountdown.d.ts +52 -0
  51. package/dist/camera/captureCountdown.js +76 -0
  52. package/dist/camera/captureWarnings.d.ts +90 -0
  53. package/dist/camera/captureWarnings.js +108 -0
  54. package/dist/camera/classifyStitchError.d.ts +30 -0
  55. package/dist/camera/classifyStitchError.js +42 -0
  56. package/dist/camera/cropGeometry.d.ts +136 -0
  57. package/dist/camera/cropGeometry.js +223 -0
  58. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  59. package/dist/camera/displayDecodeImageProps.js +29 -0
  60. package/dist/camera/guidanceGraphics.d.ts +58 -0
  61. package/dist/camera/guidanceGraphics.js +280 -0
  62. package/dist/camera/guidanceTokens.d.ts +54 -0
  63. package/dist/camera/guidanceTokens.js +58 -0
  64. package/dist/camera/panModeGate.d.ts +54 -0
  65. package/dist/camera/panModeGate.js +62 -0
  66. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  67. package/dist/camera/pickCaptureFormat.js +85 -0
  68. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  69. package/dist/camera/stitchDebugInfo.js +55 -0
  70. package/dist/camera/usePanMotion.d.ts +250 -0
  71. package/dist/camera/usePanMotion.js +451 -0
  72. package/dist/index.d.ts +24 -3
  73. package/dist/index.js +33 -2
  74. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  75. package/dist/stitching/computeInscribedRect.js +55 -0
  76. package/dist/stitching/cropQuad.d.ts +78 -0
  77. package/dist/stitching/cropQuad.js +116 -0
  78. package/dist/stitching/incremental.d.ts +74 -0
  79. package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
  80. package/dist/stitching/useIncrementalStitcher.js +7 -1
  81. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
  82. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
  83. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
  84. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  85. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  86. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  87. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
  88. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  89. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  90. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  91. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  92. package/package.json +5 -1
  93. package/src/camera/Camera.tsx +945 -47
  94. package/src/camera/CameraView.tsx +48 -16
  95. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  96. package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
  97. package/src/camera/CaptureMemoryPill.tsx +50 -12
  98. package/src/camera/CapturePreview.tsx +5 -0
  99. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  100. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  101. package/src/camera/LateralMotionModal.tsx +199 -0
  102. package/src/camera/PanHowToOverlay.tsx +246 -0
  103. package/src/camera/PanoramaBandOverlay.tsx +9 -1
  104. package/src/camera/PanoramaSettings.ts +27 -7
  105. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  106. package/src/camera/RectCropPreview.tsx +638 -0
  107. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  108. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  109. package/src/camera/cameraErrorMessages.ts +39 -2
  110. package/src/camera/cameraGuidanceCopy.ts +145 -0
  111. package/src/camera/captureCountdown.ts +83 -0
  112. package/src/camera/captureWarnings.ts +190 -0
  113. package/src/camera/classifyStitchError.ts +68 -0
  114. package/src/camera/cropGeometry.ts +268 -0
  115. package/src/camera/displayDecodeImageProps.ts +25 -0
  116. package/src/camera/guidanceGraphics.tsx +347 -0
  117. package/src/camera/guidanceTokens.ts +57 -0
  118. package/src/camera/panModeGate.ts +81 -0
  119. package/src/camera/pickCaptureFormat.ts +130 -0
  120. package/src/camera/stitchDebugInfo.ts +71 -0
  121. package/src/camera/usePanMotion.ts +667 -0
  122. package/src/index.ts +66 -3
  123. package/src/stitching/computeInscribedRect.ts +81 -0
  124. package/src/stitching/cropQuad.ts +167 -0
  125. package/src/stitching/incremental.ts +74 -0
  126. package/src/stitching/useIncrementalStitcher.ts +13 -0
  127. package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
  128. package/cpp/tests/CMakeLists.txt +0 -104
  129. package/cpp/tests/README.md +0 -86
  130. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  131. package/cpp/tests/pose_test.cpp +0 -74
  132. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  133. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  134. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  135. package/cpp/tests/warp_guard_test.cpp +0 -48
  136. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  137. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  138. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  139. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  140. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  141. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  142. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  143. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  144. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  145. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  146. package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
@@ -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
@@ -190,6 +190,7 @@ declare function tileRotation(orientation: BandCaptureOrientation, vertical: boo
190
190
  export declare const _bandThumbRotationForTests: typeof bandThumbRotation;
191
191
  /** @internal test-only export — see `tileRotation`. */
192
192
  export declare const _tileRotationForTests: typeof tileRotation;
193
- export declare function PanoramaBandOverlay({ state, frameUris, captureOrientation, vertical, }: PanoramaBandOverlayProps): React.JSX.Element | null;
193
+ declare function PanoramaBandOverlayImpl({ state, frameUris, captureOrientation, vertical, }: PanoramaBandOverlayProps): React.JSX.Element | null;
194
+ export declare const PanoramaBandOverlay: React.MemoExoticComponent<typeof PanoramaBandOverlayImpl>;
194
195
  export {};
195
196
  //# sourceMappingURL=PanoramaBandOverlay.d.ts.map
@@ -92,8 +92,7 @@ var __importStar = (this && this.__importStar) || (function () {
92
92
  };
93
93
  })();
94
94
  Object.defineProperty(exports, "__esModule", { value: true });
95
- exports._tileRotationForTests = exports._bandThumbRotationForTests = void 0;
96
- exports.PanoramaBandOverlay = PanoramaBandOverlay;
95
+ exports.PanoramaBandOverlay = exports._tileRotationForTests = exports._bandThumbRotationForTests = void 0;
97
96
  const react_1 = __importStar(require("react"));
98
97
  const react_native_1 = require("react-native");
99
98
  // ── Layout constants — tuned to read clearly at arm's length ────────
@@ -295,7 +294,7 @@ function layoutFor(orientation, vertical) {
295
294
  arrowGlyph: '→',
296
295
  };
297
296
  }
298
- function PanoramaBandOverlay({ state, frameUris, captureOrientation, vertical = false, }) {
297
+ function PanoramaBandOverlayImpl({ state, frameUris, captureOrientation, vertical = false, }) {
299
298
  // 2026-05-18 (Issue #3 fix) — orientation source priority:
300
299
  // 1. `captureOrientation` prop from the host (4-way; correct
301
300
  // for landscape-left vs landscape-right disambiguation).
@@ -456,6 +455,13 @@ function PanoramaBandOverlay({ state, frameUris, captureOrientation, vertical =
456
455
  react_1.default.createElement(react_native_1.View, { style: styles.arrowTrack },
457
456
  react_1.default.createElement(react_native_1.Text, { style: styles.arrowGlyph }, layout.arrowGlyph))))));
458
457
  }
458
+ // 2026-06-16 (audit #7) — memoized. This is the lone ~6 Hz consumer that mounts
459
+ // in PRODUCTION (the debug pills are settings.debug-gated), and most engine ticks
460
+ // are REJECTED frames that don't change its visible inputs (frameUris /
461
+ // acceptedCount / orientation). React.memo skips the re-render on those, so the
462
+ // ~6×/sec engine emits no longer re-render this overlay's subtree on the hot
463
+ // capture path (battery/heat on long captures).
464
+ exports.PanoramaBandOverlay = react_1.default.memo(PanoramaBandOverlayImpl);
459
465
  const styles = react_native_1.StyleSheet.create({
460
466
  // Properties common to every layout — uniform border-radius so the
461
467
  // band reads as a single capsule regardless of which edge it's
@@ -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
  /**
@@ -77,7 +77,18 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
77
77
  captureSource: 'ar',
78
78
  debug: false,
79
79
  stitcher: {
80
+ // v0.16 — AUTO by default. Reverted from the brief 'panorama' default after
81
+ // on-device comparison (matches the v0.15.2 behaviour, which produced better
82
+ // results for these captures). The auto-resolver now carries the
83
+ // low-rotation guard (rRadians>0.35 && t<0.25 → force PANORAMA), so the old
84
+ // IMU-gravity-leak SCANS misclassification on rotational pans is fixed; auto
85
+ // can again safely pick SCANS (high-level affine) for genuine flat scans.
80
86
  stitchMode: 'auto',
87
+ // v0.16 — PLANE by default. Reverted from 'spherical' after on-device
88
+ // comparison (matches v0.15.2 — flatter, more natural for the common 1x
89
+ // pan). Plane is unbounded, so this re-arms the manual pipeline's dynamic
90
+ // plane→SPHERICAL divergence/quality fallback (it fires only when
91
+ // warperType != 'spherical'), keeping wide/off-axis pans safe.
81
92
  warperType: 'plane',
82
93
  blenderType: 'multiband',
83
94
  seamFinderType: 'graphcut',
@@ -89,9 +100,16 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
89
100
  },
90
101
  frameSelection: {
91
102
  mode: 'flow-based',
103
+ // v0.16 — keyframe gate: a 20% novelty gate, up to 6 frames, plus a 1.5 s
104
+ // time-budget force-accept (so a slow/static pan still lands a keyframe every
105
+ // 1.5 s even when novelty is low). These match the leaner v0.15.2 cadence (6
106
+ // frames / 20% overlap) — fewer, more-novel keyframes = lighter memory + less
107
+ // redundant overlap. With 6 frames this bounds a static/slow capture to
108
+ // ~6×1.5 ≈ 9 s before the keyframe-count auto-finalize. Overlap selectable in
109
+ // the settings panel {10,15,20,30}% (native clamp floor 10%); cap clamps [3,10].
92
110
  maxKeyframes: 6,
93
111
  overlapThreshold: 0.20,
94
- maxKeyframeIntervalMs: 2000,
112
+ maxKeyframeIntervalMs: 1500,
95
113
  flow: exports.DEFAULT_FLOW_GATE_SETTINGS,
96
114
  },
97
115
  };
@@ -182,17 +182,17 @@ function PanoramaSettingsModal({ visible, settings, onChange, onClose, }) {
182
182
  react_1.default.createElement(SectionHeader, { title: "Max keyframes per capture" }),
183
183
  react_1.default.createElement(SegmentedControl, { options: ['3', '4', '5', '6', '8', '10'], value: String(settings.frameSelection.maxKeyframes), onChange: (v) => updateFrameSelection({
184
184
  maxKeyframes: parseInt(v, 10),
185
- }), caption: "Hard cap on accepted keyframes; native clamps to [3, 10]. 6 (default) matches Samsung Pano's behaviour and is the sweet spot for cv::Stitcher BA convergence." }),
185
+ }), caption: "Hard cap on accepted keyframes; native clamps to [3, 10]. 8 (default) is the sweet spot for cv::detail BA convergence while giving the 15%-overlap + 1 s time gate room to land frames." }),
186
186
  react_1.default.createElement(SectionHeader, { title: "Overlap threshold (new content per keyframe)" }),
187
- react_1.default.createElement(SegmentedControl, { options: ['20%', '30%', '40%', '50%', '60%'], value: `${Math.round(settings.frameSelection.overlapThreshold * 100)}%`, onChange: (v) => updateFrameSelection({
187
+ react_1.default.createElement(SegmentedControl, { options: ['10%', '15%', '20%', '30%'], value: `${Math.round(settings.frameSelection.overlapThreshold * 100)}%`, onChange: (v) => updateFrameSelection({
188
188
  overlapThreshold: parseInt(v, 10) / 100,
189
- }), caption: "Required NEW-content fraction. 20% (default): generous, ~5\u20136 keyframes for a 90\u00B0 pan. Native clamps to [10%, 80%]." }),
189
+ }), caption: "Required NEW-content fraction (lower = denser keyframes, more overlap). 15% (default): ~7\u20139 keyframes for a 90\u00B0 pan. 10% is the native clamp floor." }),
190
190
  react_1.default.createElement(SectionHeader, { title: "Keyframe interval (time-budget force-accept)" }),
191
191
  react_1.default.createElement(SegmentedControl, { options: ['off', '1s', '2s', '3s', '5s'], value: settings.frameSelection.maxKeyframeIntervalMs === 0
192
192
  ? 'off'
193
193
  : `${settings.frameSelection.maxKeyframeIntervalMs / 1000}s`, onChange: (v) => updateFrameSelection({
194
194
  maxKeyframeIntervalMs: v === 'off' ? 0 : parseInt(v, 10) * 1000,
195
- }), caption: "Force-accept a keyframe at least this often even if novelty is low, so slow / static pans don't leave gaps. Counts toward the keyframe cap. off = disabled. 2s (default). Applies to AR + non-AR." }),
195
+ }), caption: "Force-accept a keyframe at least this often even if novelty is low, so slow / static pans don't leave gaps. Counts toward the keyframe cap. off = disabled. 1s (default). Applies to AR + non-AR." }),
196
196
  showFlowTunables && (react_1.default.createElement(react_native_1.View, { style: styles.nested },
197
197
  react_1.default.createElement(react_native_1.Text, { style: styles.nestedLabel }, "Flow tuning"),
198
198
  react_1.default.createElement(SectionHeader, { title: "Max corners (Shi-Tomasi)" }),
@@ -0,0 +1,135 @@
1
+ /**
2
+ * RectCropPreview — item-7 of the first-time-user guidance flow: the
3
+ * post-capture crop editor.
4
+ *
5
+ * Shows the full stitched result image (contain-fit, letterboxed) with a
6
+ * 4-corner quad overlay. Each corner is INDEPENDENTLY draggable in
7
+ * on-screen coords via RN-core `PanResponder` (deliberately NO
8
+ * react-native-gesture-handler dependency — this library ships zero extra
9
+ * native deps for guidance). Corner positions are mapped to image-pixel
10
+ * space through the pure `cropGeometry` letterbox transform.
11
+ *
12
+ * ## What it surfaces (and what it does NOT do)
13
+ *
14
+ * This component is presentation + gesture only. On confirm it computes
15
+ * the 4 image-pixel corners and hands them to `onConfirm` — it does NOT
16
+ * call any native crop. The PARENT decides between the cheap axis-aligned
17
+ * `cropToRect` (when the quad is ~rectangular) and the perspective
18
+ * `cropToQuad`, using the `perspective` flag in the result:
19
+ *
20
+ * onConfirm({ quad, perspective: perspectiveCorrect && !isAxisAligned })
21
+ *
22
+ * Promoted + extended from `example/InscribedRectDebug.tsx`, which already
23
+ * did the image-px ↔ on-screen contain-fit mapping, a rect overlay, and
24
+ * the in-place native crop. This version replaces the single computed
25
+ * inscribed rect with a user-draggable free quad and the perspective
26
+ * decision; the letterbox math now lives in the shared `cropGeometry`
27
+ * module. Styling is carried over from InscribedRectDebug.
28
+ *
29
+ * ## Seeding
30
+ *
31
+ * The initial quad comes from `initialRect` (image-pixel coords) when the
32
+ * host passes one — `<Camera>` passes the panorama's MAX-INSCRIBED rectangle
33
+ * (the tightest clean rectangle with no black corners; item 2) so the editor
34
+ * opens on a sensible crop the user drags to taste. With no `initialRect`
35
+ * (native inscribed-rect unavailable) it falls back to an 8 %-inset
36
+ * rectangle. "Reset" returns to whichever seed was used.
37
+ */
38
+ import React from 'react';
39
+ import { type GuidanceCopy } from './cameraGuidanceCopy';
40
+ import { type Quad } from './cropGeometry';
41
+ /** Image-pixel rectangle, used for the optional `initialRect` seed. */
42
+ export interface ImageRect {
43
+ x: number;
44
+ y: number;
45
+ width: number;
46
+ height: number;
47
+ }
48
+ /** What the host receives when the user taps Crop. */
49
+ export interface RectCropResult {
50
+ /**
51
+ * The 4 chosen corners in IMAGE-PIXEL space, canonically ordered
52
+ * [TL, TR, BR, BL]. The host feeds these to the native crop.
53
+ */
54
+ quad: Quad;
55
+ /**
56
+ * `true` → the host should perspective-rectify (`cropToQuad`): the user
57
+ * picked a non-rectangular quad and `perspectiveCorrect` is enabled.
58
+ * `false` → the host can use the cheap axis-aligned `cropToRect` (the
59
+ * quad is ~rectangular, or perspective correction is disabled).
60
+ */
61
+ perspective: boolean;
62
+ }
63
+ export interface RectCropPreviewProps {
64
+ /** file:// URI of the full result image to crop. */
65
+ imageUri: string;
66
+ /** Intrinsic pixel width of `imageUri`. */
67
+ imageWidth: number;
68
+ /** Intrinsic pixel height of `imageUri`. */
69
+ imageHeight: number;
70
+ /** Show / hide the editor. */
71
+ visible: boolean;
72
+ /**
73
+ * Tapped on "Crop". Receives the ordered image-pixel quad + the
74
+ * perspective decision; the host performs the actual native crop.
75
+ */
76
+ onConfirm: (result: RectCropResult) => void;
77
+ /**
78
+ * Tapped on "Use original" (or hardware back / dismiss) — emit the stitch
79
+ * un-cropped. Also called when the user collapses the quad to something
80
+ * un-warpable, so a degenerate quad never reaches the native crop.
81
+ */
82
+ onUseOriginal: (uri?: string) => void;
83
+ /**
84
+ * Tapped on "Retake" — discard this capture entirely and return to the
85
+ * camera. No result is emitted (the host clears the editor + lets the
86
+ * user capture again).
87
+ */
88
+ onRetake: () => void;
89
+ /**
90
+ * Optional non-fatal warning messages (e.g. "<70 % of frames used") shown
91
+ * as a banner across the top of the editor so the user sees them before
92
+ * accepting a crop. Empty / undefined → no banner.
93
+ */
94
+ warnings?: string[];
95
+ /**
96
+ * Crop mode vs preview-only mode. `true` (default) shows the draggable
97
+ * quad + corner handles + the [Retake][Use original][Crop] bar — the full
98
+ * crop editor. `false` hides the quad and all crop affordances, showing
99
+ * just the stitched image with a [Retake][Confirm] bar — a plain preview
100
+ * (`<Camera showPreview>` without `rectCrop`). Confirm emits the image
101
+ * un-cropped (same as "Use original").
102
+ */
103
+ showCropControls?: boolean;
104
+ /**
105
+ * Optional image-pixel seed rect for the draggable quad. Defaults to
106
+ * an 8 %-inset rectangle of the full image. Ignored in preview-only mode.
107
+ */
108
+ initialRect?: ImageRect;
109
+ /** Copy overrides (cropConfirm / cropReset). Falls back to defaults. */
110
+ copy?: Partial<GuidanceCopy>;
111
+ /**
112
+ * Safe-area insets (px). The editor is a full-screen Modal, so the host
113
+ * passes `insets.top`/`insets.bottom` to keep the top toolbar (warnings)
114
+ * clear of the notch/Dynamic Island and the bottom button bar clear of the
115
+ * home indicator. Default 0.
116
+ */
117
+ topInset?: number;
118
+ bottomInset?: number;
119
+ /**
120
+ * 2026-06-14 (DEV overlay) — optional multi-line debug text describing how
121
+ * this output was stitched (pipeline / warper / route / seam / blend / score
122
+ * / frames / size). When non-empty, rendered as a small monospace pill in
123
+ * the top-right corner. The host gates this on `__DEV__`; this component
124
+ * just renders whatever non-empty string it's given.
125
+ */
126
+ debugInfo?: string;
127
+ /**
128
+ * 2026-06-15 — show the live memory-footprint pill (polled native RSS,
129
+ * green/amber/red) on the preview too, so the operator can watch the spike
130
+ * when the on-demand high-level re-stitch fires. Host gates on settings.debug.
131
+ */
132
+ showMemoryPill?: boolean;
133
+ }
134
+ export declare function RectCropPreview(props: RectCropPreviewProps): React.JSX.Element;
135
+ //# sourceMappingURL=RectCropPreview.d.ts.map