react-native-image-stitcher 0.16.0 → 0.16.2
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.
- package/CHANGELOG.md +80 -0
- package/README.md +41 -44
- package/android/build.gradle +34 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +52 -7
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +135 -59
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +481 -87
- package/cpp/stitcher.hpp +52 -0
- package/dist/camera/Camera.d.ts +13 -0
- package/dist/camera/Camera.js +9 -64
- package/dist/camera/CaptureFrameCounterOverlay.js +12 -1
- package/dist/camera/CaptureMemoryPill.d.ts +15 -7
- package/dist/camera/CaptureMemoryPill.js +34 -9
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.js +22 -25
- package/dist/camera/RectCropPreview.d.ts +3 -29
- package/dist/camera/RectCropPreview.js +20 -130
- package/dist/stitching/incremental.d.ts +29 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +103 -26
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +21 -1
- package/package.json +1 -1
- package/src/camera/Camera.tsx +21 -70
- package/src/camera/CaptureFrameCounterOverlay.tsx +15 -1
- package/src/camera/CaptureMemoryPill.tsx +33 -9
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +22 -25
- package/src/camera/RectCropPreview.tsx +38 -220
- package/src/stitching/incremental.ts +29 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
package/cpp/stitcher.hpp
CHANGED
|
@@ -47,6 +47,23 @@
|
|
|
47
47
|
#include <vector>
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
// ── 2026-06-16 — memory-profiling compile gate (shared) ─────────────────
|
|
51
|
+
// Hard gate for the peak sampler + per-stitch record + [memstat] phase logs
|
|
52
|
+
// (stitcher.cpp) and the mallopt purge diagnostic READS (image_stitcher_jni.cpp).
|
|
53
|
+
// Default: ON in debug, OFF in release, so production pays nothing. Override
|
|
54
|
+
// with -DRNIS_MEMORY_PROFILING=1 to profile a release build. NDEBUG is the
|
|
55
|
+
// portable signal both Gradle (Debug CMake config) and Xcode use, so this is
|
|
56
|
+
// uniform across Android + iOS with no per-build-system flag. Defined in the
|
|
57
|
+
// shared header so all three native translation units agree.
|
|
58
|
+
#ifndef RNIS_MEMORY_PROFILING
|
|
59
|
+
# ifdef NDEBUG
|
|
60
|
+
# define RNIS_MEMORY_PROFILING 0
|
|
61
|
+
# else
|
|
62
|
+
# define RNIS_MEMORY_PROFILING 1
|
|
63
|
+
# endif
|
|
64
|
+
#endif
|
|
65
|
+
|
|
66
|
+
|
|
50
67
|
namespace retailens {
|
|
51
68
|
|
|
52
69
|
// Stable error codes. Mirror the JS-side `StitchErrorCode` enum so
|
|
@@ -197,6 +214,23 @@ struct StitchConfig {
|
|
|
197
214
|
// flip it to true once the manual port is verified — separate
|
|
198
215
|
// commit from this V2 introduction.
|
|
199
216
|
bool useManualPipeline = false;
|
|
217
|
+
|
|
218
|
+
// ── 2026-06-16 — memory profiling hooks (DEV deploy gate) ───────────
|
|
219
|
+
// memProbeFn: resident-memory source in MB, or < 0 if unavailable. When
|
|
220
|
+
// set it is the canonical reader used by rss_mb() (so the OOM guards, the
|
|
221
|
+
// phase logs, the peak sampler and the per-stitch record all use it). Its
|
|
222
|
+
// reason for existing is iOS, which has no /proc/self/statm — the Obj-C++
|
|
223
|
+
// bridge plugs task_info(TASK_VM_INFO).phys_footprint here. Android leaves
|
|
224
|
+
// it null and rss_mb() falls back to /proc. Must be callable from a
|
|
225
|
+
// background thread (the peak sampler), so the closure must not touch
|
|
226
|
+
// thread-affine state.
|
|
227
|
+
std::function<double()> memProbeFn = nullptr;
|
|
228
|
+
// enableMemoryProfiling: runtime gate (plumbed from settings.debug) for the
|
|
229
|
+
// peak sampler + the per-stitch record + the [memstat] phase logs. The
|
|
230
|
+
// COMPILE-time RNIS_MEMORY_PROFILING flag (off in release) is the hard gate;
|
|
231
|
+
// this is the per-call switch on top. The mallopt(M_PURGE) CALL is NOT
|
|
232
|
+
// gated by this — only its diagnostic READS are.
|
|
233
|
+
bool enableMemoryProfiling = false;
|
|
200
234
|
};
|
|
201
235
|
|
|
202
236
|
|
|
@@ -235,6 +269,24 @@ struct StitchResult {
|
|
|
235
269
|
// Empty on builds that don't populate it (back-compat). iOS marshals it up
|
|
236
270
|
// to the JS finalize dict; Android leaves it in the log for now.
|
|
237
271
|
std::string debugSummary;
|
|
272
|
+
|
|
273
|
+
// ── 2026-06-16 — per-stitch memory record (DEV profiling) ───────────
|
|
274
|
+
// All in MB; -1.0 when profiling is off or no memory source is available.
|
|
275
|
+
// memBeforeMB: resident at entry (after the leak-fix once-guard).
|
|
276
|
+
// memPeakMB: max resident sampled DURING the stitch by the 50 ms peak
|
|
277
|
+
// sampler — the transient warp-all + GraphCut + MultiBand
|
|
278
|
+
// spike that the phase-boundary reads miss (it decides OOM).
|
|
279
|
+
// memAfterMB: resident after the pipeline returns (blender pyramids freed).
|
|
280
|
+
// memFloorMB: resident after the platform's post-stitch reclaim — Android
|
|
281
|
+
// fills it after mallopt(M_PURGE); iOS after a settle read.
|
|
282
|
+
// This is the leak-PLATEAU metric (the bridge sets it; the
|
|
283
|
+
// core leaves it at -1).
|
|
284
|
+
// memSource: "phys_footprint" (iOS task_info) | "rss" (/proc) | "".
|
|
285
|
+
double memBeforeMB = -1.0;
|
|
286
|
+
double memPeakMB = -1.0;
|
|
287
|
+
double memAfterMB = -1.0;
|
|
288
|
+
double memFloorMB = -1.0;
|
|
289
|
+
std::string memSource;
|
|
238
290
|
};
|
|
239
291
|
|
|
240
292
|
|
package/dist/camera/Camera.d.ts
CHANGED
|
@@ -123,6 +123,19 @@ export type CameraCaptureResult = {
|
|
|
123
123
|
* cv::Stitcher at finalize).
|
|
124
124
|
*/
|
|
125
125
|
stitchModeResolved?: 'panorama' | 'scans';
|
|
126
|
+
/**
|
|
127
|
+
* 2026-06-15 (DEV) — gyro rotation magnitude of the capture, in radians.
|
|
128
|
+
* Shown on the dev preview so the panorama-vs-SCANS rotation threshold can
|
|
129
|
+
* be tuned. `0` = no pose-derived rotation signal (non-AR with no poses).
|
|
130
|
+
*/
|
|
131
|
+
rRadians?: number;
|
|
132
|
+
/**
|
|
133
|
+
* 2026-06-16 (DEV) — translation magnitude (m) + auto decision ratio
|
|
134
|
+
* (`>=0.55` → SCANS) that drove panorama-vs-SCANS. Shown on the dev
|
|
135
|
+
* readout alongside `rRadians` to tune the threshold from real captures.
|
|
136
|
+
*/
|
|
137
|
+
tMeters?: number;
|
|
138
|
+
decisionRatio?: number;
|
|
126
139
|
/**
|
|
127
140
|
* 2026-06-14 (DEV overlay) — semicolon-separated `key=value` trace of the
|
|
128
141
|
* stitcher's runtime choices (pipe/warp/route/seam/blend) for this
|
package/dist/camera/Camera.js
CHANGED
|
@@ -379,58 +379,6 @@ function Camera(props) {
|
|
|
379
379
|
// CameraCaptureResult we'd otherwise have emitted, stashed so cancel /
|
|
380
380
|
// crop-confirm can emit it (possibly with cropped dims) afterwards.
|
|
381
381
|
const [cropPending, setCropPending] = (0, react_1.useState)(null);
|
|
382
|
-
// 2026-06-15 — ON-DEMAND high-level preview. Manual is the default/eager
|
|
383
|
-
// output; when the user switches to the "high-level" tab in the preview we
|
|
384
|
-
// re-stitch the SAME captured keyframes through stock cv::Stitcher via
|
|
385
|
-
// `refinePanorama` (useManualPipeline:false). Resolves with the high-level
|
|
386
|
-
// JPEG's file:// uri AND its OWN DEV-overlay recipe (so the preview pill shows
|
|
387
|
-
// the high-level recipe — pipe=highlevel;… — while that tab is viewed, not the
|
|
388
|
-
// manual primary's recipe), or null when unavailable (no keyframe paths —
|
|
389
|
-
// e.g. Android — or the stitch failed). Computed lazily so it costs nothing
|
|
390
|
-
// unless the user actually asks for it.
|
|
391
|
-
const requestHighLevelAlt = (0, react_1.useCallback)(async () => {
|
|
392
|
-
const pending = cropPending;
|
|
393
|
-
const kf = pending?.captureResultObj.keyframePaths;
|
|
394
|
-
if (!pending || !kf || kf.length < 2)
|
|
395
|
-
return null;
|
|
396
|
-
const native = (0, incremental_1.getIncrementalNativeModule)();
|
|
397
|
-
if (!native)
|
|
398
|
-
return null;
|
|
399
|
-
const outputPath = `${(0, paths_1.toBareFilePath)(pending.uri).replace(/\.jpg$/i, '')}-highlevel.jpg`;
|
|
400
|
-
try {
|
|
401
|
-
const r = await native.refinePanorama({
|
|
402
|
-
framePaths: kf,
|
|
403
|
-
outputPath,
|
|
404
|
-
config: {
|
|
405
|
-
useManualPipeline: false,
|
|
406
|
-
warperType: 'spherical',
|
|
407
|
-
stitchMode: 'panorama',
|
|
408
|
-
// Match the manual output's rotation — without this the high-level
|
|
409
|
-
// re-stitch bakes "portrait" (no rotation) and comes out sideways.
|
|
410
|
-
captureOrientation: pending.captureResultObj.captureOrientation,
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
// Plain file:// uri — the path is unique per capture and computed once, so
|
|
414
|
-
// no cache-bust here (the accept handler adds one when emitting). The
|
|
415
|
-
// DEV pill text is the HIGH-LEVEL stitch's own recipe (only the fields
|
|
416
|
-
// IncrementalRefineResult carries; buildStitchDebugInfo tolerates the rest
|
|
417
|
-
// being absent).
|
|
418
|
-
return {
|
|
419
|
-
uri: (0, paths_1.toFileUri)(r.panoramaPath),
|
|
420
|
-
debugInfo: (0, stitchDebugInfo_1.buildStitchDebugInfo)({
|
|
421
|
-
debugSummary: r.debugSummary,
|
|
422
|
-
finalConfidenceThresh: r.finalConfidenceThresh,
|
|
423
|
-
framesIncluded: r.framesIncluded,
|
|
424
|
-
framesRequested: r.framesRequested,
|
|
425
|
-
width: r.width,
|
|
426
|
-
height: r.height,
|
|
427
|
-
}),
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
catch {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
}, [cropPending]);
|
|
434
382
|
// 2026-05-22 (audit F9 + F3) — debug stitch-stats toast. Hook
|
|
435
383
|
// exposes an imperative API; we fire `showResult(finalizeResult)`
|
|
436
384
|
// on every successful finalize when settings.debug is on (gated
|
|
@@ -1140,7 +1088,7 @@ function Camera(props) {
|
|
|
1140
1088
|
// native side uses pose-derived translation and ignores this).
|
|
1141
1089
|
const imuTotalTranslationM = isNonAR ? imuGate.getTotalAbsMetres() : 0;
|
|
1142
1090
|
const result = await incremental.finalize(panoOutputPath, 90, // default JPEG quality
|
|
1143
|
-
deviceOrientation, imuTotalTranslationM);
|
|
1091
|
+
deviceOrientation, imuTotalTranslationM, lens);
|
|
1144
1092
|
if (typeof result.framesRequested === 'number'
|
|
1145
1093
|
&& typeof result.framesIncluded === 'number'
|
|
1146
1094
|
&& result.framesIncluded < result.framesRequested) {
|
|
@@ -1174,6 +1122,9 @@ function Camera(props) {
|
|
|
1174
1122
|
finalConfidenceThresh: result.finalConfidenceThresh ?? -1,
|
|
1175
1123
|
durationMs: Date.now() - (recordingStartedAt ?? Date.now()),
|
|
1176
1124
|
stitchModeResolved: result.stitchModeResolved,
|
|
1125
|
+
rRadians: result.rRadians,
|
|
1126
|
+
tMeters: result.tMeters,
|
|
1127
|
+
decisionRatio: result.decisionRatio,
|
|
1177
1128
|
debugSummary: result.debugSummary,
|
|
1178
1129
|
keyframePaths: result.batchKeyframePaths,
|
|
1179
1130
|
captureOrientation: result.captureOrientation,
|
|
@@ -1279,6 +1230,10 @@ function Camera(props) {
|
|
|
1279
1230
|
isNonAR,
|
|
1280
1231
|
imuGate,
|
|
1281
1232
|
stitchToast,
|
|
1233
|
+
// 2026-06-16 — the finalize passes `lens` (the high-level warper tree's zoom
|
|
1234
|
+
// signal); without it here the closure would send a STALE lens if the user
|
|
1235
|
+
// switched 1x↔0.5x after this callback was last memoized.
|
|
1236
|
+
lens,
|
|
1282
1237
|
// feature/pano-ux-guidance — the release also tears down the
|
|
1283
1238
|
// pan-duration timer + a pending rotate-gate, and decides whether to
|
|
1284
1239
|
// route the result through the crop editor.
|
|
@@ -1569,17 +1524,7 @@ function Camera(props) {
|
|
|
1569
1524
|
, {
|
|
1570
1525
|
// Remount per capture so the dragged-quad + layout state re-seed to
|
|
1571
1526
|
// the new image (RectCropPreview seeds its quad once via useState).
|
|
1572
|
-
key: cropPending?.uri ?? 'crop', visible: cropPending != null, imageUri: cropPending?.uri ?? '', imageWidth: cropPending?.width ?? 0, imageHeight: cropPending?.height ?? 0,
|
|
1573
|
-
// 2026-06-15 — manual is the default/eager output. The high-level tab
|
|
1574
|
-
// is ON DEMAND: RectCropPreview calls onRequestAlt() (which re-stitches
|
|
1575
|
-
// the captured keyframes via cv::Stitcher) only when the user switches
|
|
1576
|
-
// to it. DEBUG-ONLY: it's a pipeline-comparison tool (dev-jargon
|
|
1577
|
-
// "Manual"/"High-level" labels), gated behind `settings.debug` like the
|
|
1578
|
-
// rest of the diagnostic UI. Also requires keyframePaths, so it only
|
|
1579
|
-
// appears where it can run (iOS); Android returns no paths → no tab.
|
|
1580
|
-
onRequestAlt: settings.debug && cropPending?.captureResultObj.keyframePaths?.length
|
|
1581
|
-
? requestHighLevelAlt
|
|
1582
|
-
: undefined, initialRect: cropPending?.initialRect, warnings: cropPending?.warnings.map((w) => w.message) ?? [], showCropControls: rectCrop, topInset: insets.top, bottomInset: insets.bottom, copy: guidanceCopyResolved,
|
|
1527
|
+
key: cropPending?.uri ?? 'crop', visible: cropPending != null, imageUri: cropPending?.uri ?? '', imageWidth: cropPending?.width ?? 0, imageHeight: cropPending?.height ?? 0, initialRect: cropPending?.initialRect, warnings: cropPending?.warnings.map((w) => w.message) ?? [], showCropControls: rectCrop, topInset: insets.top, bottomInset: insets.bottom, copy: guidanceCopyResolved,
|
|
1583
1528
|
// Carry the live memory pill onto the preview too (same settings.debug
|
|
1584
1529
|
// gate as the camera), so the operator can watch the RSS spike when the
|
|
1585
1530
|
// on-demand high-level re-stitch fires.
|
|
@@ -32,13 +32,24 @@ exports.topCenterForOrientation = topCenterForOrientation;
|
|
|
32
32
|
const react_1 = __importDefault(require("react"));
|
|
33
33
|
const react_native_1 = require("react-native");
|
|
34
34
|
const guidanceTokens_1 = require("./guidanceTokens");
|
|
35
|
+
/**
|
|
36
|
+
* Extra distance (px) to drop the counter from the user-top in landscape so it
|
|
37
|
+
* clears the pan how-to coach-mark's bouncing arrow. Landscape only; portrait
|
|
38
|
+
* is unaffected. 72 px (the symmetric lift) over-cleared, so this is smaller.
|
|
39
|
+
*/
|
|
40
|
+
const COUNTER_LANDSCAPE_EXTRA_INSET = 40;
|
|
35
41
|
function CaptureFrameCounterOverlay({ visible, framesCaptured, framesMax, orientation, style, }) {
|
|
36
42
|
if (!visible || framesMax <= 0)
|
|
37
43
|
return null;
|
|
38
44
|
// Clamp the displayed numerator into [0, framesMax] — the engine can
|
|
39
45
|
// briefly report the cap-th accept before the parent finalizes.
|
|
40
46
|
const k = Math.max(0, Math.min(framesCaptured, framesMax));
|
|
41
|
-
|
|
47
|
+
// 2026-06-16 — in LANDSCAPE, push the counter further from the user-top so it
|
|
48
|
+
// clears the pan how-to coach-mark's bouncing amber arrow, which sits near the
|
|
49
|
+
// top there and otherwise overlaps it. Portrait keeps the standard inset.
|
|
50
|
+
// Tune COUNTER_LANDSCAPE_EXTRA_INSET if the gap is too small / too large.
|
|
51
|
+
const isLandscape = orientation === 'landscape-left' || orientation === 'landscape-right';
|
|
52
|
+
const { container, rotate } = topCenterForOrientation(orientation, guidanceTokens_1.GUIDANCE_COUNTDOWN.inset + (isLandscape ? COUNTER_LANDSCAPE_EXTRA_INSET : 0));
|
|
42
53
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.layer, container, style] },
|
|
43
54
|
react_1.default.createElement(react_native_1.View, { style: [styles.pill, { transform: [{ rotate }] }] },
|
|
44
55
|
react_1.default.createElement(react_native_1.View, { style: styles.dot }),
|
|
@@ -2,15 +2,23 @@
|
|
|
2
2
|
* CaptureMemoryPill — top-right diagnostic pill showing native
|
|
3
3
|
* process memory footprint in MB, polled at 500 ms.
|
|
4
4
|
*
|
|
5
|
-
* Color-coded against the
|
|
5
|
+
* Color-coded against the device's per-process memory budget, which is read
|
|
6
|
+
* once at mount via `getDeviceTotalRamMB()` (RAM-aware):
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
8
|
+
* budget = max(RAM × 0.42, 900 MB) (mirrors warp_guard.hpp
|
|
9
|
+
* perProcessMemoryBudgetMB)
|
|
10
|
+
* - green < 55 % of budget (comfortable)
|
|
11
|
+
* - amber 55–70 % of budget (approaching pressure)
|
|
12
|
+
* - red > 70 % of budget (close to limit — capture may be killed)
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* Why RAM-aware: the old fixed 1500/2200 MB thresholds were tuned for the
|
|
15
|
+
* iPhone 16 Pro and NEVER tripped on a 4 GB Android phone that jetsams ~1.3 GB
|
|
16
|
+
* (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
|
|
17
|
+
* RAM read is unavailable.
|
|
18
|
+
*
|
|
19
|
+
* Backed by the `getMemoryFootprintMB()` native module (iOS: `task_info`
|
|
20
|
+
* `phys_footprint`; Android: `/proc/self/statm` RSS — the SAME number the C++
|
|
21
|
+
* `[memstat]` logs report). Returns -1 if the native call fails.
|
|
14
22
|
*
|
|
15
23
|
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
16
24
|
* polls native every 500 ms and is unwanted in production builds.
|
|
@@ -4,15 +4,23 @@
|
|
|
4
4
|
* CaptureMemoryPill — top-right diagnostic pill showing native
|
|
5
5
|
* process memory footprint in MB, polled at 500 ms.
|
|
6
6
|
*
|
|
7
|
-
* Color-coded against the
|
|
7
|
+
* Color-coded against the device's per-process memory budget, which is read
|
|
8
|
+
* once at mount via `getDeviceTotalRamMB()` (RAM-aware):
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
10
|
+
* budget = max(RAM × 0.42, 900 MB) (mirrors warp_guard.hpp
|
|
11
|
+
* perProcessMemoryBudgetMB)
|
|
12
|
+
* - green < 55 % of budget (comfortable)
|
|
13
|
+
* - amber 55–70 % of budget (approaching pressure)
|
|
14
|
+
* - red > 70 % of budget (close to limit — capture may be killed)
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* Why RAM-aware: the old fixed 1500/2200 MB thresholds were tuned for the
|
|
17
|
+
* iPhone 16 Pro and NEVER tripped on a 4 GB Android phone that jetsams ~1.3 GB
|
|
18
|
+
* (false comfort exactly where OOM happens). Falls back to 1500/2200 if the
|
|
19
|
+
* RAM read is unavailable.
|
|
20
|
+
*
|
|
21
|
+
* Backed by the `getMemoryFootprintMB()` native module (iOS: `task_info`
|
|
22
|
+
* `phys_footprint`; Android: `/proc/self/statm` RSS — the SAME number the C++
|
|
23
|
+
* `[memstat]` logs report). Returns -1 if the native call fails.
|
|
16
24
|
*
|
|
17
25
|
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
18
26
|
* polls native every 500 ms and is unwanted in production builds.
|
|
@@ -57,11 +65,22 @@ const react_native_1 = require("react-native");
|
|
|
57
65
|
const incremental_1 = require("../stitching/incremental");
|
|
58
66
|
function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, style, }) {
|
|
59
67
|
const [memMB, setMemMB] = (0, react_1.useState)(null);
|
|
68
|
+
// Device total RAM (MB), read once — drives the RAM-aware pressure bands.
|
|
69
|
+
const [ramMB, setRamMB] = (0, react_1.useState)(null);
|
|
60
70
|
(0, react_1.useEffect)(() => {
|
|
61
71
|
const native = (0, incremental_1.getIncrementalNativeModule)();
|
|
62
72
|
if (!native?.getMemoryFootprintMB)
|
|
63
73
|
return undefined;
|
|
64
74
|
let cancelled = false;
|
|
75
|
+
// One-time RAM read for the bands (optional native method — older bridges
|
|
76
|
+
// without it just keep the fixed-threshold fallback).
|
|
77
|
+
native
|
|
78
|
+
.getDeviceTotalRamMB?.()
|
|
79
|
+
.then((r) => {
|
|
80
|
+
if (!cancelled && r > 0)
|
|
81
|
+
setRamMB(r);
|
|
82
|
+
})
|
|
83
|
+
.catch(() => { });
|
|
65
84
|
const tick = async () => {
|
|
66
85
|
try {
|
|
67
86
|
const mb = await native.getMemoryFootprintMB();
|
|
@@ -81,8 +100,14 @@ function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, style, }) {
|
|
|
81
100
|
}, [pollIntervalMs]);
|
|
82
101
|
if (memMB === null || memMB < 0)
|
|
83
102
|
return null;
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
// RAM-aware bands: budget = max(RAM × 0.42, 900) (mirrors warp_guard.hpp
|
|
104
|
+
// perProcessMemoryBudgetMB); amber at 55 %, red at 70 %. Fall back to the
|
|
105
|
+
// iPhone-tuned fixed thresholds when RAM is unknown.
|
|
106
|
+
const budget = ramMB != null ? Math.max(ramMB * 0.42, 900) : null;
|
|
107
|
+
const redAt = budget != null ? budget * 0.7 : 2200;
|
|
108
|
+
const amberAt = budget != null ? budget * 0.55 : 1500;
|
|
109
|
+
const bg = memMB > redAt ? 'rgba(239, 68, 68, 0.92)' // red
|
|
110
|
+
: memMB > amberAt ? 'rgba(245, 158, 11, 0.92)' // amber
|
|
86
111
|
: 'rgba(34, 197, 94, 0.92)'; // green
|
|
87
112
|
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
88
113
|
styles.container,
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
@@ -77,21 +77,19 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
|
|
|
77
77
|
captureSource: 'ar',
|
|
78
78
|
debug: false,
|
|
79
79
|
stitcher: {
|
|
80
|
-
// v0.16 —
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// v0.
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
// manual pipeline (it only fires when warperType != spherical).
|
|
94
|
-
warperType: 'spherical',
|
|
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.
|
|
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.
|
|
92
|
+
warperType: 'plane',
|
|
95
93
|
blenderType: 'multiband',
|
|
96
94
|
seamFinderType: 'graphcut',
|
|
97
95
|
// v0.15 — inscribed-rect crop is OFF by default (bbox crop keeps all
|
|
@@ -102,16 +100,15 @@ exports.DEFAULT_PANORAMA_SETTINGS = {
|
|
|
102
100
|
},
|
|
103
101
|
frameSelection: {
|
|
104
102
|
mode: 'flow-based',
|
|
105
|
-
// v0.16 —
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// the
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
overlapThreshold: 0.15,
|
|
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].
|
|
110
|
+
maxKeyframes: 6,
|
|
111
|
+
overlapThreshold: 0.20,
|
|
115
112
|
maxKeyframeIntervalMs: 1500,
|
|
116
113
|
flow: exports.DEFAULT_FLOW_GATE_SETTINGS,
|
|
117
114
|
},
|
|
@@ -67,32 +67,6 @@ export interface RectCropPreviewProps {
|
|
|
67
67
|
imageWidth: number;
|
|
68
68
|
/** Intrinsic pixel height of `imageUri`. */
|
|
69
69
|
imageHeight: number;
|
|
70
|
-
/**
|
|
71
|
-
* DEBUG A/B harness — file:// URI of the SAME capture stitched by the
|
|
72
|
-
* OPPOSITE pipeline (manual cv::detail + plane). When set, a toggle appears
|
|
73
|
-
* that flips the displayed panorama between the primary (high-level +
|
|
74
|
-
* spherical) and this one, for on-device comparison on a single capture.
|
|
75
|
-
* Its dimensions are read at runtime via `Image.getSize`. When the manual
|
|
76
|
-
* output is showing, the crop quad is hidden and the accept button emits
|
|
77
|
-
* THIS uri (so you can pick the better pipeline per capture).
|
|
78
|
-
*/
|
|
79
|
-
altImageUri?: string;
|
|
80
|
-
/**
|
|
81
|
-
* 2026-06-15 — ON-DEMAND alt (high-level) stitch. The PRIMARY image is the
|
|
82
|
-
* MANUAL pipeline (the default output); this callback re-stitches the SAME
|
|
83
|
-
* captured keyframes via cv::Stitcher and resolves with a file:// uri (or
|
|
84
|
-
* null on failure). It runs only the FIRST time the user taps the
|
|
85
|
-
* "High-level" tab — nothing is computed unless asked for. When provided (or
|
|
86
|
-
* `altImageUri` is), the A/B toggle appears.
|
|
87
|
-
*
|
|
88
|
-
* Resolves with the high-level output's file:// `uri` AND its OWN
|
|
89
|
-
* DEV-overlay `debugInfo` recipe (so the params pill can switch to the
|
|
90
|
-
* high-level recipe while that tab is viewed), or `null` on failure.
|
|
91
|
-
*/
|
|
92
|
-
onRequestAlt?: () => Promise<{
|
|
93
|
-
uri: string;
|
|
94
|
-
debugInfo: string;
|
|
95
|
-
} | null>;
|
|
96
70
|
/** Show / hide the editor. */
|
|
97
71
|
visible: boolean;
|
|
98
72
|
/**
|
|
@@ -136,9 +110,9 @@ export interface RectCropPreviewProps {
|
|
|
136
110
|
copy?: Partial<GuidanceCopy>;
|
|
137
111
|
/**
|
|
138
112
|
* Safe-area insets (px). The editor is a full-screen Modal, so the host
|
|
139
|
-
* passes `insets.top`/`insets.bottom` to keep the top toolbar (
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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.
|
|
142
116
|
*/
|
|
143
117
|
topInset?: number;
|
|
144
118
|
bottomInset?: number;
|