react-native-image-stitcher 0.14.2 → 0.15.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 (120) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +35 -0
  3. package/RNImageStitcher.podspec +8 -7
  4. package/android/build.gradle +0 -16
  5. package/android/src/main/cpp/CMakeLists.txt +2 -63
  6. package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
  7. package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
  8. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
  9. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
  10. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
  11. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
  12. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +129 -71
  13. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +49 -0
  14. package/cpp/keyframe_gate.cpp +82 -23
  15. package/cpp/keyframe_gate.hpp +31 -2
  16. package/cpp/stitcher.cpp +208 -28
  17. package/cpp/tests/CMakeLists.txt +18 -12
  18. package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
  19. package/cpp/tests/warp_guard_test.cpp +48 -0
  20. package/cpp/warp_guard.hpp +41 -0
  21. package/dist/camera/Camera.d.ts +31 -16
  22. package/dist/camera/Camera.js +11 -3
  23. package/dist/camera/CameraView.js +93 -3
  24. package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
  25. package/dist/camera/CaptureStitchStatsToast.js +27 -7
  26. package/dist/camera/PanoramaSettings.d.ts +10 -223
  27. package/dist/camera/PanoramaSettings.js +6 -28
  28. package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
  29. package/dist/camera/PanoramaSettingsBridge.js +3 -102
  30. package/dist/camera/PanoramaSettingsModal.js +7 -1
  31. package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
  32. package/dist/camera/buildPanoramaInitialSettings.js +4 -0
  33. package/dist/camera/cameraErrorMessages.d.ts +32 -0
  34. package/dist/camera/cameraErrorMessages.js +53 -0
  35. package/dist/camera/selectCaptureDevice.d.ts +5 -1
  36. package/dist/camera/selectCaptureDevice.js +22 -2
  37. package/dist/camera/useCapture.js +38 -0
  38. package/dist/index.d.ts +5 -8
  39. package/dist/index.js +11 -34
  40. package/dist/stitching/incremental.d.ts +1 -117
  41. package/dist/stitching/stitchVideo.d.ts +0 -35
  42. package/dist/types.d.ts +0 -87
  43. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
  44. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
  45. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
  46. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
  47. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
  48. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
  49. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
  50. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
  51. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
  52. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +82 -7
  53. package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
  54. package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
  55. package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
  56. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
  57. package/package.json +3 -2
  58. package/src/camera/Camera.tsx +44 -23
  59. package/src/camera/CameraView.tsx +113 -4
  60. package/src/camera/CaptureStitchStatsToast.tsx +58 -14
  61. package/src/camera/PanoramaSettings.ts +16 -289
  62. package/src/camera/PanoramaSettingsBridge.ts +3 -114
  63. package/src/camera/PanoramaSettingsModal.tsx +14 -1
  64. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
  65. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
  66. package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
  67. package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
  68. package/src/camera/buildPanoramaInitialSettings.ts +17 -0
  69. package/src/camera/cameraErrorMessages.ts +84 -0
  70. package/src/camera/selectCaptureDevice.ts +28 -3
  71. package/src/camera/useCapture.ts +44 -1
  72. package/src/index.ts +11 -40
  73. package/src/stitching/incremental.ts +3 -140
  74. package/src/stitching/stitchVideo.ts +0 -26
  75. package/src/types.ts +0 -95
  76. package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
  77. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
  78. package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
  79. package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
  80. package/cpp/stitcher_frame_jsi.cpp +0 -214
  81. package/cpp/stitcher_frame_jsi.hpp +0 -108
  82. package/cpp/stitcher_proxy_jsi.cpp +0 -109
  83. package/cpp/stitcher_proxy_jsi.hpp +0 -46
  84. package/cpp/stitcher_worklet_dispatch.cpp +0 -103
  85. package/cpp/stitcher_worklet_dispatch.hpp +0 -71
  86. package/cpp/stitcher_worklet_registry.cpp +0 -91
  87. package/cpp/stitcher_worklet_registry.hpp +0 -146
  88. package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
  89. package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
  90. package/dist/stitching/IncrementalStitcherView.js +0 -157
  91. package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
  92. package/dist/stitching/StitcherWorkletRegistry.js +0 -78
  93. package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
  94. package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
  95. package/dist/stitching/useFrameProcessor.d.ts +0 -119
  96. package/dist/stitching/useFrameProcessor.js +0 -196
  97. package/dist/stitching/useFrameStream.d.ts +0 -34
  98. package/dist/stitching/useFrameStream.js +0 -234
  99. package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
  100. package/dist/stitching/useThrottledFrameProcessor.js +0 -132
  101. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
  102. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
  105. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
  106. package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
  107. package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
  108. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
  109. package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
  110. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
  111. package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
  112. package/src/stitching/IncrementalStitcherView.tsx +0 -198
  113. package/src/stitching/StitcherWorkletRegistry.ts +0 -156
  114. package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
  115. package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
  116. package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
  117. package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
  118. package/src/stitching/useFrameProcessor.ts +0 -226
  119. package/src/stitching/useFrameStream.ts +0 -271
  120. package/src/stitching/useThrottledFrameProcessor.ts +0 -145
