react-native-image-stitcher 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +316 -1
- package/README.md +1 -1
- package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
- package/cpp/stitcher.cpp +101 -1
- package/cpp/stitcher.hpp +8 -0
- package/dist/camera/Camera.d.ts +9 -0
- package/dist/camera/Camera.js +118 -8
- package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
- package/dist/camera/CaptureDebugOverlay.js +146 -0
- package/dist/camera/CaptureKeyframePill.d.ts +28 -0
- package/dist/camera/CaptureKeyframePill.js +60 -0
- package/dist/camera/CaptureMemoryPill.d.ts +28 -0
- package/dist/camera/CaptureMemoryPill.js +109 -0
- package/dist/camera/CaptureOrientationPill.d.ts +22 -0
- package/dist/camera/CaptureOrientationPill.js +44 -0
- package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
- package/dist/camera/CaptureStitchStatsToast.js +133 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +6 -5
- package/dist/index.d.ts +10 -0
- package/dist/index.js +15 -1
- package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
- package/dist/sensors/useIMUTranslationGate.js +83 -1
- package/dist/stitching/incremental.d.ts +25 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
- package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
- package/package.json +1 -1
- package/src/camera/Camera.tsx +165 -7
- package/src/camera/CaptureDebugOverlay.tsx +180 -0
- package/src/camera/CaptureKeyframePill.tsx +77 -0
- package/src/camera/CaptureMemoryPill.tsx +96 -0
- package/src/camera/CaptureOrientationPill.tsx +57 -0
- package/src/camera/CaptureStitchStatsToast.tsx +155 -0
- package/src/camera/PanoramaSettingsModal.tsx +6 -5
- package/src/index.ts +19 -0
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
package/dist/camera/Camera.js
CHANGED
|
@@ -84,6 +84,11 @@ const ARCameraView_1 = require("./ARCameraView");
|
|
|
84
84
|
const CameraShutter_1 = require("./CameraShutter");
|
|
85
85
|
const CameraView_1 = require("./CameraView");
|
|
86
86
|
const CaptureStatusOverlay_1 = require("./CaptureStatusOverlay");
|
|
87
|
+
const CaptureDebugOverlay_1 = require("./CaptureDebugOverlay");
|
|
88
|
+
const CaptureMemoryPill_1 = require("./CaptureMemoryPill");
|
|
89
|
+
const CaptureKeyframePill_1 = require("./CaptureKeyframePill");
|
|
90
|
+
const CaptureOrientationPill_1 = require("./CaptureOrientationPill");
|
|
91
|
+
const CaptureStitchStatsToast_1 = require("./CaptureStitchStatsToast");
|
|
87
92
|
const PanoramaBandOverlay_1 = require("./PanoramaBandOverlay");
|
|
88
93
|
const PanoramaSettingsModal_1 = require("./PanoramaSettingsModal");
|
|
89
94
|
const useCapture_1 = require("./useCapture");
|
|
@@ -273,6 +278,11 @@ function Camera(props) {
|
|
|
273
278
|
const [statusPhase, setStatusPhase] = (0, react_1.useState)('idle');
|
|
274
279
|
const [recordingStartedAt, setRecordingStartedAt] = (0, react_1.useState)(null);
|
|
275
280
|
const [incrementalState, setIncrementalState] = (0, react_1.useState)(null);
|
|
281
|
+
// 2026-05-22 (audit F9 + F3) — debug stitch-stats toast. Hook
|
|
282
|
+
// exposes an imperative API; we fire `showResult(finalizeResult)`
|
|
283
|
+
// on every successful finalize when settings.debug is on (gated
|
|
284
|
+
// a few hundred lines below in handleHoldEnd's onCapture branch).
|
|
285
|
+
const stitchToast = (0, CaptureStitchStatsToast_1.useStitchStatsToast)();
|
|
276
286
|
const [batchKeyframeThumbnails, setBatchKeyframeThumbnails] = (0, react_1.useState)([]);
|
|
277
287
|
const [cameraTransitioning, setCameraTransitioning] = (0, react_1.useState)(false);
|
|
278
288
|
// ARKit / ARCore device-support probe. `isAvailable` is `false`
|
|
@@ -380,6 +390,15 @@ function Camera(props) {
|
|
|
380
390
|
// the C++ engine to force-accept the next frame. This is what
|
|
381
391
|
// keeps non-AR captures producing keyframes at all (the flow-
|
|
382
392
|
// novelty algorithm alone is too strict in practice).
|
|
393
|
+
//
|
|
394
|
+
// 2026-05-22 (audit F2f) — IMU translation gate. The gate's own
|
|
395
|
+
// `totalAbsMetres` accumulator (banks each segment's |displacement|
|
|
396
|
+
// at every anchor reset) is the right input for the finalize-time
|
|
397
|
+
// auto-resolver in non-AR mode (where pose-derived translation is
|
|
398
|
+
// 0). Pre-F2f this was reconstructed from `fires × budget +
|
|
399
|
+
// |residual|` — which undercounted any time a non-IMU accept
|
|
400
|
+
// (flow novelty, force-last) reset the integrator before the
|
|
401
|
+
// budget threshold was reached.
|
|
383
402
|
const imuGate = (0, useIMUTranslationGate_1.useIMUTranslationGate)({
|
|
384
403
|
enabled: isNonAR
|
|
385
404
|
&& statusPhase === 'recording'
|
|
@@ -430,12 +449,52 @@ function Camera(props) {
|
|
|
430
449
|
});
|
|
431
450
|
return () => { sub?.remove?.(); };
|
|
432
451
|
}, []);
|
|
452
|
+
// 2026-05-23 (race fix) — Previously this useEffect cleared
|
|
453
|
+
// `batchKeyframeThumbnails` + `incrementalState` when statusPhase
|
|
454
|
+
// transitioned to 'recording'. But handleHoldStart is async
|
|
455
|
+
// (`await incremental.start(...)`), and on Android the ARSession
|
|
456
|
+
// was already alive on the GL thread — it could emit an ACCEPT
|
|
457
|
+
// event during the await window, BEFORE the effect ran. Order
|
|
458
|
+
// observed in logcat:
|
|
459
|
+
// 1. setStatusPhase('recording') queued
|
|
460
|
+
// 2. await incremental.start() yields
|
|
461
|
+
// 3. ARCore frame → ingest → JS [state] emit
|
|
462
|
+
// 4. setBatchKeyframeThumbnails((prev=[]) => [keyframe-0.jpg])
|
|
463
|
+
// 5. React commits statusPhase change → THIS effect ran
|
|
464
|
+
// 6. setBatchKeyframeThumbnails([]) ← WIPED frame 0!
|
|
465
|
+
// 7. Frame 1 arrives → updater sees prev=[] → adds only frame 1
|
|
466
|
+
// ⇒ final array missing keyframe-0.jpg
|
|
467
|
+
// The reset is now done synchronously at the top of
|
|
468
|
+
// handleHoldStart, before any await, so the GL thread can't race
|
|
469
|
+
// ahead. This effect is intentionally removed.
|
|
470
|
+
// 2026-05-22 (audit F2f) — every accepted keyframe is a fresh
|
|
471
|
+
// anchor for the IMU translation gate, regardless of which
|
|
472
|
+
// mechanism qualified the frame (flow novelty, plane-overlap,
|
|
473
|
+
// angular fallback, IMU-budget force-accept, force-last). Reset
|
|
474
|
+
// the gate's per-segment integrator on every acceptedCount
|
|
475
|
+
// increment so the operator sees `imuΔ` reset to 0 in the debug
|
|
476
|
+
// overlay after every accept — consistent UX regardless of WHY
|
|
477
|
+
// the gate took the frame. Pre-F2f only the IMU-budget path
|
|
478
|
+
// reset the integrator; flow accepts left `posX` ticking up
|
|
479
|
+
// forever, which surprised the user.
|
|
480
|
+
//
|
|
481
|
+
// The gate's `totalAbsMetres` cumulative accumulator banks the
|
|
482
|
+
// |segment displacement| before zeroing, so finalize-time
|
|
483
|
+
// translation magnitude is preserved across non-IMU accepts.
|
|
484
|
+
const lastAcceptedCountRef = (0, react_1.useRef)(0);
|
|
433
485
|
(0, react_1.useEffect)(() => {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
486
|
+
const accepted = incrementalState?.acceptedCount ?? 0;
|
|
487
|
+
if (accepted > lastAcceptedCountRef.current) {
|
|
488
|
+
lastAcceptedCountRef.current = accepted;
|
|
489
|
+
if (isNonAR) {
|
|
490
|
+
imuGate.resetAnchor();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (accepted === 0) {
|
|
494
|
+
// New capture (state cleared) — reset our edge-detect ref.
|
|
495
|
+
lastAcceptedCountRef.current = 0;
|
|
437
496
|
}
|
|
438
|
-
}, [
|
|
497
|
+
}, [incrementalState?.acceptedCount, isNonAR, imuGate]);
|
|
439
498
|
// ── Shutter handlers ────────────────────────────────────────────
|
|
440
499
|
const handleTap = (0, react_1.useCallback)(async () => {
|
|
441
500
|
if (!enablePhotoMode)
|
|
@@ -503,6 +562,17 @@ function Camera(props) {
|
|
|
503
562
|
return;
|
|
504
563
|
}
|
|
505
564
|
try {
|
|
565
|
+
// 2026-05-23 (race fix) — synchronously clear thumbnails +
|
|
566
|
+
// engine state at the top of handleHoldStart, BEFORE awaiting
|
|
567
|
+
// incremental.start(). In the previous effect-based design
|
|
568
|
+
// the GL thread could ingest an AR frame during the await
|
|
569
|
+
// window and add to thumbnails BEFORE React's
|
|
570
|
+
// statusPhase-effect ran and wiped them. See the removed
|
|
571
|
+
// useEffect a few hundred lines above for the full log trace.
|
|
572
|
+
// Synchronous reset here means any racing frame ingest sees
|
|
573
|
+
// an empty array and accumulates from there.
|
|
574
|
+
setBatchKeyframeThumbnails([]);
|
|
575
|
+
setIncrementalState(null);
|
|
506
576
|
setStatusPhase('recording');
|
|
507
577
|
setRecordingStartedAt(Date.now());
|
|
508
578
|
const orientationRotation = deviceOrientation === 'portrait' ? 90
|
|
@@ -520,16 +590,35 @@ function Camera(props) {
|
|
|
520
590
|
canvasHeight: 5000,
|
|
521
591
|
engine: 'batch-keyframe',
|
|
522
592
|
config: {
|
|
593
|
+
// ── cv::Stitcher (batch finalize) ─────────────────────────
|
|
523
594
|
stitchMode: settings.stitchMode,
|
|
524
595
|
warperType: settings.warperType,
|
|
525
596
|
blenderType: settings.blenderType,
|
|
526
597
|
seamFinderType: settings.seamFinderType,
|
|
598
|
+
enableMaxInscribedRectCrop: settings.enableMaxInscribedRectCrop,
|
|
599
|
+
// ── KeyframeGate (per-frame selection) ────────────────────
|
|
600
|
+
// F6 audit fix: pass settings.frameSelectionMode through
|
|
601
|
+
// instead of hardcoding 'flow-based' (which silently made the
|
|
602
|
+
// time-based / pose-based modal options no-ops).
|
|
603
|
+
frameSelectionMode: settings.frameSelectionMode,
|
|
604
|
+
keyframeMaxCount: settings.keyframeMaxCount,
|
|
605
|
+
keyframeOverlapThreshold: settings.keyframeOverlapThreshold,
|
|
606
|
+
// ── Flow-strategy tunables ────────────────────────────────
|
|
607
|
+
// F4 audit fix: previously omitted, which made the modal
|
|
608
|
+
// sliders for these three a complete no-op (only iOS native
|
|
609
|
+
// even read them, and only when JS sent them).
|
|
527
610
|
flowNoveltyPercentile: settings.flowNoveltyPercentile,
|
|
528
611
|
flowEvalEveryNFrames: settings.flowEvalEveryNFrames,
|
|
529
612
|
flowMaxTranslationCm: settings.flowMaxTranslationCm,
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
613
|
+
flowMaxCorners: settings.flowMaxCorners,
|
|
614
|
+
flowQualityLevel: settings.flowQualityLevel,
|
|
615
|
+
flowMinDistance: settings.flowMinDistance,
|
|
616
|
+
// ── Engine-routing flags consumed by native ───────────────
|
|
617
|
+
// F1 audit fix: Android keyframe gate's disableAngularFallback
|
|
618
|
+
// opt-out reads this to decide whether to skip the angular
|
|
619
|
+
// fallback (gyro pose is too noisy for the FoV-overlap calc
|
|
620
|
+
// in non-AR mode, causing degenerate cv::Stitcher params).
|
|
621
|
+
captureSource: settings.captureSource,
|
|
533
622
|
},
|
|
534
623
|
});
|
|
535
624
|
imuGate.resetAnchor();
|
|
@@ -574,7 +663,14 @@ function Camera(props) {
|
|
|
574
663
|
const panoOutputPath = outputDir
|
|
575
664
|
? `${(0, paths_1.toBareFilePath)(outputDir).replace(/\/$/, '')}/${(0, files_1.defaultPanoramaFilename)()}`
|
|
576
665
|
: `${await (0, files_1.getDefaultCaptureDir)()}/${(0, files_1.defaultPanoramaFilename)()}`;
|
|
577
|
-
|
|
666
|
+
// 2026-05-22 (audit F2f) — total IMU translation directly from
|
|
667
|
+
// the gate's cumulative accumulator (banks |segment displacement|
|
|
668
|
+
// at every anchor reset, including non-IMU-driven resets like
|
|
669
|
+
// flow-novelty accepts). No more fires × budget + residual
|
|
670
|
+
// reconstruction. Only meaningful in non-AR mode (in AR the
|
|
671
|
+
// native side uses pose-derived translation and ignores this).
|
|
672
|
+
const imuTotalTranslationM = isNonAR ? imuGate.getTotalAbsMetres() : 0;
|
|
673
|
+
const result = await incremental.finalize(panoOutputPath, 90, deviceOrientation, imuTotalTranslationM);
|
|
578
674
|
if (typeof result.framesRequested === 'number'
|
|
579
675
|
&& typeof result.framesIncluded === 'number'
|
|
580
676
|
&& result.framesIncluded < result.framesRequested) {
|
|
@@ -595,7 +691,15 @@ function Camera(props) {
|
|
|
595
691
|
framesDropped: (result.framesRequested ?? 0) - (result.framesIncluded ?? 0),
|
|
596
692
|
finalConfidenceThresh: result.finalConfidenceThresh ?? -1,
|
|
597
693
|
durationMs: Date.now() - (recordingStartedAt ?? Date.now()),
|
|
694
|
+
stitchModeResolved: result.stitchModeResolved,
|
|
598
695
|
});
|
|
696
|
+
// 2026-05-22 (audit F9) — fire the debug stitch-stats toast on
|
|
697
|
+
// every successful finalize when settings.debug is on. Shows
|
|
698
|
+
// the leaveBiggestComponent retry telemetry + resolved mode so
|
|
699
|
+
// the operator can see what choice the auto-resolver made.
|
|
700
|
+
if (settings.debug) {
|
|
701
|
+
stitchToast.showResult(result);
|
|
702
|
+
}
|
|
599
703
|
}
|
|
600
704
|
catch (err) {
|
|
601
705
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -641,6 +745,12 @@ function Camera(props) {
|
|
|
641
745
|
// which has run on `video` (true) for months without issue.
|
|
642
746
|
video: true, flash: "off", style: react_native_1.StyleSheet.absoluteFill })),
|
|
643
747
|
react_1.default.createElement(CaptureStatusOverlay_1.CaptureStatusOverlay, { phase: statusPhase, topInset: insets.top, recordingStartedAt: recordingStartedAt ?? undefined }),
|
|
748
|
+
settings.debug && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
749
|
+
react_1.default.createElement(CaptureOrientationPill_1.CaptureOrientationPill, { orientation: deviceOrientation, topInset: insets.top }),
|
|
750
|
+
react_1.default.createElement(CaptureKeyframePill_1.CaptureKeyframePill, { state: incrementalState, topInset: insets.top }),
|
|
751
|
+
react_1.default.createElement(CaptureMemoryPill_1.CaptureMemoryPill, { topInset: insets.top }),
|
|
752
|
+
react_1.default.createElement(CaptureDebugOverlay_1.CaptureDebugOverlay, { incrementalState: incrementalState, imuTranslationMetres: isNonAR ? imuGate.getTranslationMetres() : null, captureSource: effectiveCaptureSource, frameSelectionMode: settings.frameSelectionMode, stitchMode: settings.stitchMode }))),
|
|
753
|
+
react_1.default.createElement(CaptureStitchStatsToast_1.CaptureStitchStatsToast, { message: stitchToast.message, topInset: insets.top }),
|
|
644
754
|
showSettingsButton && (react_1.default.createElement(SettingsButton, { topInset: insets.top, onPress: () => setSettingsModalVisible(true) })),
|
|
645
755
|
react_1.default.createElement(react_native_1.View, { pointerEvents: "box-none", style: [styles.bottomArea, { paddingBottom: insets.bottom + 12 }] },
|
|
646
756
|
statusPhase === 'recording' && (react_1.default.createElement(PanoramaBandOverlay_1.PanoramaBandOverlay, { state: incrementalState, frameUris: batchKeyframeThumbnails, captureOrientation: deviceOrientation })),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureDebugOverlay — diagnostic overlay for capture sessions.
|
|
3
|
+
*
|
|
4
|
+
* Shows the live engine state in a floating pill at the top of the
|
|
5
|
+
* capture screen so operators can see:
|
|
6
|
+
*
|
|
7
|
+
* - which frame outcome the engine just emitted (accept/skip/reject)
|
|
8
|
+
* - keyframe count vs. cap (e.g. "3 / 6")
|
|
9
|
+
* - per-frame newContent fraction + overlap percent
|
|
10
|
+
* - latest processingMs (how long the gate eval took)
|
|
11
|
+
* - JS-side IMU translation accumulator (when non-AR)
|
|
12
|
+
* - JS heap usage estimate (rough — RN doesn't expose Native heap)
|
|
13
|
+
*
|
|
14
|
+
* The overlay is gated by `<Camera>`'s `settings.debug` flag. When
|
|
15
|
+
* `debug = false` the component renders null and consumes no CPU.
|
|
16
|
+
*
|
|
17
|
+
* Why a separate component (not inline in Camera.tsx)?
|
|
18
|
+
*
|
|
19
|
+
* Camera.tsx is already a 1200-line beast and the debug pill needs
|
|
20
|
+
* its own styling/layout that would distract from the main capture
|
|
21
|
+
* UX. Splitting it out keeps Camera.tsx focused and the debug
|
|
22
|
+
* surface easy to evolve independently (future F9 work — port the
|
|
23
|
+
* richer memory bubble + stitch toast from the RetaiLens host).
|
|
24
|
+
*
|
|
25
|
+
* This component is intentionally PRESENTATIONAL — all data is
|
|
26
|
+
* pushed in as props. The host (Camera.tsx) owns the
|
|
27
|
+
* subscriptions / refs / state and decides when to mount the
|
|
28
|
+
* overlay.
|
|
29
|
+
*/
|
|
30
|
+
import React from 'react';
|
|
31
|
+
import type { IncrementalState } from '../stitching/incremental';
|
|
32
|
+
export interface CaptureDebugOverlayProps {
|
|
33
|
+
/** Latest engine state (null = no capture in progress). */
|
|
34
|
+
incrementalState: IncrementalState | null;
|
|
35
|
+
/** JS-side IMU translation accumulator in metres (non-AR mode). */
|
|
36
|
+
imuTranslationMetres?: number | null;
|
|
37
|
+
/** Capture-source label so the operator knows which gate path is live. */
|
|
38
|
+
captureSource: 'ar' | 'non-ar';
|
|
39
|
+
/** Effective frame selection mode that's running right now. */
|
|
40
|
+
frameSelectionMode: 'time-based' | 'pose-based' | 'flow-based';
|
|
41
|
+
/** Effective stitchMode setting (operator-set, before auto-resolution). */
|
|
42
|
+
stitchMode: 'auto' | 'panorama' | 'scans';
|
|
43
|
+
}
|
|
44
|
+
export declare function CaptureDebugOverlay({ incrementalState, imuTranslationMetres, captureSource, frameSelectionMode, stitchMode, }: CaptureDebugOverlayProps): React.JSX.Element;
|
|
45
|
+
//# sourceMappingURL=CaptureDebugOverlay.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureDebugOverlay — diagnostic overlay for capture sessions.
|
|
5
|
+
*
|
|
6
|
+
* Shows the live engine state in a floating pill at the top of the
|
|
7
|
+
* capture screen so operators can see:
|
|
8
|
+
*
|
|
9
|
+
* - which frame outcome the engine just emitted (accept/skip/reject)
|
|
10
|
+
* - keyframe count vs. cap (e.g. "3 / 6")
|
|
11
|
+
* - per-frame newContent fraction + overlap percent
|
|
12
|
+
* - latest processingMs (how long the gate eval took)
|
|
13
|
+
* - JS-side IMU translation accumulator (when non-AR)
|
|
14
|
+
* - JS heap usage estimate (rough — RN doesn't expose Native heap)
|
|
15
|
+
*
|
|
16
|
+
* The overlay is gated by `<Camera>`'s `settings.debug` flag. When
|
|
17
|
+
* `debug = false` the component renders null and consumes no CPU.
|
|
18
|
+
*
|
|
19
|
+
* Why a separate component (not inline in Camera.tsx)?
|
|
20
|
+
*
|
|
21
|
+
* Camera.tsx is already a 1200-line beast and the debug pill needs
|
|
22
|
+
* its own styling/layout that would distract from the main capture
|
|
23
|
+
* UX. Splitting it out keeps Camera.tsx focused and the debug
|
|
24
|
+
* surface easy to evolve independently (future F9 work — port the
|
|
25
|
+
* richer memory bubble + stitch toast from the RetaiLens host).
|
|
26
|
+
*
|
|
27
|
+
* This component is intentionally PRESENTATIONAL — all data is
|
|
28
|
+
* pushed in as props. The host (Camera.tsx) owns the
|
|
29
|
+
* subscriptions / refs / state and decides when to mount the
|
|
30
|
+
* overlay.
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CaptureDebugOverlay = CaptureDebugOverlay;
|
|
37
|
+
const react_1 = __importDefault(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
/**
|
|
40
|
+
* Map the numeric `outcome` enum to a short human label. Mirrors
|
|
41
|
+
* the iOS/Android C++ enum. Hidden in production builds — only
|
|
42
|
+
* surfaced via this debug overlay.
|
|
43
|
+
*/
|
|
44
|
+
function outcomeLabel(outcome) {
|
|
45
|
+
switch (outcome) {
|
|
46
|
+
case 1: return 'accept';
|
|
47
|
+
case 2: return 'reject';
|
|
48
|
+
case 3: return 'cap-hit';
|
|
49
|
+
default: return outcome == null ? '—' : String(outcome);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function CaptureDebugOverlay({ incrementalState, imuTranslationMetres, captureSource, frameSelectionMode, stitchMode, }) {
|
|
53
|
+
const accepted = incrementalState?.acceptedCount ?? 0;
|
|
54
|
+
const cap = incrementalState?.keyframeMax ?? 0;
|
|
55
|
+
const overlap = incrementalState?.overlapPercent;
|
|
56
|
+
const proc = incrementalState?.processingMs;
|
|
57
|
+
const outcome = outcomeLabel(incrementalState?.outcome);
|
|
58
|
+
const isLandscape = incrementalState?.isLandscape;
|
|
59
|
+
const painted = incrementalState?.paintedExtent ?? 0;
|
|
60
|
+
const panTotal = incrementalState?.panExtent ?? 0;
|
|
61
|
+
const fillPct = panTotal > 0 ? Math.round((painted / panTotal) * 100) : 0;
|
|
62
|
+
// Translation pill is only meaningful in non-AR mode (in AR the
|
|
63
|
+
// engine's own pose is the source of truth; we don't surface the
|
|
64
|
+
// tx/ty/tz separately because the operator can't act on them).
|
|
65
|
+
const showImu = captureSource === 'non-ar' && imuTranslationMetres != null;
|
|
66
|
+
const imuCm = showImu ? (imuTranslationMetres * 100).toFixed(1) : null;
|
|
67
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: styles.container },
|
|
68
|
+
react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
69
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.label },
|
|
70
|
+
captureSource,
|
|
71
|
+
"/",
|
|
72
|
+
frameSelectionMode,
|
|
73
|
+
"/",
|
|
74
|
+
stitchMode)),
|
|
75
|
+
react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
76
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricKey }, "frames"),
|
|
77
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricVal },
|
|
78
|
+
accepted,
|
|
79
|
+
cap > 0 ? ` / ${cap}` : '')),
|
|
80
|
+
react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
81
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricKey }, "last"),
|
|
82
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricVal }, outcome)),
|
|
83
|
+
(overlap != null && overlap >= 0) && (react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
84
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricKey }, "overlap"),
|
|
85
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricVal },
|
|
86
|
+
overlap.toFixed(0),
|
|
87
|
+
"%"))),
|
|
88
|
+
(proc != null && proc > 0) && (react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
89
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricKey }, "proc"),
|
|
90
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricVal },
|
|
91
|
+
proc.toFixed(0),
|
|
92
|
+
"ms"))),
|
|
93
|
+
panTotal > 0 && (react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
94
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricKey }, "pan"),
|
|
95
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricVal },
|
|
96
|
+
fillPct,
|
|
97
|
+
"% (",
|
|
98
|
+
isLandscape ? 'L' : 'P',
|
|
99
|
+
")"))),
|
|
100
|
+
showImu && (react_1.default.createElement(react_native_1.View, { style: styles.row },
|
|
101
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricKey }, "imu\u0394"),
|
|
102
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.metricVal },
|
|
103
|
+
imuCm,
|
|
104
|
+
"cm")))));
|
|
105
|
+
}
|
|
106
|
+
const styles = react_native_1.StyleSheet.create({
|
|
107
|
+
container: {
|
|
108
|
+
position: 'absolute',
|
|
109
|
+
// 2026-05-22 — moved from top-left to left-middle so it doesn't
|
|
110
|
+
// collide with the orientation pill (top-left) or the keyframe
|
|
111
|
+
// pill (top-center) when all three are mounted together in
|
|
112
|
+
// <Camera>'s debug mode.
|
|
113
|
+
top: 160,
|
|
114
|
+
left: 12,
|
|
115
|
+
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
|
116
|
+
paddingHorizontal: 10,
|
|
117
|
+
paddingVertical: 6,
|
|
118
|
+
borderRadius: 8,
|
|
119
|
+
minWidth: 130,
|
|
120
|
+
},
|
|
121
|
+
row: {
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
justifyContent: 'space-between',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
marginVertical: 1,
|
|
126
|
+
},
|
|
127
|
+
label: {
|
|
128
|
+
color: '#fff',
|
|
129
|
+
fontSize: 11,
|
|
130
|
+
fontWeight: '600',
|
|
131
|
+
fontFamily: 'Menlo',
|
|
132
|
+
},
|
|
133
|
+
metricKey: {
|
|
134
|
+
color: '#9aa',
|
|
135
|
+
fontSize: 10,
|
|
136
|
+
fontFamily: 'Menlo',
|
|
137
|
+
marginRight: 8,
|
|
138
|
+
},
|
|
139
|
+
metricVal: {
|
|
140
|
+
color: '#fff',
|
|
141
|
+
fontSize: 11,
|
|
142
|
+
fontWeight: '600',
|
|
143
|
+
fontFamily: 'Menlo',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=CaptureDebugOverlay.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureKeyframePill — top-center "Keyframes: N/M" diagnostic pill.
|
|
3
|
+
*
|
|
4
|
+
* Renders while a capture is in flight AND the engine is running
|
|
5
|
+
* the pose-driven / flow-driven keyframe gate (keyframeMax > 0).
|
|
6
|
+
* Hidden when the gate is disabled (time-based frame selection) or
|
|
7
|
+
* when no capture is active.
|
|
8
|
+
*
|
|
9
|
+
* Color-coded by closeness to the cap:
|
|
10
|
+
*
|
|
11
|
+
* - green N < M − 1 (plenty of budget remaining)
|
|
12
|
+
* - amber N ≥ M − 1 (last frame, or cap already hit — next
|
|
13
|
+
* accept will be rejected)
|
|
14
|
+
*
|
|
15
|
+
* Layer-2 hosts that compose their own capture UI can mount this
|
|
16
|
+
* pill directly; Layer-1 `<Camera>` mounts it automatically when
|
|
17
|
+
* `settings.debug = true`.
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import type { IncrementalState } from '../stitching/incremental';
|
|
21
|
+
export interface CaptureKeyframePillProps {
|
|
22
|
+
/** Latest engine state. Null = capture not running. */
|
|
23
|
+
state: IncrementalState | null;
|
|
24
|
+
/** Top inset for safe-area placement. Pill pinned `topInset + 56`. */
|
|
25
|
+
topInset?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function CaptureKeyframePill({ state, topInset, }: CaptureKeyframePillProps): React.JSX.Element | null;
|
|
28
|
+
//# sourceMappingURL=CaptureKeyframePill.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureKeyframePill — top-center "Keyframes: N/M" diagnostic pill.
|
|
5
|
+
*
|
|
6
|
+
* Renders while a capture is in flight AND the engine is running
|
|
7
|
+
* the pose-driven / flow-driven keyframe gate (keyframeMax > 0).
|
|
8
|
+
* Hidden when the gate is disabled (time-based frame selection) or
|
|
9
|
+
* when no capture is active.
|
|
10
|
+
*
|
|
11
|
+
* Color-coded by closeness to the cap:
|
|
12
|
+
*
|
|
13
|
+
* - green N < M − 1 (plenty of budget remaining)
|
|
14
|
+
* - amber N ≥ M − 1 (last frame, or cap already hit — next
|
|
15
|
+
* accept will be rejected)
|
|
16
|
+
*
|
|
17
|
+
* Layer-2 hosts that compose their own capture UI can mount this
|
|
18
|
+
* pill directly; Layer-1 `<Camera>` mounts it automatically when
|
|
19
|
+
* `settings.debug = true`.
|
|
20
|
+
*/
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.CaptureKeyframePill = CaptureKeyframePill;
|
|
26
|
+
const react_1 = __importDefault(require("react"));
|
|
27
|
+
const react_native_1 = require("react-native");
|
|
28
|
+
function CaptureKeyframePill({ state, topInset = 0, }) {
|
|
29
|
+
const accepted = state?.acceptedCount ?? 0;
|
|
30
|
+
const max = state?.keyframeMax ?? 0;
|
|
31
|
+
if (max <= 0)
|
|
32
|
+
return null;
|
|
33
|
+
const isAmber = accepted >= max - 1;
|
|
34
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
35
|
+
styles.container,
|
|
36
|
+
{
|
|
37
|
+
top: topInset + 56,
|
|
38
|
+
backgroundColor: isAmber
|
|
39
|
+
? 'rgba(245, 158, 11, 0.95)'
|
|
40
|
+
: 'rgba(34, 197, 94, 0.95)',
|
|
41
|
+
},
|
|
42
|
+
], accessibilityRole: "alert", accessibilityLiveRegion: "polite" },
|
|
43
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.text }, `Keyframes: ${accepted}/${max}`)));
|
|
44
|
+
}
|
|
45
|
+
const styles = react_native_1.StyleSheet.create({
|
|
46
|
+
container: {
|
|
47
|
+
position: 'absolute',
|
|
48
|
+
alignSelf: 'center',
|
|
49
|
+
paddingHorizontal: 14,
|
|
50
|
+
paddingVertical: 6,
|
|
51
|
+
borderRadius: 999,
|
|
52
|
+
zIndex: 100,
|
|
53
|
+
},
|
|
54
|
+
text: {
|
|
55
|
+
color: '#fff',
|
|
56
|
+
fontSize: 13,
|
|
57
|
+
fontWeight: '600',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=CaptureKeyframePill.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureMemoryPill — top-right diagnostic pill showing native
|
|
3
|
+
* process memory footprint in MB, polled at 500 ms.
|
|
4
|
+
*
|
|
5
|
+
* Color-coded against the iPhone 16 Pro per-process jetsam limit:
|
|
6
|
+
*
|
|
7
|
+
* - green <1500 MB (comfortable)
|
|
8
|
+
* - amber 1500–2200 (approaching pressure)
|
|
9
|
+
* - red >2200 (close to limit — capture may be killed)
|
|
10
|
+
*
|
|
11
|
+
* Backed by the existing `getMemoryFootprintMB()` native module
|
|
12
|
+
* (iOS: `task_info phys_footprint`, Android: `Debug.MemoryInfo
|
|
13
|
+
* getTotalPss * 1024`). Returns -1 if the native call fails.
|
|
14
|
+
*
|
|
15
|
+
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
16
|
+
* polls native every 500 ms and is unwanted in production builds.
|
|
17
|
+
*/
|
|
18
|
+
import React from 'react';
|
|
19
|
+
export interface CaptureMemoryPillProps {
|
|
20
|
+
/** Top inset (status bar / notch). Pill pinned `topInset + 56`. */
|
|
21
|
+
topInset?: number;
|
|
22
|
+
/** Polling interval in ms. Default 500. Lower wastes battery
|
|
23
|
+
* for no visible benefit; higher loses correlation with capture
|
|
24
|
+
* activity. */
|
|
25
|
+
pollIntervalMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function CaptureMemoryPill({ topInset, pollIntervalMs, }: CaptureMemoryPillProps): React.JSX.Element | null;
|
|
28
|
+
//# sourceMappingURL=CaptureMemoryPill.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CaptureMemoryPill — top-right diagnostic pill showing native
|
|
5
|
+
* process memory footprint in MB, polled at 500 ms.
|
|
6
|
+
*
|
|
7
|
+
* Color-coded against the iPhone 16 Pro per-process jetsam limit:
|
|
8
|
+
*
|
|
9
|
+
* - green <1500 MB (comfortable)
|
|
10
|
+
* - amber 1500–2200 (approaching pressure)
|
|
11
|
+
* - red >2200 (close to limit — capture may be killed)
|
|
12
|
+
*
|
|
13
|
+
* Backed by the existing `getMemoryFootprintMB()` native module
|
|
14
|
+
* (iOS: `task_info phys_footprint`, Android: `Debug.MemoryInfo
|
|
15
|
+
* getTotalPss * 1024`). Returns -1 if the native call fails.
|
|
16
|
+
*
|
|
17
|
+
* Mount this pill inside a `settings.debug`-gated branch — it
|
|
18
|
+
* polls native every 500 ms and is unwanted in production builds.
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.CaptureMemoryPill = CaptureMemoryPill;
|
|
55
|
+
const react_1 = __importStar(require("react"));
|
|
56
|
+
const react_native_1 = require("react-native");
|
|
57
|
+
const incremental_1 = require("../stitching/incremental");
|
|
58
|
+
function CaptureMemoryPill({ topInset = 0, pollIntervalMs = 500, }) {
|
|
59
|
+
const [memMB, setMemMB] = (0, react_1.useState)(null);
|
|
60
|
+
(0, react_1.useEffect)(() => {
|
|
61
|
+
const native = (0, incremental_1.getIncrementalNativeModule)();
|
|
62
|
+
if (!native?.getMemoryFootprintMB)
|
|
63
|
+
return undefined;
|
|
64
|
+
let cancelled = false;
|
|
65
|
+
const tick = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const mb = await native.getMemoryFootprintMB();
|
|
68
|
+
if (!cancelled)
|
|
69
|
+
setMemMB(mb);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Bridge error — leave the previous reading visible.
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
tick();
|
|
76
|
+
const id = setInterval(tick, pollIntervalMs);
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
clearInterval(id);
|
|
80
|
+
};
|
|
81
|
+
}, [pollIntervalMs]);
|
|
82
|
+
if (memMB === null || memMB < 0)
|
|
83
|
+
return null;
|
|
84
|
+
const bg = memMB > 2200 ? 'rgba(239, 68, 68, 0.92)' // red
|
|
85
|
+
: memMB > 1500 ? 'rgba(245, 158, 11, 0.92)' // amber
|
|
86
|
+
: 'rgba(34, 197, 94, 0.92)'; // green
|
|
87
|
+
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
88
|
+
styles.container,
|
|
89
|
+
{ top: topInset + 56, backgroundColor: bg },
|
|
90
|
+
], accessibilityRole: "alert" },
|
|
91
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.text }, `${Math.round(memMB)} MB`)));
|
|
92
|
+
}
|
|
93
|
+
const styles = react_native_1.StyleSheet.create({
|
|
94
|
+
container: {
|
|
95
|
+
position: 'absolute',
|
|
96
|
+
right: 12,
|
|
97
|
+
paddingHorizontal: 10,
|
|
98
|
+
paddingVertical: 5,
|
|
99
|
+
borderRadius: 999,
|
|
100
|
+
zIndex: 100,
|
|
101
|
+
},
|
|
102
|
+
text: {
|
|
103
|
+
color: '#fff',
|
|
104
|
+
fontSize: 12,
|
|
105
|
+
fontWeight: '700',
|
|
106
|
+
fontFamily: 'Menlo',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=CaptureMemoryPill.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaptureOrientationPill — diagnostic pill showing the operator's
|
|
3
|
+
* current hold orientation as detected by the pose-derived hook.
|
|
4
|
+
*
|
|
5
|
+
* Useful for diagnosing rotation issues — if the pill says
|
|
6
|
+
* `landscape-left` but the band overlay is rendering as if it's
|
|
7
|
+
* `portrait`, there's a mismatch between the JS orientation hook
|
|
8
|
+
* and the engine's pose-derived isLandscape signal.
|
|
9
|
+
*
|
|
10
|
+
* Pinned top-left below the status bar. Layer-2 hosts can mount
|
|
11
|
+
* this directly; Layer-1 `<Camera>` mounts it automatically when
|
|
12
|
+
* `settings.debug = true`.
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
export interface CaptureOrientationPillProps {
|
|
16
|
+
/** Current device orientation (typically from useDeviceOrientation). */
|
|
17
|
+
orientation: string;
|
|
18
|
+
/** Top inset for safe-area placement. Pill pinned `topInset + 56`. */
|
|
19
|
+
topInset?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function CaptureOrientationPill({ orientation, topInset, }: CaptureOrientationPillProps): React.JSX.Element;
|
|
22
|
+
//# sourceMappingURL=CaptureOrientationPill.d.ts.map
|