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
@@ -1,74 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- //
3
- // pose_test.cpp — v0.10.0 audit #9A
4
- //
5
- // Layout / size invariants for the cross-platform POD structs that
6
- // marshal AR-frame pose data between Swift/Kotlin and shared C++.
7
- // The Pose / PlaneTransform structs MUST stay binary-compatible
8
- // across iOS (Swift → C++) and Android (Kotlin → JNI → C++) — any
9
- // silent field reorder, padding shift, or size change would diverge
10
- // gate decisions between platforms.
11
- //
12
- // These are pinned to the contract in `cpp/ar_frame_pose.h`'s
13
- // docstring; if the struct shape evolves intentionally, update both
14
- // the docstring and these tests in the same commit.
15
-
16
- #include "ar_frame_pose.h"
17
-
18
- #include <gtest/gtest.h>
19
-
20
- #include <cstddef>
21
- #include <type_traits>
22
-
23
- using retailens::Pose;
24
- using retailens::PlaneTransform;
25
-
26
- TEST(PoseLayoutTest, IsStandardLayoutPod) {
27
- // Required for `memcpy` marshalling and for the iOS Obj-C++ /
28
- // Android JNI bridges to write into the struct directly.
29
- EXPECT_TRUE(std::is_standard_layout<Pose>::value);
30
- EXPECT_TRUE(std::is_trivially_copyable<Pose>::value);
31
- }
32
-
33
- TEST(PoseLayoutTest, SizeMatchesExpectedFields) {
34
- // 11 floats (tx, ty, tz, qx, qy, qz, qw, fx, fy, cx, cy) + 2 int32_t
35
- // (imageWidth, imageHeight) = 11*4 + 2*4 = 52 bytes. No padding
36
- // expected: every field is 4-byte aligned and the struct contains
37
- // only 4-byte primitives.
38
- EXPECT_EQ(sizeof(Pose), static_cast<std::size_t>(11 * 4 + 2 * 4));
39
- }
40
-
41
- TEST(PoseLayoutTest, FieldOrderMatchesContract) {
42
- // Translation comes before rotation; rotation before intrinsics;
43
- // intrinsics before image dimensions. Swift / Kotlin marshallers
44
- // assume this order — flipping any pair silently breaks the
45
- // memcpy-based bridge.
46
- EXPECT_EQ(offsetof(Pose, tx), 0u);
47
- EXPECT_EQ(offsetof(Pose, ty), sizeof(float) * 1);
48
- EXPECT_EQ(offsetof(Pose, tz), sizeof(float) * 2);
49
- EXPECT_EQ(offsetof(Pose, qx), sizeof(float) * 3);
50
- EXPECT_EQ(offsetof(Pose, qy), sizeof(float) * 4);
51
- EXPECT_EQ(offsetof(Pose, qz), sizeof(float) * 5);
52
- EXPECT_EQ(offsetof(Pose, qw), sizeof(float) * 6);
53
- EXPECT_EQ(offsetof(Pose, fx), sizeof(float) * 7);
54
- EXPECT_EQ(offsetof(Pose, fy), sizeof(float) * 8);
55
- EXPECT_EQ(offsetof(Pose, cx), sizeof(float) * 9);
56
- EXPECT_EQ(offsetof(Pose, cy), sizeof(float) * 10);
57
- EXPECT_EQ(offsetof(Pose, imageWidth), sizeof(float) * 11);
58
- EXPECT_EQ(offsetof(Pose, imageHeight),
59
- sizeof(float) * 11 + sizeof(int32_t));
60
- }
61
-
62
- TEST(PlaneTransformLayoutTest, IsStandardLayoutPod) {
63
- EXPECT_TRUE(std::is_standard_layout<PlaneTransform>::value);
64
- EXPECT_TRUE(std::is_trivially_copyable<PlaneTransform>::value);
65
- }
66
-
67
- TEST(PlaneTransformLayoutTest, SixteenFloatsContiguous) {
68
- // The `m[16]` array MUST be a contiguous 64-byte block — both
69
- // bridges call `memcpy(planeTransform.m, source, 64)`. No leading
70
- // padding, no field reorder (there's only one field, but pinning the
71
- // size catches any accidental wrapper/struct change).
72
- EXPECT_EQ(sizeof(PlaneTransform), static_cast<std::size_t>(16 * 4));
73
- EXPECT_EQ(offsetof(PlaneTransform, m), 0u);
74
- }
@@ -1,132 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- //
3
- // stitcher_frame_data_test.cpp — v0.10.0 audit #9A
4
- //
5
- // Sanity coverage for the `StitcherFrameData` POD payload + the
6
- // `PixelBufferReader` interface contract. The shared C++
7
- // `StitcherFrameData` is constructed by both the iOS Obj-C++ side
8
- // (`StitcherFrameHostObject.mm`) and the Android JNI side
9
- // (`stitcher_frame_jni.cpp`); these tests pin the default-construction
10
- // invariants both sides depend on (e.g. `hasTranslation=false`,
11
- // `qw=1.0`).
12
- //
13
- // The `PixelBufferReader` tests use a fake-buffer implementation
14
- // (`FakePixelBufferReader`) to validate the `copyTo` clipping
15
- // behaviour the docstring promises.
16
-
17
- #include "stitcher_frame_data.hpp"
18
-
19
- #include <gtest/gtest.h>
20
-
21
- #include <cstdint>
22
- #include <cstring>
23
- #include <memory>
24
- #include <vector>
25
-
26
- using retailens::PixelBufferReader;
27
- using retailens::StitcherFrameData;
28
-
29
- namespace {
30
-
31
- /// Minimal fake reader — backs a fixed byte vector. Used by the
32
- /// `PixelBufferReader` contract tests below to verify
33
- /// `copyTo` clips at the smaller of (maxBytes, byteSize).
34
- class FakePixelBufferReader : public PixelBufferReader {
35
- public:
36
- explicit FakePixelBufferReader(std::vector<uint8_t> bytes)
37
- : _bytes(std::move(bytes)) {}
38
-
39
- std::size_t byteSize() const override { return _bytes.size(); }
40
-
41
- std::size_t copyTo(uint8_t* dst, std::size_t maxBytes) override {
42
- const std::size_t n =
43
- maxBytes < _bytes.size() ? maxBytes : _bytes.size();
44
- std::memcpy(dst, _bytes.data(), n);
45
- return n;
46
- }
47
-
48
- private:
49
- std::vector<uint8_t> _bytes;
50
- };
51
-
52
- } // namespace
53
-
54
- // ─── StitcherFrameData default-construction invariants ─────────────
55
-
56
- TEST(StitcherFrameDataTest, DefaultsAreSafeForJSIDispatch) {
57
- // The JSI host object's `get()` dispatch keys off these defaults to
58
- // expose `undefined` for unset fields. In particular:
59
- // - `hasTranslation == false` → pose.translation === undefined
60
- // - `arTrackingState.empty()` → arTrackingState === undefined
61
- // - `qw == 1.0` with rest zero → identity rotation (safe default
62
- // for non-AR mode where rotation is unknown)
63
- StitcherFrameData d;
64
- EXPECT_EQ(d.width, 0);
65
- EXPECT_EQ(d.height, 0);
66
- EXPECT_TRUE(d.source.empty());
67
- EXPECT_TRUE(d.pixelFormat.empty());
68
- EXPECT_TRUE(d.orientation.empty());
69
- EXPECT_DOUBLE_EQ(d.timestampNs, 0.0);
70
- EXPECT_DOUBLE_EQ(d.qx, 0.0);
71
- EXPECT_DOUBLE_EQ(d.qy, 0.0);
72
- EXPECT_DOUBLE_EQ(d.qz, 0.0);
73
- EXPECT_DOUBLE_EQ(d.qw, 1.0); // identity rotation
74
- EXPECT_DOUBLE_EQ(d.tx, 0.0);
75
- EXPECT_DOUBLE_EQ(d.ty, 0.0);
76
- EXPECT_DOUBLE_EQ(d.tz, 0.0);
77
- EXPECT_FALSE(d.hasTranslation);
78
- EXPECT_TRUE(d.arTrackingState.empty());
79
- EXPECT_EQ(d.pixelReader, nullptr);
80
- }
81
-
82
- TEST(StitcherFrameDataTest, IsCopyable) {
83
- // `StitcherFrameData` is documented as "value-typed (cheap to copy;
84
- // ~100 bytes)". Copy needs to deep-copy the strings + bump the
85
- // pixelReader shared_ptr refcount.
86
- StitcherFrameData a;
87
- a.source = "ar";
88
- a.width = 1920;
89
- a.height = 1080;
90
- a.pixelReader = std::make_shared<FakePixelBufferReader>(
91
- std::vector<uint8_t>{1, 2, 3});
92
-
93
- StitcherFrameData b = a;
94
- EXPECT_EQ(b.source, "ar");
95
- EXPECT_EQ(b.width, 1920);
96
- EXPECT_EQ(b.height, 1080);
97
- ASSERT_NE(b.pixelReader, nullptr);
98
- EXPECT_EQ(b.pixelReader.use_count(), 2); // both a and b hold a ref
99
- EXPECT_EQ(b.pixelReader->byteSize(), 3u);
100
- }
101
-
102
- // ─── PixelBufferReader contract ────────────────────────────────────
103
-
104
- TEST(PixelBufferReaderTest, CopyToReturnsAllBytesWhenMaxBytesExceedsSize) {
105
- FakePixelBufferReader reader({0x11, 0x22, 0x33});
106
- uint8_t buf[8] = {0};
107
- const std::size_t written = reader.copyTo(buf, sizeof(buf));
108
- EXPECT_EQ(written, 3u);
109
- EXPECT_EQ(buf[0], 0x11);
110
- EXPECT_EQ(buf[1], 0x22);
111
- EXPECT_EQ(buf[2], 0x33);
112
- EXPECT_EQ(buf[3], 0u); // untouched tail
113
- }
114
-
115
- TEST(PixelBufferReaderTest, CopyToClipsWhenMaxBytesIsSmaller) {
116
- // Contract per stitcher_frame_data.hpp: "Implementations MUST handle
117
- // the case where maxBytes < byteSize() (clip silently)."
118
- FakePixelBufferReader reader({0xAA, 0xBB, 0xCC, 0xDD});
119
- uint8_t buf[2] = {0};
120
- const std::size_t written = reader.copyTo(buf, sizeof(buf));
121
- EXPECT_EQ(written, 2u);
122
- EXPECT_EQ(buf[0], 0xAA);
123
- EXPECT_EQ(buf[1], 0xBB);
124
- }
125
-
126
- TEST(PixelBufferReaderTest, CopyToWithZeroMaxBytesReturnsZero) {
127
- FakePixelBufferReader reader({0x01, 0x02, 0x03});
128
- uint8_t dummy = 0xFF;
129
- const std::size_t written = reader.copyTo(&dummy, 0);
130
- EXPECT_EQ(written, 0u);
131
- EXPECT_EQ(dummy, 0xFF); // dst untouched when maxBytes == 0
132
- }
@@ -1,33 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- //
3
- // jsi.h — TEST-ONLY stub of facebook::jsi types.
4
- //
5
- // The real jsi.h ships with React Native and pulls in a large surface
6
- // area (Runtime, Value, Object, Function, HostObject, HostFunction,
7
- // Array, ArrayBuffer, PropNameID, …) along with the build infra to
8
- // link them. For pure-C++ unit tests that exercise data-structure
9
- // invariants of code that REFERENCES jsi types but never CALLS into
10
- // them (e.g. `StitcherWorkletRegistry` storing a `shared_ptr` and
11
- // forwarding `Runtime&` to a constructor stub), we only need the
12
- // types to be NAMED so headers compile.
13
- //
14
- // Pattern: this stub is placed first on the test target's include
15
- // path so `#include <jsi/jsi.h>` resolves here instead of to RN's
16
- // real header. Production builds NEVER see this file — it lives
17
- // only under `cpp/tests/stubs/`, which is referenced exclusively by
18
- // `cpp/tests/CMakeLists.txt`.
19
- //
20
- // Tests that need to actually CONSTRUCT or CALL into JSI types should
21
- // not use this stub — they should run against a real JSI runtime (a
22
- // future v0.11.0+ test target that links Hermes).
23
-
24
- #pragma once
25
-
26
- namespace facebook {
27
- namespace jsi {
28
-
29
- class Runtime {};
30
- class Value {};
31
-
32
- } // namespace jsi
33
- } // namespace facebook
@@ -1,34 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- //
3
- // WKTJsiWorklet.h — TEST-ONLY stub of RNWorklet::WorkletInvoker.
4
- //
5
- // `cpp/stitcher_worklet_registry.cpp` constructs a
6
- // `std::make_shared<RNWorklet::WorkletInvoker>(runtime, value)` inside
7
- // `install`. The real WorkletInvoker (from react-native-worklets-core)
8
- // captures the worklet's source / closure / runtime affinity and is
9
- // non-trivial to stand up in a unit-test context.
10
- //
11
- // This stub provides JUST the symbols needed for the registry to
12
- // compile and link. The constructor and destructor are no-ops; calling
13
- // methods on a stub invoker is undefined behaviour, but the registry
14
- // itself never does (it only stores the shared_ptr and hands it out
15
- // via `snapshot`). Tests construct entries directly via
16
- // `_installEntryForTests(nullptr)` to avoid even the trivial
17
- // allocation.
18
- //
19
- // See cpp/tests/stubs/jsi/jsi.h for the parallel stub of facebook::jsi.
20
-
21
- #pragma once
22
-
23
- #include <jsi/jsi.h>
24
-
25
- namespace RNWorklet {
26
-
27
- class WorkletInvoker {
28
- public:
29
- WorkletInvoker(facebook::jsi::Runtime& /*runtime*/,
30
- const facebook::jsi::Value& /*workletValue*/) {}
31
- ~WorkletInvoker() = default;
32
- };
33
-
34
- } // namespace RNWorklet
@@ -1,48 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * Unit tests for the warp-canvas size guard (cpp/warp_guard.hpp).
4
- *
5
- * The guard decides when a warp ROI is "degenerate" — the trigger for
6
- * both the cylindrical-fallback pre-pass and the in-loop final safety net.
7
- * The cases that matter: normal ROIs pass, non-positive dims fail, the
8
- * 100 MP boundary is inclusive, the real observed divergence (8171×12336)
9
- * is caught, and a ROI whose int32 area would overflow is still caught.
10
- */
11
- #include "warp_guard.hpp"
12
-
13
- #include <gtest/gtest.h>
14
-
15
- using retailens::warpRoiExceedsGuard;
16
-
17
- TEST(WarpGuard, AcceptsNormalRoi) {
18
- EXPECT_FALSE(warpRoiExceedsGuard(4000, 2000)); // 8 MP
19
- EXPECT_FALSE(warpRoiExceedsGuard(1, 1));
20
- }
21
-
22
- TEST(WarpGuard, RejectsNonPositiveDims) {
23
- EXPECT_TRUE(warpRoiExceedsGuard(0, 1000));
24
- EXPECT_TRUE(warpRoiExceedsGuard(1000, 0));
25
- EXPECT_TRUE(warpRoiExceedsGuard(-5, 1000));
26
- EXPECT_TRUE(warpRoiExceedsGuard(1000, -5));
27
- }
28
-
29
- TEST(WarpGuard, BoundaryIsInclusive) {
30
- EXPECT_FALSE(warpRoiExceedsGuard(100000, 1000)); // exactly 100 MP — allowed
31
- EXPECT_TRUE(warpRoiExceedsGuard(100000, 1001)); // 100.1 MP — over
32
- }
33
-
34
- TEST(WarpGuard, RejectsTheObservedDivergence) {
35
- // 8171×12336 = 100.8 MP — the exact STITCH_CAMERA_PARAMS_FAIL canvas.
36
- EXPECT_TRUE(warpRoiExceedsGuard(8171, 12336));
37
- }
38
-
39
- TEST(WarpGuard, RejectsInt32OverflowingRoi) {
40
- // 65536×65536 = 2^32; an int32 area would wrap to 0 and slip past the
41
- // guard. The int64 area math catches it.
42
- EXPECT_TRUE(warpRoiExceedsGuard(65536, 65536));
43
- }
44
-
45
- TEST(WarpGuard, HonoursCustomThreshold) {
46
- EXPECT_FALSE(warpRoiExceedsGuard(1000, 1000, 2'000'000)); // 1 MP < 2 MP
47
- EXPECT_TRUE(warpRoiExceedsGuard(2000, 1000, 1'000'000)); // 2 MP > 1 MP
48
- }
@@ -1,190 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * Unit tests for the JS→native settings bridge.
4
- *
5
- * Scope: every adapter (panoramaSettingsToNativeConfig,
6
- * slitscanSettingsToNativeConfig, hybridSettingsToNativeConfig)
7
- * round-trip from a hierarchical typed input to the flat wire dict
8
- * the native side reads. Asserts both:
9
- *
10
- * 1. Naming — JS key `registration.ncc1d.searchRadius` becomes
11
- * native key `nccSearchRadius1d` (and similar mappings). Each
12
- * DEFAULT_* snapshot's expected wire dict is enumerated below;
13
- * drift in either direction (lib drops a key, or adds a phantom
14
- * one) is caught here.
15
- *
16
- * 2. Presence-as-enable — undefined optional sub-objects in the
17
- * typed shape (`registration.ncc1d`, `registration.ncc2d`,
18
- * `registration.ncc2d.emaSmoothing`, `registration.ncc2d.panAxisLock`,
19
- * `frameSelection.flow`, `advanced`) translate to explicit
20
- * `enable*: false` (or the absence of all the sub-object's
21
- * payload keys) on the wire. Many of these have been silent
22
- * drift hazards historically — the old flat type required the
23
- * consumer to set BOTH `enable1dNcc: true` AND `nccSearchRadius1d:
24
- * <value>`; v0.4 makes them inseparable by collapsing into a
25
- * single optional sub-object, and this file is what guarantees
26
- * the wire side still gets both halves.
27
- *
28
- * 3. Engine-discriminated coverage — plane source variants
29
- * ('Disabled' / 'ARKitDetected' / 'Virtual') gate which optional
30
- * plane fields are emitted; the bridge filters those at the
31
- * adapter boundary so the modal's per-source rendering doesn't
32
- * get mislead by stale-but-present keys from a previous source
33
- * selection.
34
- *
35
- * These tests are pure-TS; no React Native module import. Jest config
36
- * (`jest.config.js`) routes test files in `__tests__/` through ts-jest
37
- * with the `node` testEnvironment.
38
- */
39
-
40
- import {
41
- DEFAULT_FLOW_GATE_SETTINGS,
42
- DEFAULT_PANORAMA_SETTINGS,
43
- type PanoramaSettings,
44
- } from '../PanoramaSettings';
45
- import {
46
- panoramaSettingsToNativeConfig,
47
- } from '../PanoramaSettingsBridge';
48
-
49
-
50
- // ════════════════════════════════════════════════════════════════════
51
- // PANORAMA — batch-keyframe engine
52
- // ════════════════════════════════════════════════════════════════════
53
-
54
- describe('panoramaSettingsToNativeConfig', () => {
55
- it('round-trips DEFAULT_PANORAMA_SETTINGS to the expected flat dict', () => {
56
- const cfg = panoramaSettingsToNativeConfig(DEFAULT_PANORAMA_SETTINGS);
57
-
58
- // Cross-cutting
59
- expect(cfg.captureSource).toBe('ar');
60
-
61
- // BatchStitcherSettings
62
- expect(cfg.stitchMode).toBe('auto');
63
- expect(cfg.warperType).toBe('plane');
64
- expect(cfg.blenderType).toBe('multiband');
65
- expect(cfg.seamFinderType).toBe('graphcut');
66
- expect(cfg.enableMaxInscribedRectCrop).toBe(false);
67
-
68
- // FrameSelectionSettings
69
- expect(cfg.frameSelectionMode).toBe('flow-based');
70
- expect(cfg.keyframeMaxCount).toBe(6);
71
- expect(cfg.keyframeOverlapThreshold).toBe(0.2);
72
- expect(cfg.maxKeyframeIntervalMs).toBe(2000);
73
-
74
- // FlowGateSettings (flow is defined in the default)
75
- expect(cfg.flowNoveltyPercentile).toBe(0.85);
76
- expect(cfg.flowEvalEveryNFrames).toBe(5);
77
- expect(cfg.flowMaxTranslationCm).toBe(50);
78
- expect(cfg.flowMaxCorners).toBe(150);
79
- expect(cfg.flowQualityLevel).toBe(0.01);
80
- expect(cfg.flowMinDistance).toBe(10);
81
- });
82
-
83
- it('falls back to DEFAULT_FLOW_GATE_SETTINGS when frameSelection.flow is undefined', () => {
84
- // F10 Phase 2 review B1 — native compiled-in defaults disagree
85
- // with the JS defaults for two flow knobs (maxTranslationCm and
86
- // evalEveryNFrames). The bridge must always emit every flow key
87
- // so sparse-literal hosts get the JS defaults on the wire, not
88
- // the native fallbacks.
89
- const noFlow: PanoramaSettings = {
90
- ...DEFAULT_PANORAMA_SETTINGS,
91
- frameSelection: {
92
- ...DEFAULT_PANORAMA_SETTINGS.frameSelection,
93
- flow: undefined,
94
- },
95
- };
96
- const cfg = panoramaSettingsToNativeConfig(noFlow);
97
-
98
- expect(cfg.frameSelectionMode).toBe('flow-based');
99
- expect(cfg.keyframeMaxCount).toBe(6);
100
- expect(cfg.keyframeOverlapThreshold).toBe(0.2);
101
-
102
- // Every flow.* native key present, matching DEFAULT_FLOW_GATE_SETTINGS.
103
- expect(cfg.flowNoveltyPercentile).toBe(DEFAULT_FLOW_GATE_SETTINGS.noveltyPercentile);
104
- expect(cfg.flowEvalEveryNFrames).toBe(DEFAULT_FLOW_GATE_SETTINGS.evalEveryNFrames);
105
- expect(cfg.flowMaxTranslationCm).toBe(DEFAULT_FLOW_GATE_SETTINGS.maxTranslationCm);
106
- expect(cfg.flowMaxCorners).toBe(DEFAULT_FLOW_GATE_SETTINGS.maxCorners);
107
- expect(cfg.flowQualityLevel).toBe(DEFAULT_FLOW_GATE_SETTINGS.qualityLevel);
108
- expect(cfg.flowMinDistance).toBe(DEFAULT_FLOW_GATE_SETTINGS.minDistance);
109
- });
110
-
111
- it('emits flow defaults to the wire when frameSelection.flow is undefined AND mode is flow-based', () => {
112
- // F10 Phase 2 review N3 — the realistic user-facing case:
113
- // host writes `mode: 'flow-based'` but omits the flow sub-tree.
114
- // Pre-B1-fix, the gate would silently run with native fallbacks
115
- // (flowMaxTranslationCm=0, flowEvalEveryNFrames=1) instead of
116
- // the JS defaults (50 cm budget, 5× throttle).
117
- const s: PanoramaSettings = {
118
- ...DEFAULT_PANORAMA_SETTINGS,
119
- frameSelection: {
120
- mode: 'flow-based',
121
- maxKeyframes: 6,
122
- overlapThreshold: 0.20,
123
- maxKeyframeIntervalMs: 2000,
124
- // flow omitted — legal per the optional `?` in the type
125
- },
126
- };
127
- const cfg = panoramaSettingsToNativeConfig(s);
128
-
129
- expect(cfg.flowMaxTranslationCm).toBe(50);
130
- expect(cfg.flowEvalEveryNFrames).toBe(5);
131
- expect(cfg.flowNoveltyPercentile).toBe(0.85);
132
- expect(cfg.flowMaxCorners).toBe(150);
133
- expect(cfg.flowQualityLevel).toBe(0.01);
134
- expect(cfg.flowMinDistance).toBe(10);
135
- });
136
-
137
- it('locks down the full wire-key set for DEFAULT_PANORAMA_SETTINGS', () => {
138
- // F10 Phase 2 review N4 — mirror the hybrid test below. Lock
139
- // down which keys leave the bridge so a future field accidentally
140
- // riding along (e.g. `debug` being treated as a wire knob) fails
141
- // this test immediately.
142
- const cfg = panoramaSettingsToNativeConfig(DEFAULT_PANORAMA_SETTINGS);
143
- expect(Object.keys(cfg).sort()).toEqual([
144
- 'blenderType',
145
- 'captureSource',
146
- 'enableMaxInscribedRectCrop',
147
- 'flowEvalEveryNFrames',
148
- 'flowMaxCorners',
149
- 'flowMaxTranslationCm',
150
- 'flowMinDistance',
151
- 'flowNoveltyPercentile',
152
- 'flowQualityLevel',
153
- 'frameSelectionMode',
154
- 'keyframeMaxCount',
155
- 'keyframeOverlapThreshold',
156
- 'maxKeyframeIntervalMs',
157
- 'seamFinderType',
158
- 'stitchMode',
159
- 'warperType',
160
- ]);
161
- });
162
-
163
- it('honours captureSource and stitcher overrides', () => {
164
- const overridden: PanoramaSettings = {
165
- ...DEFAULT_PANORAMA_SETTINGS,
166
- captureSource: 'non-ar',
167
- debug: true,
168
- stitcher: {
169
- stitchMode: 'scans',
170
- warperType: 'spherical',
171
- blenderType: 'feather',
172
- seamFinderType: 'skip',
173
- enableMaxInscribedRectCrop: true,
174
- },
175
- };
176
- const cfg = panoramaSettingsToNativeConfig(overridden);
177
-
178
- expect(cfg.captureSource).toBe('non-ar');
179
- expect(cfg.stitchMode).toBe('scans');
180
- expect(cfg.warperType).toBe('spherical');
181
- expect(cfg.blenderType).toBe('feather');
182
- expect(cfg.seamFinderType).toBe('skip');
183
- expect(cfg.enableMaxInscribedRectCrop).toBe(true);
184
- // Note: `debug` is intentionally NOT on the wire — it's a
185
- // JS-side UI gate, not a native config knob. The bridge MUST
186
- // omit it; if a future change starts emitting it, the modal's
187
- // operator-facing semantics will silently drift.
188
- expect(cfg).not.toHaveProperty('debug');
189
- });
190
- });
@@ -1,120 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * Unit tests for the band/tile orientation-decision functions in
4
- * `PanoramaBandOverlay` — the pure logic behind the v0.13.1 EXIF
5
- * double-rotation fix.
6
- *
7
- * Why test the pure functions, not a render: the lib's jest config is
8
- * pure-TS (`ts-jest` + node env, no `@testing-library/react-native`;
9
- * see jest.config.js header). The orientation contract lives entirely
10
- * in `bandThumbRotation` / `tileRotation`, which the component now calls
11
- * directly — so exercising them here covers the real code path.
12
- *
13
- * The bug these guard against:
14
- * Saved `keyframe-N.jpg` files are sensor-native LANDSCAPE pixels with
15
- * EXIF Orientation = 6 ("rotate 90° CW"). RN's <Image> auto-rotates
16
- * them upright. v0.12 ALSO applied a JS rotate transform to the tiles
17
- * → double-rotation → thumbnails 90° off in portrait-locked landscape.
18
- * The fix: tiles get NO transform in the portrait-locked
19
- * (vertical=false) path; the single cumulative thumb (no EXIF) still
20
- * does.
21
- */
22
-
23
- // Mock react-native so importing the SUT module doesn't pull the native
24
- // StyleSheet/Image bridge (we only call the pure functions). Matches
25
- // the mocking approach in useOrientationDrift.test.ts.
26
- jest.mock('react-native', () => ({
27
- Image: 'Image',
28
- ScrollView: 'ScrollView',
29
- StyleSheet: { create: (s: Record<string, unknown>) => s, absoluteFill: {} },
30
- Text: 'Text',
31
- View: 'View',
32
- }));
33
-
34
- import {
35
- _bandThumbRotationForTests as bandThumbRotation,
36
- _tileRotationForTests as tileRotation,
37
- type BandCaptureOrientation,
38
- } from '../PanoramaBandOverlay';
39
-
40
- const PORTRAIT: BandCaptureOrientation = 'portrait';
41
- const UPSIDE: BandCaptureOrientation = 'portrait-upside-down';
42
- const LEFT: BandCaptureOrientation = 'landscape-left';
43
- const RIGHT: BandCaptureOrientation = 'landscape-right';
44
-
45
- describe('bandThumbRotation — single cumulative thumb (no EXIF source)', () => {
46
- describe('vertical=false (portrait-locked UI)', () => {
47
- it('does not rotate in portrait', () => {
48
- expect(bandThumbRotation(PORTRAIT, false)).toBeUndefined();
49
- });
50
-
51
- it('does not rotate in portrait-upside-down', () => {
52
- expect(bandThumbRotation(UPSIDE, false)).toBeUndefined();
53
- });
54
-
55
- it('rotates 90° CW for landscape-left', () => {
56
- expect(bandThumbRotation(LEFT, false)).toEqual([{ rotate: '90deg' }]);
57
- });
58
-
59
- it('rotates 90° CCW for landscape-right (opposite sign of left)', () => {
60
- expect(bandThumbRotation(RIGHT, false)).toEqual([{ rotate: '-90deg' }]);
61
- });
62
- });
63
-
64
- describe('vertical=true (non-locked, OS-rotated framebuffer)', () => {
65
- it('does not rotate in portrait', () => {
66
- expect(bandThumbRotation(PORTRAIT, true)).toBeUndefined();
67
- });
68
-
69
- it('uses the OPPOSITE sign from the portrait-locked case (left)', () => {
70
- // vertical=false → 90deg, so vertical=true → -90deg.
71
- expect(bandThumbRotation(LEFT, true)).toEqual([{ rotate: '-90deg' }]);
72
- expect(bandThumbRotation(LEFT, true)).not.toEqual(
73
- bandThumbRotation(LEFT, false),
74
- );
75
- });
76
-
77
- it('uses the OPPOSITE sign from the portrait-locked case (right)', () => {
78
- expect(bandThumbRotation(RIGHT, true)).toEqual([{ rotate: '90deg' }]);
79
- expect(bandThumbRotation(RIGHT, true)).not.toEqual(
80
- bandThumbRotation(RIGHT, false),
81
- );
82
- });
83
- });
84
- });
85
-
86
- describe('tileRotation — per-keyframe tiles (EXIF-6 source, the fix)', () => {
87
- describe('vertical=false (portrait-locked) — the regression case', () => {
88
- it.each<[BandCaptureOrientation]>([
89
- [PORTRAIT],
90
- [UPSIDE],
91
- [LEFT],
92
- [RIGHT],
93
- ])(
94
- 'applies NO transform for %s (EXIF already auto-rotates → no double-rotate)',
95
- (orientation) => {
96
- expect(tileRotation(orientation, false)).toBeUndefined();
97
- },
98
- );
99
-
100
- it('specifically does NOT rotate landscape tiles (the v0.12 bug)', () => {
101
- // Pre-fix this returned [{rotate:'90deg'}] / [{rotate:'-90deg'}]
102
- // on top of the EXIF auto-rotate → tiles 90° off. Must be undefined.
103
- expect(tileRotation(LEFT, false)).toBeUndefined();
104
- expect(tileRotation(RIGHT, false)).toBeUndefined();
105
- });
106
- });
107
-
108
- describe('vertical=true (non-locked landscape) — transform still needed', () => {
109
- it('matches bandThumbRotation in the vertical path', () => {
110
- // In the OS-rotated case the box is landscape JS coords, 90° off
111
- // the EXIF-upright tile, so the compensation IS required.
112
- expect(tileRotation(LEFT, true)).toEqual(bandThumbRotation(LEFT, true));
113
- expect(tileRotation(RIGHT, true)).toEqual(bandThumbRotation(RIGHT, true));
114
- });
115
-
116
- it('does not rotate in portrait even when vertical', () => {
117
- expect(tileRotation(PORTRAIT, true)).toBeUndefined();
118
- });
119
- });
120
- });