package/CHANGELOG.md CHANGED
@@ -16,6 +16,170 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [0.15.1] — 2026-06-08
20
+
21
+ ### Fixed
22
+
23
+ - **Camera preview now matches capture FOV on all paths (letterbox WYSIWYG).**
24
+ The preview and captured photo now share the same field of view regardless of
25
+ the container size the host app uses. Black letterbox bars fill any extra
26
+ space rather than cropping or stretching the camera feed.
27
+ - *VisionCamera path:* `CameraView` measures its rendered bounds via
28
+ `onLayout`, pins the format to 4:3 with `useCameraFormat`, then sizes the
29
+ `<Camera>` component to the largest axis-aligned box that fits the container
30
+ while preserving the format aspect ratio.
31
+ - *ARCore path (Android):* `RNSARCameraView` now selects a camera config
32
+ whose image aspect and texture aspect match within 2% (`selectMatchingCameraConfig`).
33
+ On devices (e.g. Galaxy A35) where no 4:3 matched config exists, the best
34
+ available 16:9 config is chosen — both preview and capture are 16:9.
35
+ The GL renderer letterboxes the camera texture inside the GL surface using
36
+ `setDisplayGeometry` + `glViewport`, centred on a black-cleared surface.
37
+ - *ARKit path (iOS):* `RNSARCameraView.layoutSubviews()` reads
38
+ `imageResolution` from the ARKit session and centres the scene view inside
39
+ the container bounds using the same aspect-correct letterbox calculation.
40
+
41
+ - **ARCore CPU image resolution upgraded automatically.** `selectMatchingCameraConfig`
42
+ prefers the highest-resolution matched config, so CPU image captures used for
43
+ stitching are now at full sensor resolution (1920×1080 on the Galaxy A35,
44
+ up from 640×480) with no API change required.
45
+
46
+ ### Changed
47
+
48
+ - **`defaultCaptureSource` changed from `'ar'` to `'non-ar'`.** AR mode is now
49
+ opt-in. Host apps that want AR must pass `defaultCaptureSource="ar"` or
50
+ implement a toggle; the plain camera path is the default.
51
+
52
+ ## [0.15.0] — 2026-06-07
53
+
54
+ ### Breaking — only `batch-keyframe` remains; host-worklet / frame-stream hooks removed
55
+
56
+ The live/incremental stitching engines (hybrid, slit-scan, firstwins) and the
57
+ third-party host-worklet / frame-stream observer API were archived (kept under
58
+ `archive/`, excluded from every build surface) so the SDK now ships only the
59
+ `batch-keyframe` capture path. Removed from the public API:
60
+
61
+ - **Hooks** `useFrameProcessor`, `useThrottledFrameProcessor`, `useFrameStream`
62
+ and their option types (`ThrottledFrameProcessorOptions`, `FrameStreamOptions`,
63
+ `SampledFrame`). To compose first-party stitching, use vision-camera's own
64
+ `useFrameProcessor` with `useStitcherWorklet().call(frame)` (see the example app).
65
+ - The **slit-scan / hybrid** panorama-engine settings types and their
66
+ native-config adapters (`slitscanSettingsToNativeConfig`,
67
+ `hybridSettingsToNativeConfig`).
68
+
69
+ A type-only break for the default batch-keyframe path; per the 0.x stability
70
+ policy this bumps a new MINOR.
71
+
72
+ ### Changed — iOS + Android unified on the manual `cv::detail` stitch pipeline
73
+
74
+ Both platforms now run the **same** manual `cv::detail` stitch pipeline
75
+ (`useManualPipeline=true` on both), so a given capture produces consistent,
76
+ more robust output regardless of platform. Previously iOS used the manual
77
+ pipeline while Android used the high-level `cv::Stitcher` — the two diverged on
78
+ resolution, exposure handling, and wide-capture robustness. The unified manual
79
+ path carries:
80
+
81
+ - **Exposure compensation** (`cv::detail::GainCompensator`, GAIN_BLOCKS) — evens
82
+ brightness/colour across frames before blending, removing the visible seam
83
+ steps the manual path previously had.
84
+ - **Matched registration / compositing resolution** (registration 0.6 MP,
85
+ composite 1.0 MP) on both platforms.
86
+ - The **cylindrical warp fallback** (below), so wide / 0.5× captures survive on
87
+ both platforms.
88
+
89
+ The decision was made after an on-device A/B (manual vs high-level at matched
90
+ resolution): with parity the manual path matched the high-level on quality and
91
+ was strictly more robust on wide captures. Background + the verification trail
92
+ are recorded in [`docs/stitch-pipeline-architecture.md`](docs/stitch-pipeline-architecture.md).
93
+
94
+ ### Added — cylindrical warp fallback for wide / 0.5× captures
95
+
96
+ When the configured (plane) warper would diverge on a wide or 0.5× ultra-wide
97
+ capture — a single frame's warp canvas exceeding the 100 MP guard — the stitcher
98
+ now auto-retries with the bounded cylindrical projection instead of failing with
99
+ `STITCH_CAMERA_PARAMS_FAIL`. Wide and ultra-wide (0.5×) panoramas that
100
+ previously errored out now complete. Because the pipeline is now unified
101
+ (above), this fallback applies on both iOS and Android.
102
+
103
+ ### Added — `userFacingStitchError()` for friendly recoverable-stitch copy
104
+
105
+ New public SDK export that maps a recoverable stitch `CameraErrorCode`
106
+ (`STITCH_NEED_MORE_IMGS`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_HOMOGRAPHY_FAIL`,
107
+ `STITCH_OOM`) to friendly, action-guiding `{ title, message }` copy for a host
108
+ `Alert` / toast — so the user sees "pan more slowly" / "pivot in place" instead
109
+ of the raw `cv::Stitcher` diagnostic. Returns `null` for every non-recoverable
110
+ code (permission denied, device unavailable, generic finalize failure, unknown,
111
+ …), so the host falls back to its generic error UI. Call it from `onError`:
112
+
113
+ ```tsx
114
+ import { userFacingStitchError } from 'react-native-image-stitcher';
115
+
116
+ onError={(err) => {
117
+ const friendly = userFacingStitchError(err.code);
118
+ if (friendly) Alert.alert(friendly.title, friendly.message);
119
+ else reportGenericError(err);
120
+ }}
121
+ ```
122
+
123
+ Also exports the `UserFacingStitchError` type (`{ title, message }`). Lives in
124
+ the SDK (not per-host) so every consumer shows the same vetted guidance for the
125
+ same failure, and so the mapping is unit-testable in isolation.
126
+
127
+ ### Fixed — friendlier stitch-failure classification + example UX
128
+
129
+ - `STITCH_NEED_MORE_IMGS` now also classifies the manual pipeline's "0 valid
130
+ pairwise matches / frames may not overlap enough" failure, which previously
131
+ surfaced as a generic `PANORAMA_FINALIZE_FAILED`. Both insufficient-overlap
132
+ signals now map to the same recoverable "pan more slowly" outcome (and so pick
133
+ up the `userFacingStitchError` copy above).
134
+ - The example app now shows friendly, action-guiding guidance — via
135
+ `userFacingStitchError` (an Alert) on a stitch failure (`onError`), and a
136
+ transient **toast** when frames are dropped for insufficient overlap
137
+ (`onFramesDropped`) — shown only when **>30%** of the requested frames are
138
+ missing from the final stitch (e.g. ≥2 of 6), so minor drops stay silent. The toast (`CaptureStitchStatsToast`) also
139
+ gained optional `title` (bold, above the message) and `placement`
140
+ (`'top'` | `'center'`) props; the example shows a centered title+body toast.
141
+ Failure alerts now lead with the corrective ask as the title (e.g. "Please
142
+ pan more slowly" / "Try a shorter sweep") and explain the cause in the body.
143
+
144
+ ### Fixed — reach the ultra-wide by device-swap when a logical multi-cam can't (Samsung / Camera2)
145
+
146
+ `selectCaptureDevice` now device-swaps to a standalone ultra-wide camera when a
147
+ logical multi-cam device merely *lists* the ultra-wide but can't reach it by
148
+ `zoom` (its zoom range starts at 1.0 — common on Android / Camera2 / Samsung,
149
+ where the ultra-wide is a separate physical camera rather than a zoom target).
150
+ Previously such devices stayed on the multi-cam device and 0.5× showed the
151
+ wide-angle FOV. A logical device whose zoom range genuinely extends to the
152
+ ultra-wide (e.g. iOS virtual devices, `minZoom ≈ 0.5`) is still preferred and
153
+ lens-switches via zoom as before.
154
+
155
+ ### Added — time-budget keyframe force-accept (`maxKeyframeIntervalMs`)
156
+
157
+ The keyframe gate now force-accepts a keyframe when a configurable wall-clock
158
+ interval has elapsed since the last accepted keyframe — even if the novelty /
159
+ overlap threshold wasn't met — so a slow or static pan never leaves a temporal
160
+ gap. Default **2000 ms (2 s)**; `0` disables it. Configurable via the
161
+ `<Camera defaultMaxKeyframeIntervalMs>` prop, the `FrameSelectionSettings.maxKeyframeIntervalMs`
162
+ field, or the in-app settings panel. Applies to BOTH AR (plane-overlap) and
163
+ non-AR (flow) capture paths; force-accepted keyframes count toward
164
+ `maxKeyframes` (the cap still finalises the capture).
165
+
166
+ ### Added — inscribed-rect panorama crop (opt-in)
167
+
168
+ `<Camera maxInscribedRectCrop={true}>` (and the `enableMaxInscribedRectCrop`
169
+ panorama setting) crops the finished panorama to the largest axis-aligned
170
+ rectangle inscribed in the coverage mask — clean edges with no black corners
171
+ from unfilled projection regions. **It is opt-in; the default is off.** The
172
+ default crop stays the bounding box of non-black pixels, which preserves all
173
+ stitched content but can leave black corners. Inscribed-rect can shrink the
174
+ output substantially on lopsided or ultra-wide masks, so it isn't the default.
175
+
176
+ ### Fixed — Android keyframe-gate flow reason labels
177
+
178
+ `KeyframeGate.reasonFromCode` (Android) didn't map the v0.3.0 flow-strategy
179
+ reason codes 12–15, so accepted keyframes logged as `unknown(12)`. They now
180
+ read `ok-flow` / `first-flow` / `overlap-too-high (flow)` / `ok-flow-translation`,
181
+ matching the iOS labels. Logging only — keyframe selection is unchanged.
182
+
19
183
  ## [0.14.2] — 2026-06-03
20
184
 
21
185
  ### Fixed — AR preview blank on first entry (intermittent camera-handoff race)
package/README.md CHANGED
@@ -212,7 +212,9 @@ These mirror the in-app settings panel; most apps never set them.
212
212
  | `defaultFlowMaxTranslationCm` | `number` | `50` | Max IMU translation between keyframes; 0 = disabled. |
213
213
  | `defaultKeyframeMaxCount` | `number` | `6` | Keyframe cap per capture (3–10). |
214
214
  | `defaultKeyframeOverlapThreshold` | `number` | `0.20` | Min overlap to accept a keyframe (0.20–0.60). |
215
+ | `defaultMaxKeyframeIntervalMs` | `number` | `2000` | Time-budget force-accept: take a keyframe at least every N ms during a pan even if the overlap/novelty threshold isn't met, so a slow or static pan never leaves a temporal gap. Force-accepted keyframes still count toward the keyframe cap. `0` = disabled. AR + non-AR. Also exposed as the `FrameSelectionSettings.maxKeyframeIntervalMs` settings field and in the in-app settings panel. |
215
216
  | `defaultCompositingResolMP` / `defaultRegistrationResolMP` / `defaultSeamEstimationResolMP` | `number` | — | Forward-looking cv::Stitcher resolution knobs (currently no-ops). |
217
+ | `maxInscribedRectCrop` | `boolean` | `false` | Opt in with `true` to crop the panorama to the largest inscribed rectangle (clean edges, no black corners) instead of the bounding box. Default keeps the bounding-box crop (all stitched content; may show black corners). Inscribed-rect can shrink the output on lopsided / ultra-wide masks. |
216
218
 
217
219
  ### UI toggles
218
220
 
@@ -287,6 +289,39 @@ type CameraCaptureResult =
287
289
  `STITCH_HOMOGRAPHY_FAIL`, `STITCH_CAMERA_PARAMS_FAIL`, `STITCH_OOM`,
288
290
  `OUTPUT_WRITE_FAILED`, plus `VISION_CAMERA_RUNTIME`.
289
291
 
292
+ #### Friendly copy for recoverable stitch failures — `userFacingStitchError`
293
+
294
+ The four `STITCH_*` codes are *recoverable*: the user can usually fix them by
295
+ re-capturing (pan more slowly, pivot in place, shorten the sweep). For those,
296
+ the SDK exports `userFacingStitchError(code)` — it returns
297
+ `{ title, message }` of vetted, action-guiding copy you can drop straight into a
298
+ host `Alert`/toast (instead of surfacing the raw `cv::Stitcher` diagnostic), and
299
+ returns `null` for every non-recoverable code so you fall back to your generic
300
+ error UI:
301
+
302
+ ```tsx
303
+ import {
304
+ Camera,
305
+ userFacingStitchError,
306
+ type UserFacingStitchError,
307
+ } from 'react-native-image-stitcher';
308
+ import { Alert } from 'react-native';
309
+
310
+ <Camera
311
+ onError={(err) => {
312
+ const friendly: UserFacingStitchError | null = userFacingStitchError(err.code);
313
+ if (friendly) {
314
+ Alert.alert(friendly.title, friendly.message); // "pan more slowly", "pivot in place", …
315
+ } else {
316
+ reportGenericError(err); // permission denied, device unavailable, etc.
317
+ }
318
+ }}
319
+ />;
320
+ ```
321
+
322
+ It lives in the SDK (not per-host) so every consumer shows the same guidance for
323
+ the same failure. The `example/` app uses it end-to-end.
324
+
290
325
  ### Migration from 0.13.x
291
326
 
292
327
  - **Removed:** the `panGuide` and `panoramaGuidance` props (the
@@ -35,14 +35,15 @@ Pod::Spec.new do |s|
35
35
 
36
36
  # Sources: iOS-specific Swift/Obj-C/Obj-C++ AND the shared C++ port
37
37
  # (cpp/) that both iOS and Android compile from a single source.
38
+ # cpp/ glob is NON-RECURSIVE on purpose: it picks up the shared C++
39
+ # port (all top-level cpp/*.cpp) but skips the maintainer-only
40
+ # GoogleTest harnesses under cpp/tests/ (which would otherwise fail
41
+ # the pod with `'gtest/gtest.h' file not found`). NOTE: using
42
+ # `cpp/**` + `s.exclude_files = ['cpp/tests/**/*']` instead broke the
43
+ # vendored opencv2.xcframework header integration for the remaining
44
+ # cpp/ files — keep this as a single non-recursive glob.
38
45
  s.source_files = ['ios/Sources/**/*.{swift,h,m,mm}',
39
- 'cpp/**/*.{h,hpp,cpp}']
40
- # Exclude the lib's own C++ unit tests — they #include <gtest/gtest.h>,
41
- # which consumer apps don't vendor. The `cpp/**/*.cpp` glob above
42
- # otherwise slurps cpp/tests/*.cpp into every host pod build, failing
43
- # with `'gtest/gtest.h' file not found`. Tests build only in the lib's
44
- # CI / example app, never in a consumer.
45
- s.exclude_files = ['cpp/tests/**/*']
46
+ 'cpp/*.{h,hpp,cpp}']
46
47
  # Restrict the umbrella header to ONLY the iOS-side Obj-C `.h`
