react-native-image-stitcher 0.15.2 → 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.
Files changed (133) hide show
  1. package/CHANGELOG.md +124 -1
  2. package/README.md +116 -5
  3. package/android/src/main/cpp/image_stitcher_jni.cpp +107 -11
  4. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
  5. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +87 -30
  6. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  7. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
  8. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
  9. package/cpp/crop_quad.cpp +162 -0
  10. package/cpp/crop_quad.hpp +163 -0
  11. package/cpp/stitcher.cpp +651 -55
  12. package/cpp/stitcher.hpp +10 -0
  13. package/cpp/warp_guard.hpp +212 -0
  14. package/dist/camera/Camera.d.ts +196 -12
  15. package/dist/camera/Camera.js +629 -35
  16. package/dist/camera/CameraView.js +35 -16
  17. package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
  18. package/dist/camera/CaptureCountdownOverlay.js +239 -0
  19. package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
  20. package/dist/camera/CaptureFrameCounterOverlay.js +142 -0
  21. package/dist/camera/CaptureMemoryPill.d.ts +9 -1
  22. package/dist/camera/CaptureMemoryPill.js +3 -3
  23. package/dist/camera/CapturePreview.js +2 -1
  24. package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
  25. package/dist/camera/CaptureStatusOverlay.js +22 -5
  26. package/dist/camera/CaptureThumbnailStrip.js +2 -1
  27. package/dist/camera/LateralMotionModal.d.ts +85 -0
  28. package/dist/camera/LateralMotionModal.js +134 -0
  29. package/dist/camera/PanHowToOverlay.d.ts +76 -0
  30. package/dist/camera/PanHowToOverlay.js +222 -0
  31. package/dist/camera/PanoramaSettings.d.ts +8 -6
  32. package/dist/camera/PanoramaSettings.js +26 -5
  33. package/dist/camera/PanoramaSettingsModal.js +4 -4
  34. package/dist/camera/RectCropPreview.d.ts +161 -0
  35. package/dist/camera/RectCropPreview.js +480 -0
  36. package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
  37. package/dist/camera/RotateToLandscapePrompt.js +138 -0
  38. package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
  39. package/dist/camera/buildPanoramaInitialSettings.js +9 -0
  40. package/dist/camera/cameraErrorMessages.d.ts +30 -1
  41. package/dist/camera/cameraErrorMessages.js +26 -10
  42. package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
  43. package/dist/camera/cameraGuidanceCopy.js +80 -0
  44. package/dist/camera/captureCountdown.d.ts +52 -0
  45. package/dist/camera/captureCountdown.js +76 -0
  46. package/dist/camera/captureWarnings.d.ts +90 -0
  47. package/dist/camera/captureWarnings.js +108 -0
  48. package/dist/camera/classifyStitchError.d.ts +30 -0
  49. package/dist/camera/classifyStitchError.js +42 -0
  50. package/dist/camera/cropGeometry.d.ts +136 -0
  51. package/dist/camera/cropGeometry.js +223 -0
  52. package/dist/camera/displayDecodeImageProps.d.ts +25 -0
  53. package/dist/camera/displayDecodeImageProps.js +29 -0
  54. package/dist/camera/guidanceGraphics.d.ts +58 -0
  55. package/dist/camera/guidanceGraphics.js +280 -0
  56. package/dist/camera/guidanceTokens.d.ts +54 -0
  57. package/dist/camera/guidanceTokens.js +58 -0
  58. package/dist/camera/panModeGate.d.ts +54 -0
  59. package/dist/camera/panModeGate.js +62 -0
  60. package/dist/camera/pickCaptureFormat.d.ts +71 -0
  61. package/dist/camera/pickCaptureFormat.js +85 -0
  62. package/dist/camera/stitchDebugInfo.d.ts +27 -0
  63. package/dist/camera/stitchDebugInfo.js +55 -0
  64. package/dist/camera/usePanMotion.d.ts +250 -0
  65. package/dist/camera/usePanMotion.js +451 -0
  66. package/dist/index.d.ts +24 -3
  67. package/dist/index.js +33 -2
  68. package/dist/stitching/computeInscribedRect.d.ts +40 -0
  69. package/dist/stitching/computeInscribedRect.js +55 -0
  70. package/dist/stitching/cropQuad.d.ts +78 -0
  71. package/dist/stitching/cropQuad.js +116 -0
  72. package/dist/stitching/incremental.d.ts +45 -0
  73. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +56 -8
  74. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
  75. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
  76. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
  77. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +191 -7
  78. package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
  79. package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
  80. package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
  81. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
  82. package/package.json +5 -1
  83. package/src/camera/Camera.tsx +994 -47
  84. package/src/camera/CameraView.tsx +48 -16
  85. package/src/camera/CaptureCountdownOverlay.tsx +272 -0
  86. package/src/camera/CaptureFrameCounterOverlay.tsx +183 -0
  87. package/src/camera/CaptureMemoryPill.tsx +17 -3
  88. package/src/camera/CapturePreview.tsx +5 -0
  89. package/src/camera/CaptureStatusOverlay.tsx +35 -7
  90. package/src/camera/CaptureThumbnailStrip.tsx +4 -0
  91. package/src/camera/LateralMotionModal.tsx +199 -0
  92. package/src/camera/PanHowToOverlay.tsx +246 -0
  93. package/src/camera/PanoramaSettings.ts +34 -11
  94. package/src/camera/PanoramaSettingsModal.tsx +4 -4
  95. package/src/camera/RectCropPreview.tsx +820 -0
  96. package/src/camera/RotateToLandscapePrompt.tsx +188 -0
  97. package/src/camera/buildPanoramaInitialSettings.ts +30 -1
  98. package/src/camera/cameraErrorMessages.ts +39 -2
  99. package/src/camera/cameraGuidanceCopy.ts +145 -0
  100. package/src/camera/captureCountdown.ts +83 -0
  101. package/src/camera/captureWarnings.ts +190 -0
  102. package/src/camera/classifyStitchError.ts +68 -0
  103. package/src/camera/cropGeometry.ts +268 -0
  104. package/src/camera/displayDecodeImageProps.ts +25 -0
  105. package/src/camera/guidanceGraphics.tsx +347 -0
  106. package/src/camera/guidanceTokens.ts +57 -0
  107. package/src/camera/panModeGate.ts +81 -0
  108. package/src/camera/pickCaptureFormat.ts +130 -0
  109. package/src/camera/stitchDebugInfo.ts +71 -0
  110. package/src/camera/usePanMotion.ts +667 -0
  111. package/src/index.ts +66 -3
  112. package/src/stitching/computeInscribedRect.ts +81 -0
  113. package/src/stitching/cropQuad.ts +167 -0
  114. package/src/stitching/incremental.ts +45 -0
  115. package/cpp/tests/CMakeLists.txt +0 -104
  116. package/cpp/tests/README.md +0 -86
  117. package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
  118. package/cpp/tests/pose_test.cpp +0 -74
  119. package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
  120. package/cpp/tests/stubs/jsi/jsi.h +0 -33
  121. package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
  122. package/cpp/tests/warp_guard_test.cpp +0 -48
  123. package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
  124. package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
  125. package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
  126. package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
  127. package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
  128. package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
  129. package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
  130. package/src/camera/__tests__/useContentRotation.test.ts +0 -89
  131. package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
  132. package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
  133. package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
