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.
- package/CHANGELOG.md +124 -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 +35 -16
- 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 +48 -16
- 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/cpp/tests/pose_test.cpp
DELETED
|
@@ -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
|
-
});
|