47
48
  # files. Without this, CocoaPods defaults every header in
48
49
  # `source_files` (including the C++ `.hpp` files under cpp/) to
@@ -267,22 +267,6 @@ dependencies {
267
267
  android.sourceSets.main.java.exclude '**/CvFlowGateFrameProcessor.kt'
268
268
  }
269
269
 
270
- // v0.8.0 Phase 4b.ii — react-native-worklets-core's Android
271
- // prefab (`rnworklets`) is consumed by the native shim
272
- // (`stitcher_worklet_registry.cpp` constructs
273
- // `RNWorklet::WorkletInvoker`s). `implementation` not
274
- // `compileOnly` because we need the prefab's `.so` available at
275
- // both link time AND runtime — without the runtime presence,
276
- // `dlopen` would fail when our `libimage_stitcher.so` is loaded.
277
- //
278
- // Host apps that use this lib already declare worklets-core as
279
- // a peer dep (see package.json's peerDependencies); RN
280
- // autolinking + Gradle deduplicates, so the host doesn't get
281
- // a second copy.
282
- if (findProject(':react-native-worklets-core') != null) {
283
- implementation project(':react-native-worklets-core')
284
- }
285
-
286
270
  // v0.10.0 audit #11A — Android JUnit test scaffold. JVM unit