@@ -0,0 +1,90 @@
1
+ /**
2
+ * captureWarnings — non-fatal quality / behaviour signals attached to a
3
+ * SUCCESSFUL capture result. A stitch that *failed* surfaces a
4
+ * `CameraError` (via the `ok:false` result + `onError`); these warnings
5
+ * cover the "it succeeded but the host/user should know something" cases:
6
+ *
7
+ * • LOW_FRAME_UTILIZATION — fewer than `threshold` (default 70 %) of the
8
+ * captured frames survived the confidence filter, so the panorama may
9
+ * be patchy / shorter than intended.
10
+ * • LATERAL_DRIFT_FINALIZE — the capture was auto-finalized early because
11
+ * the phone drifted sideways (item 6); only the pre-drift portion was
12
+ * stitched.
13
+ * • HIGH_PAN_SPEED — the pan exceeded the recommended pace at some point
14
+ * during the capture (the live "too fast" cue fired), so motion blur /
15
+ * thin overlap may have hurt the result.
16
+ *
17
+ * `<Camera>` builds these at finalize and threads them into BOTH the
18
+ * `onCapture` result payload (so any host — not just the example app —
19
+ * learns of degraded output programmatically) AND the crop editor's banner
20
+ * (so the user sees it before accepting the crop).
21
+ *
22
+ * Pure + dependency-free so it's unit-testable in isolation (the lib's jest
23
+ * config is pure-TS and can't mount `<Camera>`), mirroring
24
+ * `classifyStitchError` / `buildPanoramaInitialSettings`.
25
+ */
26
+ /** Stable codes a host can branch on (in addition to the message). */
27
+ export type CaptureWarningCode = 'LOW_FRAME_UTILIZATION' | 'LATERAL_DRIFT_FINALIZE' | 'HIGH_PAN_SPEED';
28
+ export interface CaptureWarning {
29
+ /** Stable, host-switchable code. */
30
+ code: CaptureWarningCode;
31
+ /** Plain-language default message (shown in the crop banner). */
32
+ message: string;
33
+ /** Frames the engine tried to use (LOW_FRAME_UTILIZATION only). */
34
+ framesRequested?: number;
35
+ /** Frames that survived the confidence filter (LOW_FRAME_UTILIZATION). */
36
+ framesIncluded?: number;
37
+ /** included / requested in [0, 1] (LOW_FRAME_UTILIZATION only). */
38
+ utilization?: number;
39
+ }
40
+ /**
41
+ * Default trip point for LOW_FRAME_UTILIZATION: warn when fewer than 70 %
42
+ * of captured frames survived. Matches the threshold the user specified.
43
+ */
44
+ export declare const LOW_FRAME_UTILIZATION_THRESHOLD = 0.7;
45
+ /**
46
+ * The overridable message strings for the three capture warnings. This is
47
+ * the SINGLE SOURCE OF TRUTH for the default English warning copy — the
48
+ * `GuidanceCopy` surface re-uses these defaults (see `cameraGuidanceCopy`),
49
+ * so a host that localises via the `guidanceCopy` `<Camera>` prop re-words
50
+ * these too.
51
+ *
52
+ * `lowFrameUtilization` is a TEMPLATE: the placeholders `{included}`,
53
+ * `{requested}` and `{percent}` are substituted at build time with the
54
+ * actual frame counts. A translation must keep the placeholders (any it
55
+ * omits is simply not interpolated; an unknown placeholder is left as-is).
56
+ */
57
+ export interface CaptureWarningCopy {
58
+ /** LOW_FRAME_UTILIZATION — template; `{included}`/`{requested}`/`{percent}`. */
59
+ lowFrameUtilization: string;
60
+ /** LATERAL_DRIFT_FINALIZE. */
61
+ lateralDriftFinalize: string;
62
+ /** HIGH_PAN_SPEED. */
63
+ highPanSpeed: string;
64
+ }
65
+ export declare const DEFAULT_CAPTURE_WARNING_COPY: CaptureWarningCopy;
66
+ export interface BuildCaptureWarningsInput {
67
+ /** `framesRequested` from the native finalize result. */
68
+ framesRequested?: number;
69
+ /** `framesIncluded` from the native finalize result. */
70
+ framesIncluded?: number;
71
+ /** True when this finalize was triggered by lateral-drift auto-stop. */
72
+ lateralFinalize?: boolean;
73
+ /** True when the pan exceeded the recommended pace during the capture. */
74
+ highPanSpeed?: boolean;
75
+ /** Override the LOW_FRAME_UTILIZATION trip point (fraction in (0, 1]). */
76
+ lowFrameUtilizationThreshold?: number;
77
+ /**
78
+ * Localised / re-worded warning messages. Missing keys fall back to
79
+ * {@link DEFAULT_CAPTURE_WARNING_COPY}. `<Camera>` threads the resolved
80
+ * `guidanceCopy` here so the crop-banner warnings honour the host's i18n.
81
+ */
82
+ copy?: Partial<CaptureWarningCopy>;
83
+ }
84
+ /**
85
+ * Build the warning list for a successful capture. Order is by cause →
86
+ * symptom: a lateral-drift stop (the reason a capture is short) is listed
87
+ * before the low-utilization symptom it usually produces.
88
+ */
89
+ export declare function buildCaptureWarnings(input: BuildCaptureWarningsInput): CaptureWarning[];
90
+ //# sourceMappingURL=captureWarnings.d.ts.map
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * captureWarnings — non-fatal quality / behaviour signals attached to a
5
+ * SUCCESSFUL capture result. A stitch that *failed* surfaces a
6
+ * `CameraError` (via the `ok:false` result + `onError`); these warnings
7
+ * cover the "it succeeded but the host/user should know something" cases:
8
+ *
9
+ * • LOW_FRAME_UTILIZATION — fewer than `threshold` (default 70 %) of the
10
+ * captured frames survived the confidence filter, so the panorama may
11
+ * be patchy / shorter than intended.
12
+ * • LATERAL_DRIFT_FINALIZE — the capture was auto-finalized early because
13
+ * the phone drifted sideways (item 6); only the pre-drift portion was
14
+ * stitched.
15
+ * • HIGH_PAN_SPEED — the pan exceeded the recommended pace at some point
16
+ * during the capture (the live "too fast" cue fired), so motion blur /
17
+ * thin overlap may have hurt the result.
18
+ *
19
+ * `<Camera>` builds these at finalize and threads them into BOTH the
20
+ * `onCapture` result payload (so any host — not just the example app —
21
+ * learns of degraded output programmatically) AND the crop editor's banner
22
+ * (so the user sees it before accepting the crop).
23
+ *
24
+ * Pure + dependency-free so it's unit-testable in isolation (the lib's jest
25
+ * config is pure-TS and can't mount `<Camera>`), mirroring
26
+ * `classifyStitchError` / `buildPanoramaInitialSettings`.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.DEFAULT_CAPTURE_WARNING_COPY = exports.LOW_FRAME_UTILIZATION_THRESHOLD = void 0;
30
+ exports.buildCaptureWarnings = buildCaptureWarnings;
31
+ /**
32
+ * Default trip point for LOW_FRAME_UTILIZATION: warn when fewer than 70 %
33
+ * of captured frames survived. Matches the threshold the user specified.
34
+ */
35
+ exports.LOW_FRAME_UTILIZATION_THRESHOLD = 0.7;
36
+ exports.DEFAULT_CAPTURE_WARNING_COPY = {
37
+ lowFrameUtilization: 'Only {included} of {requested} captured frames ({percent}%) could be '
38
+ + 'used — the panorama may be incomplete. Pan more slowly and steadily '
39
+ + 'next time.',
40
+ lateralDriftFinalize: 'Capture stopped early because the phone drifted sideways — only the '
41
+ + 'part captured before the drift was stitched.',
42
+ highPanSpeed: 'The capture was taken faster than the recommended pace — the result '
43
+ + 'may not be the best. Pan more slowly next time.',
44
+ };
45
+ /**
46
+ * Substitute `{name}` placeholders in a template with `vars[name]`. An
47
+ * unknown placeholder is left verbatim (so a malformed translation degrades
48
+ * to showing `{percent}` rather than throwing).
49
+ */
50
+ function fillTemplate(tpl, vars) {
51
+ return tpl.replace(/\{(\w+)\}/g, (m, k) => k in vars ? String(vars[k]) : m);
52
+ }
53
+ /**
54
+ * Build the warning list for a successful capture. Order is by cause →
55
+ * symptom: a lateral-drift stop (the reason a capture is short) is listed
56
+ * before the low-utilization symptom it usually produces.
57
+ */
58
+ function buildCaptureWarnings(input) {
59
+ const { framesRequested, framesIncluded, lateralFinalize = false, highPanSpeed = false, lowFrameUtilizationThreshold = exports.LOW_FRAME_UTILIZATION_THRESHOLD, } = input;
60
+ const copy = {
61
+ ...exports.DEFAULT_CAPTURE_WARNING_COPY,
62
+ ...stripUndefinedCopy(input.copy),
63
+ };
64
+ const warnings = [];
65
+ if (lateralFinalize) {
66
+ warnings.push({
67
+ code: 'LATERAL_DRIFT_FINALIZE',
68
+ message: copy.lateralDriftFinalize,
69
+ });
70
+ }
71
+ if (highPanSpeed) {
72
+ warnings.push({
73
+ code: 'HIGH_PAN_SPEED',
74
+ message: copy.highPanSpeed,
75
+ });
76
+ }
77
+ if (typeof framesRequested === 'number'
78
+ && typeof framesIncluded === 'number'
79
+ && framesRequested > 0
80
+ && framesIncluded >= 0
81
+ && framesIncluded < framesRequested * lowFrameUtilizationThreshold) {
82
+ const utilization = framesIncluded / framesRequested;
83
+ warnings.push({
84
+ code: 'LOW_FRAME_UTILIZATION',
85
+ message: fillTemplate(copy.lowFrameUtilization, {
86
+ included: framesIncluded,
87
+ requested: framesRequested,
88
+ percent: Math.round(utilization * 100),
89
+ }),
90
+ framesRequested,
91
+ framesIncluded,
92
+ utilization,
93
+ });
94
+ }
95
+ return warnings;
96
+ }
97
+ /** Drop `undefined` values so a partial override never clobbers a default. */
98
+ function stripUndefinedCopy(o) {
99
+ if (!o)
100
+ return {};
101
+ const out = {};
102
+ Object.keys(o).forEach((k) => {
103
+ if (o[k] !== undefined)
104
+ out[k] = o[k];
105
+ });
106
+ return out;
107
+ }
108
+ //# sourceMappingURL=captureWarnings.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * classifyStitchError — map a raw native stitch-failure message to a
3
+ * `CameraErrorCode`.
4
+ *
5
+ * This is the load-bearing C++↔JS contract: the native pipeline reports
6
+ * failures only as exception strings (see cpp/stitcher.cpp — the warp /
7
+ * cumulative-canvas guards throw "warpRoi too large … degenerate camera
8
+ * params", cv::Stitcher reports "need more images", etc.), and this
9
+ * function is the single place that turns those strings into the typed
10
+ * codes `<Camera onError>` surfaces. The code then drives the friendly
11
+ * copy in {@link userFacingStitchError} (cameraErrorMessages.ts) — e.g.
12
+ * STITCH_CAMERA_PARAMS_FAIL → "Please pan more slowly".
13
+ *
14
+ * Extracted from the inline chain in Camera.tsx so the contract is
15
+ * unit-testable against the actual native strings (the lib's jest config
16
+ * is pure-TS and can't mount <Camera>).
17
+ *
18
+ * Ordering matters — the branches are checked top-to-bottom and the first
19
+ * match wins:
20
+ * 1. need-more-images (insufficient overlap) — most specific.
21
+ * 2. homography estimation.
22
+ * 3. degenerate camera params / warp-canvas guard (the divergent-warp
23
+ * OOM path, now converted to a clean throw by the canvas guard).
24
+ * 4. low-quality / disjoint output (the post-stitch validator, v0.16).
25
+ * 5. out-of-memory (incl. the pre-stitch memory abort).
26
+ * 6. fallback — an unclassified finalize failure.
27
+ */
28
+ import type { CameraErrorCode } from './Camera';
29
+ export declare function classifyStitchError(message: string): CameraErrorCode;
30
+ //# sourceMappingURL=classifyStitchError.d.ts.map
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyStitchError = classifyStitchError;
4
+ function classifyStitchError(message) {
5
+ // Insufficient overlap surfaces two ways: cv::Stitcher's
6
+ // ERR_NEED_MORE_IMGS ("need more images") and the manual pipeline's
7
+ // "0 valid pairwise matches / frames may not overlap enough" — both are
8
+ // the same recoverable "pan more slowly" case.
9
+ if (/need more images|pairwise match|overlap enough/i.test(message)) {
10
+ return 'STITCH_NEED_MORE_IMGS';
11
+ }
12
+ if (/homography/i.test(message)) {
13
+ return 'STITCH_HOMOGRAPHY_FAIL';
14
+ }
15
+ // Degenerate camera params — the rapid/wide-pan divergent-warp path.
16
+ // Broadened beyond the original "camera params" so a future reword of
17
+ // the native throw can't silently drop the "pan more slowly" copy: the
18
+ // per-frame warp guard and the cumulative-canvas guard carry "warpRoi"
19
+ // / "canvas too large" / "degenerate" respectively (see cpp/stitcher.cpp
20
+ // degenerateFrameException / degenerateCanvasException). Kept AFTER the
21
+ // homography branch and BEFORE the OOM branch so a true OOM string still
22
+ // routes to STITCH_OOM.
23
+ if (/camera params|warpRoi|degenerate|canvas too large/i.test(message)) {
24
+ return 'STITCH_CAMERA_PARAMS_FAIL';
25
+ }
26
+ // v0.16 — the native post-stitch validator rejected the output as
27
+ // disjoint / fragmented / wildly mis-proportioned (frames didn't connect
28
+ // into one coherent panorama). The cpp throw carries "stitch validation"
29
+ // / "disjoint" / "fragmented" (see cpp/stitcher.cpp validateStitchOutput).
30
+ // Kept BEFORE the OOM branch so its distinct message isn't swallowed.
31
+ if (/stitch validation|disjoint|fragmented|low-quality stitch/i.test(message)) {
32
+ return 'STITCH_LOW_QUALITY';
33
+ }
34
+ // OOM, including the pre-stitch headroom abort ("pre-stitch memory abort"
35
+ // / "memory abort") that fires when even the minimal streaming config
36
+ // won't fit — same user remedy (shorter sweep), so same code.
37
+ if (/out of memory|oom|memory abort/i.test(message)) {
38
+ return 'STITCH_OOM';
39
+ }
40
+ return 'PANORAMA_FINALIZE_FAILED';
41
+ }
42
+ //# sourceMappingURL=classifyStitchError.js.map
@@ -0,0 +1,136 @@
1
+ /**
2
+ * cropGeometry — pure coordinate + quad helpers behind the item-7
3
+ * draggable-corner crop editor (`RectCropPreview`).
4
+ *
5
+ * The editor shows the full result image with a `resizeMode="contain"`
6
+ * letterbox: the image is centred and uniformly scaled to fit the layout
7
+ * box, leaving symmetric bars on one axis. The 4 draggable corners live in
8
+ * ON-SCREEN coordinates (the touch space PanResponder reports), but the
9
+ * native crop needs IMAGE-PIXEL coordinates. These helpers are the
10
+ * letterbox transform and its inverse — extracted verbatim from
11
+ * `example/InscribedRectDebug.tsx` (~lines 178-204), which mapped an
12
+ * inscribed-rect from image px → screen the same way.
13
+ *
14
+ * Everything here is pure (no React, no native) so it's unit-testable
15
+ * without booting a render — same posture as `contentRotationDeg` and
16
+ * `buildPanoramaInitialSettings`.
17
+ *
18
+ * Coordinate conventions:
19
+ * - A `Point` is `{ x, y }`. Screen points are in the layout box's
20
+ * local space (origin = box top-left); image points are in pixel
21
+ * space (origin = image top-left, range [0..imageW] × [0..imageH]).
22
+ * - A `Quad` is exactly 4 points. `orderQuadCorners` canonicalises
23
+ * winding to [TL, TR, BR, BL] so downstream native perspective
24
+ * rectify gets corners in the order it expects.
25
+ */
26
+ /** A 2-D point in either screen-local or image-pixel space. */
27
+ export interface Point {
28
+ x: number;
29
+ y: number;
30
+ }
31
+ /** The contain-fit letterbox layout of the image inside its box. */
32
+ export interface ContainLayout {
33
+ /** Layout box width (on-screen px). */
34
+ width: number;
35
+ /** Layout box height (on-screen px). */
36
+ height: number;
37
+ }
38
+ /** Exactly four points (corners of a crop quad). */
39
+ export type Quad = [Point, Point, Point, Point];
40
+ /**
41
+ * The contain-fit transform: uniform scale + centring offsets that map
42
+ * image-pixel space into the on-screen layout box. Returns `null` when
43
+ * any dimension is non-positive (nothing to lay out).
44
+ *
45
+ * `scale` is `min(box.w / imageW, box.h / imageH)` — the same
46
+ * `resizeMode="contain"` math RN's <Image> applies — and `offX`/`offY`
47
+ * centre the scaled image, producing the letterbox bars.
48
+ */
49
+ export declare function containFit(layout: ContainLayout, imageW: number, imageH: number): {
50
+ scale: number;
51
+ offX: number;
52
+ offY: number;
53
+ } | null;
54
+ /**
55
+ * Map an on-screen point (layout-box local coords) → image-pixel coords.
56
+ * Inverse of {@link imageToScreen}. The result is clamped to
57
+ * `[0..imageW] × [0..imageH]` so a corner dragged onto / past the
58
+ * letterbox bar still yields a valid in-bounds pixel for the native crop
59
+ * (the user can't pick pixels that don't exist).
60
+ *
61
+ * Returns the un-mapped point unchanged when the layout is degenerate
62
+ * (see {@link containFit}) — the caller has no valid letterbox yet.
63
+ */
64
+ export declare function screenToImage(point: Point, layout: ContainLayout, imageW: number, imageH: number): Point;
65
+ /**
66
+ * Map an image-pixel point → on-screen point (layout-box local coords).
67
+ * Inverse of {@link screenToImage}. Used to seed the draggable corners
68
+ * from an image-space initial rect and to keep the overlay aligned to the
69
+ * letterboxed image.
70
+ *
71
+ * Returns the un-mapped point unchanged when the layout is degenerate.
72
+ */
73
+ export declare function imageToScreen(point: Point, layout: ContainLayout, imageW: number, imageH: number): Point;
74
+ /**
75
+ * Re-order 4 arbitrary corner points into canonical
76
+ * [TL, TR, BR, BL] (clockwise from top-left) winding.
77
+ *
78
+ * Strategy (robust to slight perspective skew, no trig):
79
+ * - Top two = the two points with the smallest `y`; bottom two = the
80
+ * largest `y`. Within each pair, the smaller `x` is left.
81
+ * Ties on `y` (a perfectly axis-aligned rect) resolve deterministically
82
+ * because the sort is stable and we then split by `x`.
83
+ *
84
+ * This matches the corner order the native `cropToQuad` perspective
85
+ * rectify expects (dst rect: TL→TR→BR→BL).
86
+ */
87
+ export declare function orderQuadCorners(pts: Quad): Quad;
88
+ /**
89
+ * 2× the signed area of a polygon via the shoelace formula. Positive for
90
+ * counter-clockwise winding, negative for clockwise, ~0 for degenerate.
91
+ * Exported for tests + reused by {@link isQuadValid}.
92
+ */
93
+ export declare function signedArea2(pts: Quad): number;
94
+ /**
95
+ * True when the 4 points form a usable crop quad:
96
+ * 1. **Non-degenerate area** — `|signed area|` ≥ `minArea` (default
97
+ * `1`, i.e. at least 1 px²). Rejects all-collinear / zero-size.
98
+ * 2. **Convex** — every cross-product of consecutive edges shares one
99
+ * sign (allowing zero for a straight, axis-aligned corner). Rejects
100
+ * self-intersecting / "bowtie" quads, which the native perspective
101
+ * warp can't rectify.
102
+ *
103
+ * Operates on the points in their given winding (call `orderQuadCorners`
104
+ * first if you need canonical order); convexity is winding-agnostic.
105
+ */
106
+ export declare function isQuadValid(pts: Quad, minArea?: number): boolean;
107
+ /**
108
+ * Target rectangle size for the perspective `dst` quad, derived from the
109
+ * 4 ORDERED ([TL, TR, BR, BL]) image-pixel corners:
110
+ * - `w` = average of the top edge (TL→TR) and bottom edge (BL→BR)
111
+ * lengths.
112
+ * - `h` = average of the left edge (TL→BL) and right edge (TR→BR)
113
+ * lengths.
114
+ * Averaging opposite edges gives a stable output size for a skewed quad
115
+ * (each pair of opposite edges differs under perspective; the mean is the
116
+ * least-distorting target). Rounds to whole pixels — the native crop
117
+ * allocates an integer-sized bitmap.
118
+ *
119
+ * Caller must pass corners already in [TL, TR, BR, BL] order (use
120
+ * {@link orderQuadCorners}); the math assumes that winding.
121
+ */
122
+ export declare function rectSizeForQuad(orderedImagePts: Quad): {
123
+ w: number;
124
+ h: number;
125
+ };
126
+ /**
127
+ * True when an ORDERED ([TL, TR, BR, BL]) image-pixel quad is, within
128
+ * `tolerancePx`, an axis-aligned rectangle — i.e. the cheap axis-aligned
129
+ * `cropToRect` path applies and no perspective warp is needed. The parent
130
+ * uses this to choose `cropToRect` vs `cropToQuad`.
131
+ *
132
+ * Checks the two top/bottom corners share a `y` and the two left/right
133
+ * corners share an `x`, all within tolerance.
134
+ */
135
+ export declare function isAxisAlignedRect(orderedImagePts: Quad, tolerancePx?: number): boolean;
136
+ //# sourceMappingURL=cropGeometry.d.ts.map
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * cropGeometry — pure coordinate + quad helpers behind the item-7
5
+ * draggable-corner crop editor (`RectCropPreview`).
6
+ *
7
+ * The editor shows the full result image with a `resizeMode="contain"`
8
+ * letterbox: the image is centred and uniformly scaled to fit the layout
9
+ * box, leaving symmetric bars on one axis. The 4 draggable corners live in
10
+ * ON-SCREEN coordinates (the touch space PanResponder reports), but the
11
+ * native crop needs IMAGE-PIXEL coordinates. These helpers are the
12
+ * letterbox transform and its inverse — extracted verbatim from
13
+ * `example/InscribedRectDebug.tsx` (~lines 178-204), which mapped an
14
+ * inscribed-rect from image px → screen the same way.
15
+ *
16
+ * Everything here is pure (no React, no native) so it's unit-testable
17
+ * without booting a render — same posture as `contentRotationDeg` and
18
+ * `buildPanoramaInitialSettings`.
19
+ *
20
+ * Coordinate conventions:
21
+ * - A `Point` is `{ x, y }`. Screen points are in the layout box's
22
+ * local space (origin = box top-left); image points are in pixel
23
+ * space (origin = image top-left, range [0..imageW] × [0..imageH]).
24
+ * - A `Quad` is exactly 4 points. `orderQuadCorners` canonicalises
25
+ * winding to [TL, TR, BR, BL] so downstream native perspective
26
+ * rectify gets corners in the order it expects.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.containFit = containFit;
30
+ exports.screenToImage = screenToImage;
31
+ exports.imageToScreen = imageToScreen;
32
+ exports.orderQuadCorners = orderQuadCorners;
33
+ exports.signedArea2 = signedArea2;
34
+ exports.isQuadValid = isQuadValid;
35
+ exports.rectSizeForQuad = rectSizeForQuad;
36
+ exports.isAxisAlignedRect = isAxisAlignedRect;
37
+ /**
38
+ * The contain-fit transform: uniform scale + centring offsets that map
39
+ * image-pixel space into the on-screen layout box. Returns `null` when
40
+ * any dimension is non-positive (nothing to lay out).
41
+ *
42
+ * `scale` is `min(box.w / imageW, box.h / imageH)` — the same
43
+ * `resizeMode="contain"` math RN's <Image> applies — and `offX`/`offY`
44
+ * centre the scaled image, producing the letterbox bars.
45
+ */
46
+ function containFit(layout, imageW, imageH) {
47
+ if (layout.width <= 0
48
+ || layout.height <= 0
49
+ || imageW <= 0
50
+ || imageH <= 0) {
51
+ return null;
52
+ }
53
+ const scale = Math.min(layout.width / imageW, layout.height / imageH);
54
+ const dispW = imageW * scale;
55
+ const dispH = imageH * scale;
56
+ const offX = (layout.width - dispW) / 2;
57
+ const offY = (layout.height - dispH) / 2;
58
+ return { scale, offX, offY };
59
+ }
60
+ /**
61
+ * Map an on-screen point (layout-box local coords) → image-pixel coords.
62
+ * Inverse of {@link imageToScreen}. The result is clamped to
63
+ * `[0..imageW] × [0..imageH]` so a corner dragged onto / past the
64
+ * letterbox bar still yields a valid in-bounds pixel for the native crop
65
+ * (the user can't pick pixels that don't exist).
66
+ *
67
+ * Returns the un-mapped point unchanged when the layout is degenerate
68
+ * (see {@link containFit}) — the caller has no valid letterbox yet.
69
+ */
70
+ function screenToImage(point, layout, imageW, imageH) {
71
+ const fit = containFit(layout, imageW, imageH);
72
+ if (!fit)
73
+ return point;
74
+ const x = (point.x - fit.offX) / fit.scale;
75
+ const y = (point.y - fit.offY) / fit.scale;
76
+ return {
77
+ x: clamp(x, 0, imageW),
78
+ y: clamp(y, 0, imageH),
79
+ };
80
+ }
81
+ /**
82
+ * Map an image-pixel point → on-screen point (layout-box local coords).
83
+ * Inverse of {@link screenToImage}. Used to seed the draggable corners
84
+ * from an image-space initial rect and to keep the overlay aligned to the
85
+ * letterboxed image.
86
+ *
87
+ * Returns the un-mapped point unchanged when the layout is degenerate.
88
+ */
89
+ function imageToScreen(point, layout, imageW, imageH) {
90
+ const fit = containFit(layout, imageW, imageH);
91
+ if (!fit)
92
+ return point;
93
+ return {
94
+ x: fit.offX + point.x * fit.scale,
95
+ y: fit.offY + point.y * fit.scale,
96
+ };
97
+ }
98
+ /**
99
+ * Re-order 4 arbitrary corner points into canonical
100
+ * [TL, TR, BR, BL] (clockwise from top-left) winding.
101
+ *
102
+ * Strategy (robust to slight perspective skew, no trig):
103
+ * - Top two = the two points with the smallest `y`; bottom two = the
104
+ * largest `y`. Within each pair, the smaller `x` is left.
105
+ * Ties on `y` (a perfectly axis-aligned rect) resolve deterministically
106
+ * because the sort is stable and we then split by `x`.
107
+ *
108
+ * This matches the corner order the native `cropToQuad` perspective
109
+ * rectify expects (dst rect: TL→TR→BR→BL).
110
+ */
111
+ function orderQuadCorners(pts) {
112
+ // Sort a copy by y ascending so [0,1] are the top pair, [2,3] bottom.
113
+ const byY = [...pts].sort((a, b) => a.y - b.y);
114
+ const [t0, t1, b0, b1] = byY;
115
+ // Within each horizontal pair, smaller x is the left corner.
116
+ const [tl, tr] = t0.x <= t1.x ? [t0, t1] : [t1, t0];
117
+ const [bl, br] = b0.x <= b1.x ? [b0, b1] : [b1, b0];
118
+ return [tl, tr, br, bl];
119
+ }
120
+ /**
121
+ * 2× the signed area of a polygon via the shoelace formula. Positive for
122
+ * counter-clockwise winding, negative for clockwise, ~0 for degenerate.
123
+ * Exported for tests + reused by {@link isQuadValid}.
124
+ */
125
+ function signedArea2(pts) {
126
+ let sum = 0;
127
+ for (let i = 0; i < pts.length; i++) {
128
+ const a = pts[i];
129
+ const b = pts[(i + 1) % pts.length];
130
+ sum += a.x * b.y - b.x * a.y;
131
+ }
132
+ return sum;
133
+ }
134
+ /**
135
+ * True when the 4 points form a usable crop quad:
136
+ * 1. **Non-degenerate area** — `|signed area|` ≥ `minArea` (default
137
+ * `1`, i.e. at least 1 px²). Rejects all-collinear / zero-size.
138
+ * 2. **Convex** — every cross-product of consecutive edges shares one
139
+ * sign (allowing zero for a straight, axis-aligned corner). Rejects
140
+ * self-intersecting / "bowtie" quads, which the native perspective
141
+ * warp can't rectify.
142
+ *
143
+ * Operates on the points in their given winding (call `orderQuadCorners`
144
+ * first if you need canonical order); convexity is winding-agnostic.
145
+ */
146
+ function isQuadValid(pts, minArea = 1) {
147
+ if (Math.abs(signedArea2(pts)) < minArea * 2)
148
+ return false;
149
+ return isConvex(pts);
150
+ }
151
+ /**
152
+ * Target rectangle size for the perspective `dst` quad, derived from the
153
+ * 4 ORDERED ([TL, TR, BR, BL]) image-pixel corners:
154
+ * - `w` = average of the top edge (TL→TR) and bottom edge (BL→BR)
155
+ * lengths.
156
+ * - `h` = average of the left edge (TL→BL) and right edge (TR→BR)
157
+ * lengths.
158
+ * Averaging opposite edges gives a stable output size for a skewed quad
159
+ * (each pair of opposite edges differs under perspective; the mean is the
160
+ * least-distorting target). Rounds to whole pixels — the native crop
161
+ * allocates an integer-sized bitmap.
162
+ *
163
+ * Caller must pass corners already in [TL, TR, BR, BL] order (use
164
+ * {@link orderQuadCorners}); the math assumes that winding.
165
+ */
166
+ function rectSizeForQuad(orderedImagePts) {
167
+ const [tl, tr, br, bl] = orderedImagePts;
168
+ const top = dist(tl, tr);
169
+ const bottom = dist(bl, br);
170
+ const left = dist(tl, bl);
171
+ const right = dist(tr, br);
172
+ return {
173
+ w: Math.round((top + bottom) / 2),
174
+ h: Math.round((left + right) / 2),
175
+ };
176
+ }
177
+ /**
178
+ * True when an ORDERED ([TL, TR, BR, BL]) image-pixel quad is, within
179
+ * `tolerancePx`, an axis-aligned rectangle — i.e. the cheap axis-aligned
180
+ * `cropToRect` path applies and no perspective warp is needed. The parent
181
+ * uses this to choose `cropToRect` vs `cropToQuad`.
182
+ *
183
+ * Checks the two top/bottom corners share a `y` and the two left/right
184
+ * corners share an `x`, all within tolerance.
185
+ */
186
+ function isAxisAlignedRect(orderedImagePts, tolerancePx = 1) {
187
+ const [tl, tr, br, bl] = orderedImagePts;
188
+ return (Math.abs(tl.y - tr.y) <= tolerancePx
189
+ && Math.abs(bl.y - br.y) <= tolerancePx
190
+ && Math.abs(tl.x - bl.x) <= tolerancePx
191
+ && Math.abs(tr.x - br.x) <= tolerancePx);
192
+ }
193
+ /** Convexity test: all consecutive edge cross-products share a sign. */
194
+ function isConvex(pts) {
195
+ let sign = 0;
196
+ for (let i = 0; i < pts.length; i++) {
197
+ const a = pts[i];
198
+ const b = pts[(i + 1) % pts.length];
199
+ const c = pts[(i + 2) % pts.length];
200
+ const cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
201
+ if (cross !== 0) {
202
+ const s = cross > 0 ? 1 : -1;
203
+ if (sign === 0)
204
+ sign = s;
205
+ else if (s !== sign)
206
+ return false;
207
+ }
208
+ }
209
+ return true;
210
+ }
211
+ /** Euclidean distance between two points. */
212
+ function dist(a, b) {
213
+ return Math.hypot(a.x - b.x, a.y - b.y);
214
+ }
215
+ /** Clamp `v` into the inclusive `[lo, hi]` range. */
216
+ function clamp(v, lo, hi) {
217
+ if (v < lo)
218
+ return lo;
219
+ if (v > hi)
220
+ return hi;
221
+ return v;
222
+ }
223
+ //# sourceMappingURL=cropGeometry.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DISPLAY_DECODE_IMAGE_PROPS — props every <Image> that displays a
3
+ * FULL-RES capture (a stitched panorama or a photo file) must spread.
4
+ *
5
+ * Why this exists (the accumulation half of the OOM crash):
6
+ * On Android 8+, decoded bitmap pixels live in the NATIVE heap, and the
7
+ * source here is the full-resolution capture file — a wide panorama can
8
+ * be tens of megapixels. Without `resizeMethod="resize"`, Android/Fresco
9
+ * decodes the source at FULL resolution into a native bitmap that the
10
+ * mounted <Image> pins (not LRU-evictable), and Fresco's URI-keyed cache
11
+ * keeps it even after the view unmounts. Each capture (especially wide
12
+ * panoramas) then accumulates tens of MB of native heap until lmkd
13
+ * OOM-kills the app. 'resize' decodes at the on-screen (~device-width)
14
+ * size instead, making per-image memory tiny and panorama-size-
15
+ * independent. No-op on iOS (harmless).
16
+ *
17
+ * Centralised (rather than a bare `resizeMethod="resize"` at each call
18
+ * site) so the decode strategy + its rationale have one home, and so the
19
+ * contract is unit-testable without mounting a component. Spread it:
20
+ * <Image source={...} resizeMode="cover" {...DISPLAY_DECODE_IMAGE_PROPS} />
21
+ */
22
+ export declare const DISPLAY_DECODE_IMAGE_PROPS: {
23
+ readonly resizeMethod: "resize";
24
+ };
25
+ //# sourceMappingURL=displayDecodeImageProps.d.ts.map