react-native-image-stitcher 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -1
- package/README.md +116 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/stitcher.cpp +651 -55
- package/cpp/stitcher.hpp +10 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +196 -12
- package/dist/camera/Camera.js +629 -35
- package/dist/camera/CameraView.js +62 -5
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +142 -0
- package/dist/camera/CaptureMemoryPill.d.ts +9 -1
- package/dist/camera/CaptureMemoryPill.js +3 -3
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +26 -5
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +161 -0
- package/dist/camera/RectCropPreview.js +480 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +45 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +191 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +994 -47
- package/src/camera/CameraView.tsx +75 -5
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
- package/src/camera/CaptureMemoryPill.tsx +17 -3
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaSettings.ts +34 -11
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +820 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +45 -0
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { Camera, CameraError } from './camera/Camera';
|
|
|
28
28
|
export type {
|
|
29
29
|
CameraProps,
|
|
30
30
|
CameraCaptureResult,
|
|
31
|
+
PanoramaCaptureResult,
|
|
31
32
|
CameraErrorCode,
|
|
32
33
|
CaptureSource,
|
|
33
34
|
CaptureSourcesMode,
|
|
@@ -38,12 +39,29 @@ export type {
|
|
|
38
39
|
Warper,
|
|
39
40
|
FramesDroppedInfo,
|
|
40
41
|
} from './camera/Camera';
|
|
42
|
+
// Non-fatal capture quality signals carried on `CameraCaptureResult.warnings`.
|
|
43
|
+
export type {
|
|
44
|
+
CaptureWarning,
|
|
45
|
+
CaptureWarningCode,
|
|
46
|
+
CaptureWarningCopy,
|
|
47
|
+
} from './camera/captureWarnings';
|
|
48
|
+
// Default English warning templates (single source of truth; re-used by
|
|
49
|
+
// `DEFAULT_GUIDANCE_COPY`). Exposed so a host can diff / extend them.
|
|
50
|
+
export { DEFAULT_CAPTURE_WARNING_COPY } from './camera/captureWarnings';
|
|
41
51
|
|
|
42
52
|
// Recoverable-stitch-failure → friendly Alert copy. Hosts call this in
|
|
43
53
|
// their onError handler to surface actionable guidance ("pan more slowly",
|
|
44
|
-
// "pivot in place") instead of the raw cv::Stitcher diagnostic.
|
|
45
|
-
|
|
46
|
-
export
|
|
54
|
+
// "pivot in place") instead of the raw cv::Stitcher diagnostic. Pass an
|
|
55
|
+
// `overrides` map (keyed by `RECOVERABLE_STITCH_CODES`) to localise it.
|
|
56
|
+
export {
|
|
57
|
+
userFacingStitchError,
|
|
58
|
+
RECOVERABLE_STITCH_GUIDANCE,
|
|
59
|
+
RECOVERABLE_STITCH_CODES,
|
|
60
|
+
} from './camera/cameraErrorMessages';
|
|
61
|
+
export type {
|
|
62
|
+
UserFacingStitchError,
|
|
63
|
+
UserFacingStitchErrorOverrides,
|
|
64
|
+
} from './camera/cameraErrorMessages';
|
|
47
65
|
|
|
48
66
|
// ─────────────────────────────────────────────────────────────────────
|
|
49
67
|
// AR foundation (public since 0.1.0)
|
|
@@ -174,6 +192,51 @@ export type { UseOrientationDriftReturn } from './camera/useOrientationDrift';
|
|
|
174
192
|
export { OrientationDriftModal } from './camera/OrientationDriftModal';
|
|
175
193
|
export type { OrientationDriftModalProps } from './camera/OrientationDriftModal';
|
|
176
194
|
|
|
195
|
+
// ── Panorama capture GUIDANCE (feature/pano-ux-guidance) ──────────────
|
|
196
|
+
// The first-time-user pan-capture guidance surfaces, wired into Layer-1
|
|
197
|
+
// <Camera> automatically (panMode / panGuidance / maxPanDurationMs /
|
|
198
|
+
// lateralBudgetCm / rectCrop / showPreview / guidanceCopy props). Exported for
|
|
199
|
+
// Layer-2 hosts composing their own capture UX on CameraView + the
|
|
200
|
+
// incremental engine.
|
|
201
|
+
//
|
|
202
|
+
// `PanMode` is the landscape-only-vs-both flag; `GuidanceCopy` +
|
|
203
|
+
// `DEFAULT_GUIDANCE_COPY` are the overridable copy surface.
|
|
204
|
+
export type { PanMode } from './camera/panModeGate';
|
|
205
|
+
export {
|
|
206
|
+
DEFAULT_GUIDANCE_COPY,
|
|
207
|
+
} from './camera/cameraGuidanceCopy';
|
|
208
|
+
export type { GuidanceCopy } from './camera/cameraGuidanceCopy';
|
|
209
|
+
// Shared motion hook — one gyro + one accelerometer subscription feeding
|
|
210
|
+
// the pan-speed bucket (item 4) and the lateral-drift latch (item 6).
|
|
211
|
+
export { usePanMotion } from './camera/usePanMotion';
|
|
212
|
+
export type {
|
|
213
|
+
UsePanMotionOptions,
|
|
214
|
+
UsePanMotionReturn,
|
|
215
|
+
PanSpeedBucket,
|
|
216
|
+
PanAxis,
|
|
217
|
+
} from './camera/usePanMotion';
|
|
218
|
+
// Presentational guidance surfaces (each renders null when not visible).
|
|
219
|
+
export { RotateToLandscapePrompt } from './camera/RotateToLandscapePrompt';
|
|
220
|
+
export type { RotateToLandscapePromptProps } from './camera/RotateToLandscapePrompt';
|
|
221
|
+
export { PanHowToOverlay } from './camera/PanHowToOverlay';
|
|
222
|
+
export type { PanHowToOverlayProps } from './camera/PanHowToOverlay';
|
|
223
|
+
export { CaptureCountdownOverlay } from './camera/CaptureCountdownOverlay';
|
|
224
|
+
export type { CaptureCountdownOverlayProps } from './camera/CaptureCountdownOverlay';
|
|
225
|
+
export { CaptureFrameCounterOverlay } from './camera/CaptureFrameCounterOverlay';
|
|
226
|
+
export type { CaptureFrameCounterOverlayProps } from './camera/CaptureFrameCounterOverlay';
|
|
227
|
+
export { LateralMotionModal } from './camera/LateralMotionModal';
|
|
228
|
+
export type { LateralMotionModalProps } from './camera/LateralMotionModal';
|
|
229
|
+
export { RectCropPreview } from './camera/RectCropPreview';
|
|
230
|
+
export type {
|
|
231
|
+
RectCropPreviewProps,
|
|
232
|
+
RectCropResult,
|
|
233
|
+
ImageRect,
|
|
234
|
+
} from './camera/RectCropPreview';
|
|
235
|
+
// Native perspective-rectify crop used by RectCropPreview's confirm
|
|
236
|
+
// path; hosts driving their own crop UI call it directly.
|
|
237
|
+
export { cropQuad } from './stitching/cropQuad';
|
|
238
|
+
export type { CropQuadOptions, CropQuadResult } from './stitching/cropQuad';
|
|
239
|
+
|
|
177
240
|
// ── Incremental stitching engine ──────────────────────────────────────
|
|
178
241
|
// JS bindings around the native `IncrementalStitcher` module. Use
|
|
179
242
|
// these when you need finer control than <Camera>'s built-in
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* computeInscribedRect — resolve the largest axis-aligned rectangle that
|
|
4
|
+
* fits entirely inside the non-black (coverage) region of a stitched
|
|
5
|
+
* panorama, via native `BatchStitcher.computeInscribedRect`.
|
|
6
|
+
*
|
|
7
|
+
* Used to SEED the post-capture crop editor (`RectCropPreview`): instead of
|
|
8
|
+
* a blind 8 %-inset rectangle, the editor opens on the max-inscribed rect —
|
|
9
|
+
* the tightest clean rectangle with no black corners — which the user then
|
|
10
|
+
* fine-tunes. Best-effort: callers fall back to the default inset seed if
|
|
11
|
+
* the native module is absent or the call rejects.
|
|
12
|
+
*
|
|
13
|
+
* Same native module + defensive-availability posture as
|
|
14
|
+
* `src/stitching/cropQuad.ts` and `src/quality/normaliseOrientation.ts`.
|
|
15
|
+
* The native side (Android `BatchStitcher.computeInscribedRect`, iOS
|
|
16
|
+
* `OpenCVStitcher.computeInscribedRect`) is the exact `maxInscribedRectFromMask`
|
|
17
|
+
* port used by the opt-in auto-crop, so the seed matches what that crop would
|
|
18
|
+
* pick — only here the user can then drag outward to keep more content.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { NativeModules } from 'react-native';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/** Resolved max-inscribed rectangle, in image-pixel coords. */
|
|
25
|
+
export interface InscribedRect {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
/** Intrinsic dimensions of the source image the rect was computed on. */
|
|
31
|
+
imageWidth: number;
|
|
32
|
+
imageHeight: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The shape of the native module method we call. */
|
|
36
|
+
interface InscribedRectNativeModule {
|
|
37
|
+
computeInscribedRect: (options: {
|
|
38
|
+
imagePath: string;
|
|
39
|
+
}) => Promise<InscribedRect>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the native `computeInscribedRect` function off
|
|
45
|
+
* `NativeModules.BatchStitcher`, or `null` when the module / method isn't
|
|
46
|
+
* registered (e.g. an older native build).
|
|
47
|
+
*/
|
|
48
|
+
function resolveComputeInscribedRect():
|
|
49
|
+
| InscribedRectNativeModule['computeInscribedRect']
|
|
50
|
+
| null {
|
|
51
|
+
const native: unknown =
|
|
52
|
+
(NativeModules as Record<string, unknown>)['BatchStitcher'];
|
|
53
|
+
if (
|
|
54
|
+
native
|
|
55
|
+
&& typeof native === 'object'
|
|
56
|
+
&& typeof (native as { computeInscribedRect?: unknown })
|
|
57
|
+
.computeInscribedRect === 'function'
|
|
58
|
+
) {
|
|
59
|
+
return (native as InscribedRectNativeModule).computeInscribedRect;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute the max-inscribed rectangle of `imagePath`'s coverage mask.
|
|
67
|
+
*
|
|
68
|
+
* @param imagePath file:// URI or bare path of the stitched image (the
|
|
69
|
+
* native side strips the scheme).
|
|
70
|
+
* @returns the inscribed rect, or `null` when the native module isn't
|
|
71
|
+
* registered (older native build) — callers then fall back to the default
|
|
72
|
+
* seed. REJECTS only if the native call itself errors (decode / read
|
|
73
|
+
* failure); callers should catch and treat the rejection as "no seed".
|
|
74
|
+
*/
|
|
75
|
+
export async function computeInscribedRect(
|
|
76
|
+
imagePath: string,
|
|
77
|
+
): Promise<InscribedRect | null> {
|
|
78
|
+
const fn = resolveComputeInscribedRect();
|
|
79
|
+
if (!fn) return null;
|
|
80
|
+
return fn({ imagePath });
|
|
81
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* cropQuad — item-7 perspective crop: rectify a user-dragged
|
|
4
|
+
* quadrilateral to an upright rectangle.
|
|
5
|
+
*
|
|
6
|
+
* The post-capture crop editor (`src/camera/RectCropPreview.tsx`) lets the
|
|
7
|
+
* user drag 4 independent corners over the stitched result. When that
|
|
8
|
+
* quad isn't ~axis-aligned, the host calls THIS wrapper instead of the
|
|
9
|
+
* cheap `cropToRect`: it hands the 4 IMAGE-PIXEL corners to the native
|
|
10
|
+
* `BatchStitcher.cropToQuad`, which runs
|
|
11
|
+
* `cv::getPerspectiveTransform` + `cv::warpPerspective` to produce an
|
|
12
|
+
* upright rectangle (averaged opposite-edge dimensions) and overwrites the
|
|
13
|
+
* file in place.
|
|
14
|
+
*
|
|
15
|
+
* This is the typed twin of the `cropToRect` call in
|
|
16
|
+
* `example/InscribedRectDebug.tsx` — same native module (`BatchStitcher`),
|
|
17
|
+
* same in-place overwrite + `{ width, height }` result contract, same
|
|
18
|
+
* platform-availability fallback posture as
|
|
19
|
+
* `src/quality/normaliseOrientation.ts`.
|
|
20
|
+
*
|
|
21
|
+
* Corner-order contract: `quadImagePoints` MUST be in canonical
|
|
22
|
+
* [TL, TR, BR, BL] (clockwise from top-left) order — exactly what
|
|
23
|
+
* `cropGeometry.ts:orderQuadCorners` produces and `RectCropResult.quad`
|
|
24
|
+
* carries. The native side rectifies into a rectangle whose corners map
|
|
25
|
+
* TL→(0,0), TR→(w,0), BR→(w,h), BL→(0,h); pass un-ordered points and the
|
|
26
|
+
* output is mirrored / rotated.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { NativeModules, Platform } from 'react-native';
|
|
30
|
+
|
|
31
|
+
import type { Point, Quad } from '../camera/cropGeometry';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/** Options for {@link cropQuad}. */
|
|
35
|
+
export interface CropQuadOptions {
|
|
36
|
+
/**
|
|
37
|
+
* JPEG quality for the re-encoded output, 1–100. Defaults to 90 (the
|
|
38
|
+
* native default, matching `cropToRect`).
|
|
39
|
+
*/
|
|
40
|
+
quality?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolved result of a successful {@link cropQuad}. */
|
|
44
|
+
export interface CropQuadResult {
|
|
45
|
+
/**
|
|
46
|
+
* The file the rectified image was written to. Equals the input
|
|
47
|
+
* `imagePath` (the native crop overwrites in place) — surfaced
|
|
48
|
+
* explicitly so callers don't have to assume the in-place contract.
|
|
49
|
+
*/
|
|
50
|
+
outputPath: string;
|
|
51
|
+
/** Width of the rectified rectangle, in pixels. */
|
|
52
|
+
width: number;
|
|
53
|
+
/** Height of the rectified rectangle, in pixels. */
|
|
54
|
+
height: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/** The shape of the native module method we call. */
|
|
59
|
+
interface CropQuadNativeModule {
|
|
60
|
+
cropToQuad: (options: {
|
|
61
|
+
imagePath: string;
|
|
62
|
+
quad: number[];
|
|
63
|
+
quality: number;
|
|
64
|
+
}) => Promise<{ width: number; height: number }>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the native `cropToQuad` function off `NativeModules.BatchStitcher`,
|
|
70
|
+
* or `null` when the module / method isn't registered (e.g. an older native
|
|
71
|
+
* build). Same defensive lookup as `normaliseOrientation`.
|
|
72
|
+
*/
|
|
73
|
+
function resolveCropToQuad(): CropQuadNativeModule['cropToQuad'] | null {
|
|
74
|
+
const native: unknown =
|
|
75
|
+
(NativeModules as Record<string, unknown>)['BatchStitcher'];
|
|
76
|
+
if (
|
|
77
|
+
native
|
|
78
|
+
&& typeof native === 'object'
|
|
79
|
+
&& typeof (native as { cropToQuad?: unknown }).cropToQuad === 'function'
|
|
80
|
+
) {
|
|
81
|
+
return (native as CropQuadNativeModule).cropToQuad;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Flatten the 4 ordered ([TL, TR, BR, BL]) image-pixel corners into the
|
|
89
|
+
* `[tlX, tlY, trX, trY, brX, brY, blX, blY]` array the native module
|
|
90
|
+
* expects. Exported for unit tests + reuse.
|
|
91
|
+
*/
|
|
92
|
+
export function flattenQuad(quad: Quad): number[] {
|
|
93
|
+
const out: number[] = [];
|
|
94
|
+
for (const p of quad as ReadonlyArray<Point>) {
|
|
95
|
+
out.push(p.x, p.y);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Perspective-rectify `quadImagePoints` out of `imagePath` into an upright
|
|
103
|
+
* rectangle, overwriting the file in place, and resolve the output path +
|
|
104
|
+
* rectified dimensions.
|
|
105
|
+
*
|
|
106
|
+
* @param imagePath file:// URI (or bare path) of the image to crop.
|
|
107
|
+
* @param quadImagePoints the 4 corners in IMAGE-PIXEL space, canonically
|
|
108
|
+
* ordered [TL, TR, BR, BL] (use
|
|
109
|
+
* `orderQuadCorners`). This is exactly
|
|
110
|
+
* `RectCropResult.quad`.
|
|
111
|
+
* @param outPath where to write the result. The native crop
|
|
112
|
+
* OVERWRITES IN PLACE, so this currently MUST equal
|
|
113
|
+
* `imagePath` (or be omitted — defaults to it).
|
|
114
|
+
* Passing a different path throws, surfacing the
|
|
115
|
+
* limitation rather than silently ignoring it; see
|
|
116
|
+
* the integrator note in the item-7 handoff.
|
|
117
|
+
* @param opts optional `{ quality }`.
|
|
118
|
+
*
|
|
119
|
+
* @throws if the native module isn't registered, if `outPath` differs from
|
|
120
|
+
* `imagePath`, or if the native crop rejects (degenerate quad,
|
|
121
|
+
* canvas guard, write failure).
|
|
122
|
+
*/
|
|
123
|
+
export async function cropQuad(
|
|
124
|
+
imagePath: string,
|
|
125
|
+
quadImagePoints: Quad,
|
|
126
|
+
outPath?: string,
|
|
127
|
+
opts?: CropQuadOptions,
|
|
128
|
+
): Promise<CropQuadResult> {
|
|
129
|
+
if (outPath !== undefined && outPath !== imagePath) {
|
|
130
|
+
// The native cropToQuad (like cropToRect) only overwrites in place.
|
|
131
|
+
// Fail loudly rather than silently writing to imagePath and returning
|
|
132
|
+
// a path the file isn't at.
|
|
133
|
+
throw new Error(
|
|
134
|
+
'[capture-sdk] cropQuad: native crop overwrites in place; '
|
|
135
|
+
+ 'outPath must equal imagePath (or be omitted).',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const fn = resolveCropToQuad();
|
|
140
|
+
if (!fn) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`[capture-sdk] cropQuad: native module BatchStitcher.cropToQuad not `
|
|
143
|
+
+ `available on ${Platform.OS}. Ensure the native module is registered.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const quality = clampQuality(opts?.quality);
|
|
148
|
+
const dims = await fn({
|
|
149
|
+
imagePath,
|
|
150
|
+
quad: flattenQuad(quadImagePoints),
|
|
151
|
+
quality,
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
outputPath: imagePath,
|
|
155
|
+
width: dims.width,
|
|
156
|
+
height: dims.height,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
/** Clamp the requested JPEG quality into [1, 100]; default 90. */
|
|
162
|
+
function clampQuality(quality?: number): number {
|
|
163
|
+
if (quality === undefined || Number.isNaN(quality)) return 90;
|
|
164
|
+
if (quality < 1) return 1;
|
|
165
|
+
if (quality > 100) return 100;
|
|
166
|
+
return Math.round(quality);
|
|
167
|
+
}
|
|
@@ -682,6 +682,33 @@ export interface IncrementalFinalizeResult {
|
|
|
682
682
|
* on the just-completed capture.
|
|
683
683
|
*/
|
|
684
684
|
stitchModeResolved?: 'panorama' | 'scans';
|
|
685
|
+
/**
|
|
686
|
+
* 2026-06-14 (DEV overlay) — a semicolon-separated `key=value` trace of the
|
|
687
|
+
* stitcher's RUNTIME choices for this output, e.g.
|
|
688
|
+
* `"pipe=manual;warp=spherical;route=batch;seam=graphcut;blend=multiband"`.
|
|
689
|
+
* pipe: `manual` (cv::detail) | `highlevel` (cv::Stitcher)
|
|
690
|
+
* warp: `plane` | `cylindrical` | `spherical`
|
|
691
|
+
* route: `batch` (warp-all + seam) | `stream` (low-memory per-frame)
|
|
692
|
+
* seam: `graphcut` | `none`
|
|
693
|
+
* blend: `multiband` | `feather`
|
|
694
|
+
* Intended for a __DEV__-only overlay so the operator can see HOW the
|
|
695
|
+
* panorama was built (which warper, whether the low-memory stream/feather
|
|
696
|
+
* fallback kicked in, etc.). iOS only for now; undefined elsewhere.
|
|
697
|
+
*/
|
|
698
|
+
debugSummary?: string;
|
|
699
|
+
/**
|
|
700
|
+
* 2026-06-15 (iOS) — the exact keyframe JPEG paths used for this stitch.
|
|
701
|
+
* Lets the host re-stitch the SAME frames on demand via `refinePanorama`
|
|
702
|
+
* (e.g. the high-level preview tab) without re-running the capture or
|
|
703
|
+
* enumerating the session directory. iOS only; undefined elsewhere.
|
|
704
|
+
*/
|
|
705
|
+
batchKeyframePaths?: string[];
|
|
706
|
+
/**
|
|
707
|
+
* 2026-06-15 (iOS) — the capture orientation this stitch baked into the
|
|
708
|
+
* output. An on-demand re-stitch (refinePanorama) MUST pass this back or the
|
|
709
|
+
* result comes out in the raw sensor landscape (sideways). iOS only.
|
|
710
|
+
*/
|
|
711
|
+
captureOrientation?: string;
|
|
685
712
|
}
|
|
686
713
|
|
|
687
714
|
|
|
@@ -750,6 +777,15 @@ export interface IncrementalRefineOptions {
|
|
|
750
777
|
stitchMode?: 'auto' | 'panorama' | 'scans';
|
|
751
778
|
/** JPEG quality 1..100, default 90. */
|
|
752
779
|
jpegQuality?: number;
|
|
780
|
+
/**
|
|
781
|
+
* 2026-06-15 (iOS) — which stitch pipeline to run. `true` = the manual
|
|
782
|
+
* `cv::detail` pipeline (the default batch-capture output); `false` = stock
|
|
783
|
+
* high-level `cv::Stitcher`. Default `false` on the refine path. This is
|
|
784
|
+
* how the on-demand "high-level" preview tab re-stitches the captured
|
|
785
|
+
* keyframes via cv::Stitcher without re-running the whole capture. iOS only
|
|
786
|
+
* (Android refine is always cv::Stitcher).
|
|
787
|
+
*/
|
|
788
|
+
useManualPipeline?: boolean;
|
|
753
789
|
}
|
|
754
790
|
|
|
755
791
|
|
|
@@ -771,6 +807,15 @@ export interface IncrementalRefineResult {
|
|
|
771
807
|
framesDropped: number;
|
|
772
808
|
/** The confidence threshold that succeeded. -1 when not applicable. */
|
|
773
809
|
finalConfidenceThresh: number;
|
|
810
|
+
/**
|
|
811
|
+
* 2026-06-15 (DEV overlay A/B-aware) — the stitcher's own semicolon-separated
|
|
812
|
+
* `key=value` runtime recipe for THIS refined output, e.g.
|
|
813
|
+
* `"pipe=highlevel;warp=spherical;route=batch;seam=graphcut;blend=multiband"`.
|
|
814
|
+
* Mirrors `IncrementalFinalizeResult.debugSummary`. Lets the on-demand
|
|
815
|
+
* high-level preview tab show its OWN recipe in the __DEV__ overlay pill
|
|
816
|
+
* instead of the manual primary's recipe. iOS only; undefined elsewhere.
|
|
817
|
+
*/
|
|
818
|
+
debugSummary?: string;
|
|
774
819
|
}
|
|
775
820
|
|
|
776
821
|
|
package/cpp/tests/CMakeLists.txt
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
#
|
|
3
|
-
# cpp/tests/CMakeLists.txt — v0.10.0 audit #9A
|
|
4
|
-
#
|
|
5
|
-
# Standalone Google Test runner for the shared C++ port under `cpp/`.
|
|
6
|
-
# Build + run via:
|
|
7
|
-
#
|
|
8
|
-
# cmake -S cpp/tests -B build/cpp-tests
|
|
9
|
-
# cmake --build build/cpp-tests
|
|
10
|
-
# (cd build/cpp-tests && ctest --output-on-failure)
|
|
11
|
-
#
|
|
12
|
-
# Or, from the repo root: `scripts/run-cpp-tests.sh`.
|
|
13
|
-
#
|
|
14
|
-
# What this DOES test:
|
|
15
|
-
# - Pure-C++ types in cpp/: `Pose`, `PlaneTransform`,
|
|
16
|
-
# `StitcherFrameData`, `PixelBufferReader` interface contract.
|
|
17
|
-
# - `timeBudgetCrossed` — the keyframe gate's OpenCV-free time-budget
|
|
18
|
-
# force-accept predicate (keyframe_timebudget_test.cpp).
|
|
19
|
-
# - `warpRoiExceedsGuard` — the OpenCV-free warp-canvas size guard that
|
|
20
|
-
# triggers the cylindrical-fallback pre-pass (warp_guard_test.cpp).
|
|
21
|
-
# (The StitcherWorkletRegistry test was removed when that source was
|
|
22
|
-
# archived in the Phase-3 cleanup — its CMake entries had gone stale
|
|
23
|
-
# and broke the whole suite's configure step.)
|
|
24
|
-
#
|
|
25
|
-
# What this DOES NOT test yet (deferred to v0.11.0+):
|
|
26
|
-
# - `KeyframeGate` (the full gate) — depends on OpenCV. Will need an
|
|
27
|
-
# OpenCV-aware CMake config (link the same opencv_world the prod
|
|
28
|
-
# build uses). NOTE: the OpenCV-free `timeBudgetCrossed` predicate
|
|
29
|
-
# (the time-budget force-accept boundary logic) IS covered, via
|
|
30
|
-
# keyframe_timebudget_test.cpp — it's an inline header-only function.
|
|
31
|
-
# - JSI host-object dispatch — needs a real Hermes runtime.
|
|
32
|
-
# - Anything in `stitcher.cpp` (uses OpenCV stitching pipeline).
|
|
33
|
-
|
|
34
|
-
cmake_minimum_required(VERSION 3.20)
|
|
35
|
-
project(stitcher_cpp_tests CXX)
|
|
36
|
-
|
|
37
|
-
set(CMAKE_CXX_STANDARD 17)
|
|
38
|
-
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
39
|
-
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
40
|
-
|
|
41
|
-
# Must precede `gtest_discover_tests` — without this the discovered
|
|
42
|
-
# cases aren't registered into a CTestTestfile.cmake at this directory
|
|
43
|
-
# level, and `ctest` reports "No tests were found".
|
|
44
|
-
enable_testing()
|
|
45
|
-
|
|
46
|
-
# Fetch GoogleTest pinned to v1.14.0. Pin matches what the AOSP /
|
|
47
|
-
# Android NDK test ecosystem uses today; bumps should be deliberate.
|
|
48
|
-
include(FetchContent)
|
|
49
|
-
FetchContent_Declare(
|
|
50
|
-
googletest
|
|
51
|
-
GIT_REPOSITORY https://github.com/google/googletest.git
|
|
52
|
-
GIT_TAG v1.14.0
|
|
53
|
-
)
|
|
54
|
-
# Prevent GoogleTest from overriding our compiler/linker options
|
|
55
|
-
# (Windows-only quirk; harmless on macOS/Linux).
|
|
56
|
-
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
|
57
|
-
FetchContent_MakeAvailable(googletest)
|
|
58
|
-
|
|
59
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
60
|
-
# Include paths
|
|
61
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
62
|
-
# Order matters: stubs/ comes FIRST so `#include <jsi/jsi.h>` and
|
|
63
|
-
# `#include <react-native-worklets-core/WKTJsiWorklet.h>` resolve to
|
|
64
|
-
# the test-only stubs before any system header. The production cpp/
|
|
65
|
-
# directory comes after so retailens:: headers (e.g. ar_frame_pose.h,
|
|
66
|
-
# stitcher_frame_data.hpp) are found as the production code expects.
|
|
67
|
-
include_directories(
|
|
68
|
-
${CMAKE_CURRENT_SOURCE_DIR}/stubs
|
|
69
|
-
${CMAKE_CURRENT_SOURCE_DIR}/..
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
73
|
-
# Test executable
|
|
74
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
75
|
-
add_executable(stitcher_cpp_tests
|
|
76
|
-
# Test sources. All subjects are header-only (Pose / PlaneTransform,
|
|
77
|
-
# StitcherFrameData, and the gate's inline `timeBudgetCrossed`
|
|
78
|
-
# predicate), so no production .cpp needs linking here.
|
|
79
|
-
${CMAKE_CURRENT_SOURCE_DIR}/pose_test.cpp
|
|
80
|
-
${CMAKE_CURRENT_SOURCE_DIR}/stitcher_frame_data_test.cpp
|
|
81
|
-
${CMAKE_CURRENT_SOURCE_DIR}/keyframe_timebudget_test.cpp
|
|
82
|
-
${CMAKE_CURRENT_SOURCE_DIR}/warp_guard_test.cpp
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
target_link_libraries(stitcher_cpp_tests
|
|
86
|
-
PRIVATE
|
|
87
|
-
GTest::gtest_main
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
# Treat warnings as errors in the test build to catch the kind of
|
|
91
|
-
# silent regressions (unused variables, signed/unsigned comparisons,
|
|
92
|
-
# narrowing conversions) that production cross-compilation flags would
|
|
93
|
-
# otherwise suppress.
|
|
94
|
-
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
|
95
|
-
target_compile_options(stitcher_cpp_tests PRIVATE
|
|
96
|
-
-Wall -Wextra -Wpedantic -Werror
|
|
97
|
-
)
|
|
98
|
-
endif()
|
|
99
|
-
|
|
100
|
-
# Register with CTest so `ctest --output-on-failure` picks up the
|
|
101
|
-
# individual TEST() cases (gtest_discover_tests scans the binary at
|
|
102
|
-
# build time, no manual ADD_TEST per case).
|
|
103
|
-
include(GoogleTest)
|
|
104
|
-
gtest_discover_tests(stitcher_cpp_tests)
|
package/cpp/tests/README.md
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# `cpp/tests/` — shared C++ unit test suite
|
|
2
|
-
|
|
3
|
-
v0.10.0 audit `#9A` introduced this directory to give the cross-platform
|
|
4
|
-
shared C++ code under `cpp/` a Google Test harness that runs on the
|
|
5
|
-
developer's host machine (not on a device or emulator). Pairs with the
|
|
6
|
-
Android-side JUnit suite added in `#11A` (see
|
|
7
|
-
`android/src/test/java/io/imagestitcher/rn/`).
|
|
8
|
-
|
|
9
|
-
## Run
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
scripts/run-cpp-tests.sh # configure + build + ctest
|
|
13
|
-
scripts/run-cpp-tests.sh --clean # nuke build/cpp-tests/ first
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Requires `cmake ≥ 3.20` and a C++17 toolchain (the macOS AppleClang
|
|
17
|
-
shipped with Xcode 14+ is fine; Linux GCC 9+ / Clang 10+ also work).
|
|
18
|
-
|
|
19
|
-
Build artefacts land under `build/cpp-tests/` (gitignored). Google
|
|
20
|
-
Test is fetched at configure time via CMake `FetchContent` pinned to
|
|
21
|
-
`v1.14.0`; no system-wide install required.
|
|
22
|
-
|
|
23
|
-
## Scope (v0.10.0)
|
|
24
|
-
|
|
25
|
-
Covered:
|
|
26
|
-
|
|
27
|
-
- `Pose`, `PlaneTransform` (POD layout / size / field offsets — pinned
|
|
28
|
-
to the cross-platform marshalling contract documented in
|
|
29
|
-
`cpp/ar_frame_pose.h`).
|
|
30
|
-
- `StitcherFrameData` (default-construction invariants the JSI host
|
|
31
|
-
object's `get()` dispatch relies on).
|
|
32
|
-
- `PixelBufferReader` interface contract (clipping behaviour of
|
|
33
|
-
`copyTo` — validated via the `FakePixelBufferReader` test helper).
|
|
34
|
-
- `StitcherWorkletRegistry` storage lifecycle: shared-instance,
|
|
35
|
-
install/uninstall/count/snapshot, snapshot independence, concurrent
|
|
36
|
-
installs yield unique IDs (16 threads × 32 installs).
|
|
37
|
-
|
|
38
|
-
Not yet covered (intentional deferrals):
|
|
39
|
-
|
|
40
|
-
- `KeyframeGate` (`cpp/keyframe_gate.cpp`) — depends on OpenCV
|
|
41
|
-
(`opencv2/imgproc.hpp`, `opencv2/video.hpp` for `calcOpticalFlowPyrLK`).
|
|
42
|
-
Linking the production OpenCV xcframework / Android SDK into the
|
|
43
|
-
host-side test target would balloon CI time and disk usage; the
|
|
44
|
-
alternative is to land a stripped-down `libopencv-core` host build
|
|
45
|
-
just for tests. Deferred — comes with the v0.11.0 cross-platform
|
|
46
|
-
parity suite (`#2C`).
|
|
47
|
-
- `stitcher.cpp` — uses the full OpenCV stitching pipeline; same
|
|
48
|
-
reason as above.
|
|
49
|
-
- JSI host-object dispatch (`stitcher_frame_jsi.cpp`,
|
|
50
|
-
`stitcher_proxy_jsi.cpp`, `stitcher_worklet_dispatch.cpp`) — needs
|
|
51
|
-
a real Hermes runtime. The `StitcherWorkletRegistry` tests sidestep
|
|
52
|
-
this via the `_installEntryForTests` seam + JSI stubs under
|
|
53
|
-
`stubs/`; the JSI dispatch paths can't be similarly stubbed because
|
|
54
|
-
they actively call into the runtime.
|
|
55
|
-
|
|
56
|
-
## How the JSI-dependent registry tests work without a real JSI
|
|
57
|
-
|
|
58
|
-
`stitcher_worklet_registry.cpp` `#include`s
|
|
59
|
-
`<jsi/jsi.h>` and `<react-native-worklets-core/WKTJsiWorklet.h>` to
|
|
60
|
-
construct `WorkletInvoker` instances from a real JS runtime. The test
|
|
61
|
-
target sidesteps both by:
|
|
62
|
-
|
|
63
|
-
1. Putting `cpp/tests/stubs/` first on the compiler's include path so
|
|
64
|
-
`#include <jsi/jsi.h>` resolves to `stubs/jsi/jsi.h` (which declares
|
|
65
|
-
`facebook::jsi::Runtime` / `Value` as empty classes — enough for
|
|
66
|
-
the registry's reference-only usage), and
|
|
67
|
-
`#include <react-native-worklets-core/WKTJsiWorklet.h>` resolves to
|
|
68
|
-
`stubs/react-native-worklets-core/WKTJsiWorklet.h` (which declares
|
|
69
|
-
`RNWorklet::WorkletInvoker` with a no-op constructor).
|
|
70
|
-
2. Calling `_installEntryForTests(nullptr)` instead of the production
|
|
71
|
-
`install(runtime, value)` path. The registry stores the
|
|
72
|
-
`shared_ptr<WorkletInvoker>` but never dereferences it (it only
|
|
73
|
-
hands it back via `snapshot`), so `nullptr` is safe.
|
|
74
|
-
|
|
75
|
-
The stubs live exclusively under `cpp/tests/stubs/`; production
|
|
76
|
-
builds never see them. See `stubs/jsi/jsi.h`'s docstring for the
|
|
77
|
-
guard-rails.
|
|
78
|
-
|
|
79
|
-
## When NOT to add a test here
|
|
80
|
-
|
|
81
|
-
- If the test needs a real JSI runtime, real OpenCV operations, or
|
|
82
|
-
real-device sensor data, it belongs in `android/src/androidTest/`
|
|
83
|
-
(instrumented), the iOS Swift test target, or the v0.11.0 parity
|
|
84
|
-
harness — NOT here.
|
|
85
|
-
- If the test verifies TypeScript/JS-side behaviour, it belongs under
|
|
86
|
-
`src/**/__tests__/` (Jest).
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
//
|
|
3
|
-
// keyframe_timebudget_test.cpp — host unit tests for the pure
|
|
4
|
-
// `retailens::timeBudgetCrossed` predicate (the keyframe gate's
|
|
5
|
-
// time-budget force-accept decision).
|
|
6
|
-
//
|
|
7
|
-
// The full KeyframeGate depends on OpenCV and cannot run in this
|
|
8
|
-
// harness (see CMakeLists.txt). The time-budget boundary logic was
|
|
9
|
-
// therefore deliberately extracted into an inline, OpenCV-free
|
|
10
|
-
// predicate in keyframe_gate.hpp precisely so it CAN be unit-tested
|
|
11
|
-
// here without linking the gate's OpenCV-dependent .cpp.
|
|
12
|
-
|
|
13
|
-
#include <gtest/gtest.h>
|
|
14
|
-
|
|
15
|
-
#include "keyframe_gate.hpp"
|
|
16
|
-
|
|
17
|
-
using retailens::timeBudgetCrossed;
|
|
18
|
-
|
|
19
|
-
// intervalMs <= 0 disables the budget entirely (opt-out path).
|
|
20
|
-
TEST(TimeBudgetCrossed, DisabledWhenIntervalNonPositive) {
|
|
21
|
-
EXPECT_FALSE(timeBudgetCrossed(0.0, 1000, 999999));
|
|
22
|
-
EXPECT_FALSE(timeBudgetCrossed(-5.0, 1000, 999999));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Never fires before the first accept (lastAcceptMs sentinel -1),
|
|
26
|
-
// regardless of how large `nowMs` is.
|
|
27
|
-
TEST(TimeBudgetCrossed, NeverFiresBeforeFirstAccept) {
|
|
28
|
-
EXPECT_FALSE(timeBudgetCrossed(2000.0, -1, 1000000));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Fires exactly at the boundary (elapsed == interval) and beyond.
|
|
32
|
-
TEST(TimeBudgetCrossed, FiresAtAndAfterBoundary) {
|
|
33
|
-
EXPECT_TRUE(timeBudgetCrossed(2000.0, 1000, 3000)); // elapsed == 2000
|
|
34
|
-
EXPECT_TRUE(timeBudgetCrossed(2000.0, 1000, 5000)); // elapsed > 2000
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Does NOT fire just under the boundary.
|
|
38
|
-
TEST(TimeBudgetCrossed, DoesNotFireJustUnderBoundary) {
|
|
39
|
-
EXPECT_FALSE(timeBudgetCrossed(2000.0, 1000, 2999)); // elapsed == 1999
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// A backwards or equal clock must never fire. A monotonic source
|
|
43
|
-
// should prevent now < lastAccept, but the predicate must be robust.
|
|
44
|
-
TEST(TimeBudgetCrossed, BackwardsOrEqualClockDoesNotFire) {
|
|
45
|
-
EXPECT_FALSE(timeBudgetCrossed(2000.0, 5000, 4000)); // now < lastAccept
|
|
46
|
-
EXPECT_FALSE(timeBudgetCrossed(2000.0, 5000, 5000)); // elapsed 0 < 2000
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Sub-millisecond budget must NOT collapse to "accept every frame":
|
|
50
|
-
// the predicate compares elapsed in double, so a 0.5 ms budget needs
|
|
51
|
-
// ~1 ms elapsed (not 0). Guards the truncation regression.
|
|
52
|
-
TEST(TimeBudgetCrossed, SubMillisecondBudgetDoesNotAcceptEveryFrame) {
|
|
53
|
-
EXPECT_FALSE(timeBudgetCrossed(0.5, 1000, 1000)); // elapsed 0.0 < 0.5
|
|
54
|
-
EXPECT_TRUE(timeBudgetCrossed(0.5, 1000, 1001)); // elapsed 1.0 >= 0.5
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Realistic 2 s budget across a slow pan: a keyframe accepted at t,
|
|
58
|
-
// the next force-accept lands at t + 2000 ms, not before.
|
|
59
|
-
TEST(TimeBudgetCrossed, TwoSecondBudgetTypicalUse) {
|
|
60
|
-
const int64_t lastAccept = 10000;
|
|
61
|
-
EXPECT_FALSE(timeBudgetCrossed(2000.0, lastAccept, lastAccept + 1500));
|
|
62
|
-
EXPECT_FALSE(timeBudgetCrossed(2000.0, lastAccept, lastAccept + 1999));
|
|
63
|
-
EXPECT_TRUE(timeBudgetCrossed(2000.0, lastAccept, lastAccept + 2000));
|
|
64
|
-
EXPECT_TRUE(timeBudgetCrossed(2000.0, lastAccept, lastAccept + 2001));
|
|
65
|
-
}
|