287
271
  // tests for pure-Kotlin data wrappers + algorithm helpers that
288
272
  // don't need an Android device. Run via
@@ -80,34 +80,6 @@ if(NOT EXISTS "${SHARED_CPP_DIR}/keyframe_gate.hpp")
80
80
  "Expected react-native-image-stitcher/cpp/ — was the package layout broken?")
81
81
  endif()
82
82
 
83
- # ── React Native prefab packages for JSI ──────────────────────────
84
- #
85
- # v0.8.0 Phase 3 — activating the previously-deferred JSI integration.
86
- # The shared C++ host object (cpp/stitcher_frame_jsi.cpp) depends on
87
- # `facebook::jsi`. ReactAndroid ships JSI as a prefab starting
88
- # RN 0.71+; the lib targets RN 0.84 so this is always available.
89
- #
90
- # `buildFeatures { prefab true }` in android/build.gradle enables
91
- # consumption + `ANDROID_STL=c++_shared` aligns the STL with what
92
- # the prefabs require. The Phase-2 STL probe (`llvm-nm
93
- # libopencv_stitching.a | grep '__ndk1'`) confirmed OpenCV's
94
- # stitching archive was already built with c++_shared (768
95
- # __ndk1 symbols + 0 __cxx11 / NSt3) — switching the lib's flag
96
- # from c++_static to c++_shared just aligns + matches. The
97
- # previous c++_static was working only because the JNI shim's
98
- # `.so` boundary used POD/C types; the new c++_shared is properly
99
- # matched throughout.
100
- find_package(ReactAndroid REQUIRED CONFIG)
101
- find_package(fbjni REQUIRED CONFIG)
102
-
103
- # v0.8.0 Phase 4b.ii — react-native-worklets-core prefab. The
104
- # Gradle module name is `react-native-worklets-core`; inside it
105
- # publishes a library named `rnworklets` (matches vc's consumption
106
- # pattern in node_modules/react-native-vision-camera/android/CMakeLists.txt).
107
- # We consume both the headers (for `WKTJsiWorklet.h` etc.) AND
108
- # the .so (for `RNWorklet::WorkletInvoker` + `JsiWrapper::unwrap`
109
- # symbols, which are defined in worklets-core's WKTJsiWrapper.cpp).
110
- find_package(react-native-worklets-core REQUIRED CONFIG)
111
83
 
112
84
  # ── Our shim ───────────────────────────────────────────────────────
113
85
  add_library(image_stitcher SHARED
@@ -119,30 +91,7 @@ add_library(image_stitcher SHARED
119
91
  # retry + dimension/memory instrumentation. Used to live in this
120
92
  # file (image_stitcher_jni.cpp). See cpp/stitcher.hpp for design
121
93
  # rationale.
122
- "${SHARED_CPP_DIR}/stitcher.cpp"
123
- # v0.8.0 Phase 3 — shared JSI host object for `StitcherFrame`.
124
- # Compiles to identical dispatch on both platforms; iOS consumes
125
- # it via the .mm shim at
126
- # `ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm`.
127
- # See cpp/stitcher_frame_jsi.hpp for the class API.
128
- "${SHARED_CPP_DIR}/stitcher_frame_jsi.cpp"
129
- # v0.8.0 Phase 4b.ii — shared C++ registry of host-supplied
130
- # worklets + the `globalThis.__stitcherProxy` host object that
131
- # JS calls into. iOS picked these up via the podspec glob in
132
- # Phase 4b.i; Android adds them here.
133
- "${SHARED_CPP_DIR}/stitcher_worklet_registry.cpp"
134
- "${SHARED_CPP_DIR}/stitcher_proxy_jsi.cpp"
135
- # v0.8.0 Phase 4b.iii — shared per-frame fan-out helper. Posts
136
- # a `StitcherFrameData` onto worklets-core's default context's
137
- # worklet thread; iterates the host worklet registry; invalidates
138
- # the JSI host object after dispatch completes.
139
- "${SHARED_CPP_DIR}/stitcher_worklet_dispatch.cpp"
140
- # v0.8.0 Phase 4b.ii — Android JNI bindings for the JSI install
141
- # (`StitcherJsiInstallerModule.nativeInstall`). Reaches into the
142
- # main JS runtime via the `long` JSI handle Kotlin pulls from
143
- # `ReactApplicationContext.getJavaScriptContextHolder()`. See
144
- # worklets-core's `WorkletsModule.java` for the canonical pattern.
145
- stitcher_jsi_install_jni.cpp)
94
+ "${SHARED_CPP_DIR}/stitcher.cpp")
146
95
 
147
96
  target_include_directories(image_stitcher PRIVATE
148
97
  "${OPENCV_INCLUDE_DIR}"
@@ -169,17 +118,7 @@ target_link_libraries(image_stitcher
169
118
  opencv_stitching
170
119
  -Wl,--no-whole-archive
171
120
  opencv_java
172
- log
173
- # v0.8.0 Phase 3 — JSI for the shared C++ host object
174
- # (cpp/stitcher_frame_jsi.cpp's `facebook::jsi::HostObject`
175
- # subclass). fbjni for the Phase 3c JNI bridge between Kotlin
176
- # worklet runtime + C++ host object construction.
177
- ReactAndroid::jsi
178
- fbjni::fbjni
179
- # v0.8.0 Phase 4b.ii — worklets-core's `RNWorklet::WorkletInvoker`
180
- # is constructed in the C++ registry's `install` method and
181
- # invoked from the Android per-frame dispatch path.
182
- react-native-worklets-core::rnworklets)
121
+ log)
183
122
 
184
123
  target_compile_options(image_stitcher PRIVATE
185
124
  -fvisibility=hidden
@@ -116,6 +116,20 @@ Java_io_imagestitcher_rn_BatchStitcher_nativeStitchFramePaths(
116
116
  ? retailens::StitchMode::Panorama
117
117
  : retailens::StitchMode::Scans;
118
118
 
119
+ // 2026-06-07 — unify on the manual cv::detail pipeline. It won the
120
+ // on-device A/B: equals the high-level cv::Stitcher on quality after
121
+ // parity AND is strictly more robust — the cylindrical fallback, warp
122
+ // guard, and exposure comp all live only in the manual path, so the
123
+ // high-level path garbages wide/0.5x captures. Mirrors iOS'
124
+ // OpenCVStitcher.mm. See docs/stitch-pipeline-architecture.md §7.
125
+ cfg.useManualPipeline = true;
126
+ // Match iOS' parity resolution: the manual entry's default registration
127
+ // is 0.3 MP (vs the high-level's 0.6); bump to 0.6 unless the caller set
128
+ // an explicit value. (compositingResolMP already arrives as 1.0.)
129
+ if (cfg.registrationResolMP <= 0.0) {
130
+ cfg.registrationResolMP = 0.6;
131
+ }
132
+
119
133
  const std::string outPath = jstring_to_string(env, outputPath);
120
134
 
121
135
  retailens::StitchResult result = retailens::stitchFramePaths(
@@ -110,6 +110,19 @@ Java_io_imagestitcher_rn_KeyframeGate_nativeSetFlowMaxTranslationM(
110
110
  gate(handle)->setFlowMaxTranslationM(static_cast<double>(metres));
111
111
  }
112
112
 
113
+ // Wall-clock keyframe-interval budget (milliseconds). Force-accepts a
114
+ // frame once the elapsed time since the last accepted keyframe exceeds
115
+ // this value (applies to BOTH Pose and Flow strategies); 0 disables.
116
+ // Passed straight through — no unit conversion. See
117
+ // setMaxKeyframeIntervalMs doc in keyframe_gate.hpp. Android JNI
118
+ // counterpart of the iOS bridge method in KeyframeGateBridge.
119
+ JNIEXPORT void JNICALL
120
+ Java_io_imagestitcher_rn_KeyframeGate_nativeSetMaxKeyframeIntervalMs(
121
+ JNIEnv*, jclass, jlong handle, jdouble ms)
122
+ {
123
+ gate(handle)->setMaxKeyframeIntervalMs(static_cast<double>(ms));
124
+ }
125
+
113
126
  // 2026-05-14 — Android JNI for the percentile setter so JS Settings
114
127
  // can tune novelty aggregation on Android (was iOS-only until now).
115
128
  // See setFlowNoveltyPercentile doc in keyframe_gate.hpp.