@momo-kits/camerakit 0.168.1-rn862.1 → 0.169.2
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/ios/ReactNativeCameraKit/CameraDirection.swift +143 -0
- package/ios/ReactNativeCameraKit/CameraView.swift +12 -3
- package/ios/ReactNativeCameraKit/RealCamera.swift +277 -30
- package/ios/ReactNativeCameraKit/RealPreviewView.swift +21 -0
- package/ios/ReactNativeCameraKit/ScannerInterfaceView.swift +82 -13
- package/package.json +8 -8
- package/src/index.ts +16 -24
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// File: packages/react-native-camera-kit/ios/ReactNativeCameraKit/CameraDirection.swift
|
|
2
|
+
// Created At: 2026-10-22 10:00:00 +07:00
|
|
3
|
+
// Created By: AI
|
|
4
|
+
// AI Agent: Claude Code
|
|
5
|
+
// Model: claude-opus-5-5
|
|
6
|
+
|
|
7
|
+
// AI-GENERATED START: keep the camera backward-facing and upright (iPhone Duo), ported from the host app
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import AVKit
|
|
10
|
+
import ImageIO
|
|
11
|
+
import UIKit
|
|
12
|
+
|
|
13
|
+
/// Keeps the back camera on a camera that points away from the person, on every iPhone.
|
|
14
|
+
///
|
|
15
|
+
/// On iPhone Duo the direction a camera faces depends on the display the app is on, and opening or
|
|
16
|
+
/// closing the device moves the app to the other display. `AVCaptureDeviceDirectionCoordinator`
|
|
17
|
+
/// reports which cameras face backward relative to the preview view: the observer keeps the current
|
|
18
|
+
/// camera while it still faces backward and otherwise hands back a replacement. On an iPhone with
|
|
19
|
+
/// one display the rear cameras always face backward, so the handler never asks for a change.
|
|
20
|
+
///
|
|
21
|
+
/// Kit-internal copy of the host's `ScannerCameraDirectionObserver` (ios/EvitanSwiftUI/Camera).
|
|
22
|
+
final class CameraDirectionObserver {
|
|
23
|
+
/// A replacement camera: its `uniqueID`, and whether it sits on the front (preview stays unmirrored).
|
|
24
|
+
typealias Replacement = (uniqueID: String, isFrontPosition: Bool)
|
|
25
|
+
|
|
26
|
+
private var coordinator: AnyObject?
|
|
27
|
+
|
|
28
|
+
/// Starts observing in relation to `previewView`; call on the main queue once the view is onscreen.
|
|
29
|
+
/// - Parameters:
|
|
30
|
+
/// - deviceTypes: the rear camera types the camera captures from, in order of preference.
|
|
31
|
+
/// - currentCameraID: the `uniqueID` of the camera in use.
|
|
32
|
+
/// - onReplace: called on the main queue when the current camera no longer faces backward.
|
|
33
|
+
func start(
|
|
34
|
+
previewView: UIView,
|
|
35
|
+
deviceTypes: [AVCaptureDevice.DeviceType],
|
|
36
|
+
currentCameraID: @escaping () -> String?,
|
|
37
|
+
onReplace: @escaping (Replacement) -> Void
|
|
38
|
+
) {
|
|
39
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
40
|
+
guard #available(iOS 27.1, *), coordinator == nil else { return }
|
|
41
|
+
// the two physical front cameras stand in for the virtual front camera, which the coordinator skips
|
|
42
|
+
let observedTypes = deviceTypes + [.builtInOuterUltraWideCamera, .builtInInnerUltraWideCamera]
|
|
43
|
+
coordinator = AVCaptureDeviceDirectionCoordinator(view: previewView, deviceTypes: observedTypes) { map in
|
|
44
|
+
let backward = map.backwardFacingDeviceDescriptors
|
|
45
|
+
if let current = currentCameraID(), backward.contains(where: { $0.uniqueID == current }) { return }
|
|
46
|
+
let rank = { (type: AVCaptureDevice.DeviceType) in
|
|
47
|
+
observedTypes.firstIndex(of: type) ?? observedTypes.count
|
|
48
|
+
}
|
|
49
|
+
// AVFoundation is not called here: the caller resolves the descriptor on its session queue
|
|
50
|
+
guard let replacement = backward.min(by: { rank($0.deviceType) < rank($1.deviceType) }) else { return }
|
|
51
|
+
onReplace((replacement.uniqueID, replacement.position == .front))
|
|
52
|
+
}
|
|
53
|
+
#endif
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func stop() {
|
|
57
|
+
coordinator = nil
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Rotation that keeps a camera's captured frames and preview level with the horizon.
|
|
62
|
+
///
|
|
63
|
+
/// Wraps `AVCaptureDevice.RotationCoordinator` (iOS 17+). It reads gravity and the display the app is
|
|
64
|
+
/// on, so the angle stays right on iPhone Duo where moving between displays changes it; deriving it
|
|
65
|
+
/// from `UIDevice.orientation` or the interface orientation does not. Create one per camera.
|
|
66
|
+
///
|
|
67
|
+
/// Kit-internal copy of the host's `CaptureRotationTracker` (ios/EvitanSwiftUI/Camera).
|
|
68
|
+
///
|
|
69
|
+
/// Preview and capture angles are observed separately. They change independently: on the iPhone Duo
|
|
70
|
+
/// inner display the interface ignores a portrait-only lock and lays out in landscape, which moves the
|
|
71
|
+
/// preview angle while the capture angle (gravity) stays the same.
|
|
72
|
+
final class CaptureRotationTracker {
|
|
73
|
+
private var coordinator: AnyObject?
|
|
74
|
+
private var captureObservation: NSKeyValueObservation?
|
|
75
|
+
private var previewObservation: NSKeyValueObservation?
|
|
76
|
+
private let lock = NSLock()
|
|
77
|
+
private var latestCaptureAngle: CGFloat?
|
|
78
|
+
|
|
79
|
+
/// The capture angle in degrees (0, 90, 180, 270); nil before iOS 17 or before the first reading.
|
|
80
|
+
/// Safe to read from the session and sample buffer queues.
|
|
81
|
+
var captureAngle: CGFloat? {
|
|
82
|
+
lock.lock()
|
|
83
|
+
defer { lock.unlock() }
|
|
84
|
+
return latestCaptureAngle
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The current preview angle in degrees; nil before iOS 17 or when not tracking. Main queue only.
|
|
88
|
+
/// Read it on layout too: the coordinator reports 0 while the layer is outside a view hierarchy.
|
|
89
|
+
var previewAngle: CGFloat? {
|
|
90
|
+
guard #available(iOS 17.0, *),
|
|
91
|
+
let rotationCoordinator = coordinator as? AVCaptureDevice.RotationCoordinator else { return nil }
|
|
92
|
+
return rotationCoordinator.videoRotationAngleForHorizonLevelPreview
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Starts tracking `device`; call on the main queue, once `previewLayer` is in a window.
|
|
96
|
+
/// `onPreviewAngle` receives the preview angle on the main queue.
|
|
97
|
+
func start(device: AVCaptureDevice, previewLayer: CALayer?, onPreviewAngle: ((CGFloat) -> Void)? = nil) {
|
|
98
|
+
guard #available(iOS 17.0, *) else { return }
|
|
99
|
+
stop()
|
|
100
|
+
let rotationCoordinator = AVCaptureDevice.RotationCoordinator(device: device, previewLayer: previewLayer)
|
|
101
|
+
coordinator = rotationCoordinator
|
|
102
|
+
store(rotationCoordinator.videoRotationAngleForHorizonLevelCapture)
|
|
103
|
+
onPreviewAngle?(rotationCoordinator.videoRotationAngleForHorizonLevelPreview)
|
|
104
|
+
let captureKeyPath = \AVCaptureDevice.RotationCoordinator.videoRotationAngleForHorizonLevelCapture
|
|
105
|
+
captureObservation = rotationCoordinator.observe(captureKeyPath) { [weak self] coordinator, _ in
|
|
106
|
+
self?.store(coordinator.videoRotationAngleForHorizonLevelCapture)
|
|
107
|
+
}
|
|
108
|
+
// the coordinator delivers KVO on the main queue; hop only if that ever changes
|
|
109
|
+
let previewKeyPath = \AVCaptureDevice.RotationCoordinator.videoRotationAngleForHorizonLevelPreview
|
|
110
|
+
previewObservation = rotationCoordinator.observe(previewKeyPath) { coordinator, _ in
|
|
111
|
+
let angle = coordinator.videoRotationAngleForHorizonLevelPreview
|
|
112
|
+
if Thread.isMainThread {
|
|
113
|
+
onPreviewAngle?(angle)
|
|
114
|
+
} else {
|
|
115
|
+
DispatchQueue.main.async { onPreviewAngle?(angle) }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func stop() {
|
|
121
|
+
captureObservation = nil
|
|
122
|
+
previewObservation = nil
|
|
123
|
+
coordinator = nil
|
|
124
|
+
store(nil)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// The Vision / `CGImage` orientation of a frame captured at `angle`.
|
|
128
|
+
static func imageOrientation(forCaptureAngle angle: CGFloat, isMirrored: Bool) -> CGImagePropertyOrientation {
|
|
129
|
+
switch (Int(angle.rounded()) % 360 + 360) % 360 {
|
|
130
|
+
case 90: return isMirrored ? .leftMirrored : .right
|
|
131
|
+
case 180: return isMirrored ? .downMirrored : .down
|
|
132
|
+
case 270: return isMirrored ? .rightMirrored : .left
|
|
133
|
+
default: return isMirrored ? .upMirrored : .up
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func store(_ angle: CGFloat?) {
|
|
138
|
+
lock.lock()
|
|
139
|
+
latestCaptureAngle = angle
|
|
140
|
+
lock.unlock()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// AI-GENERATED END: keep the camera backward-facing and upright (iPhone Duo), ported from the host app
|
|
@@ -132,6 +132,13 @@ public class CameraView: UIView {
|
|
|
132
132
|
|
|
133
133
|
focusInterfaceView.delegate = camera
|
|
134
134
|
|
|
135
|
+
// AI-GENERATED START: scan area follows the frame's layout (resize, rotation, iPhone Duo fold)
|
|
136
|
+
scannerInterfaceView.onFrameChange = { [weak self] frame in
|
|
137
|
+
guard let self else { return }
|
|
138
|
+
self.camera.update(scannerFrameSize: self.showFrame ? frame : nil)
|
|
139
|
+
}
|
|
140
|
+
// AI-GENERATED END: scan area follows the frame's layout (resize, rotation, iPhone Duo fold)
|
|
141
|
+
|
|
135
142
|
handleCameraPermission()
|
|
136
143
|
|
|
137
144
|
configureHardwareInteraction()
|
|
@@ -184,8 +191,9 @@ public class CameraView: UIView {
|
|
|
184
191
|
camera.previewView.frame = bounds
|
|
185
192
|
|
|
186
193
|
scannerInterfaceView.frame = bounds
|
|
187
|
-
//
|
|
188
|
-
|
|
194
|
+
// AI-GENERATED START: the scanner reports its frame from layoutSubviews (onFrameChange)
|
|
195
|
+
scannerInterfaceView.setNeedsLayout()
|
|
196
|
+
// AI-GENERATED END: the scanner reports its frame from layoutSubviews (onFrameChange)
|
|
189
197
|
|
|
190
198
|
focusInterfaceView.frame = bounds
|
|
191
199
|
|
|
@@ -274,8 +282,9 @@ public class CameraView: UIView {
|
|
|
274
282
|
|
|
275
283
|
if changedProps.contains("barcodeFrameSize"), let barcodeFrameSize, showFrame, scanBarcode {
|
|
276
284
|
if let width = barcodeFrameSize["width"] as? CGFloat, let height = barcodeFrameSize["height"] as? CGFloat {
|
|
285
|
+
// AI-GENERATED START: the new frame reaches the camera from layoutSubviews (onFrameChange)
|
|
277
286
|
scannerInterfaceView.update(frameSize: CGSize(width: width, height: height))
|
|
278
|
-
|
|
287
|
+
// AI-GENERATED END: the new frame reaches the camera from layoutSubviews (onFrameChange)
|
|
279
288
|
}
|
|
280
289
|
}
|
|
281
290
|
|
|
@@ -70,6 +70,27 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
70
70
|
|
|
71
71
|
private var zoomStartedAt: Double = 1.0
|
|
72
72
|
|
|
73
|
+
// AI-GENERATED START: backward-facing, upright camera when iPhone Duo folds or unfolds
|
|
74
|
+
private let directionObserver = CameraDirectionObserver()
|
|
75
|
+
private let rotationTracker = CaptureRotationTracker()
|
|
76
|
+
/// The camera type JS asked for (sessionQueue only). On iPhone Duo the "back" camera can be a
|
|
77
|
+
/// front-position device, so the input's `position` no longer tells which type is in use.
|
|
78
|
+
private var cameraType: CameraType?
|
|
79
|
+
/// Main queue only: whether the direction coordinator runs, and the camera it compares against.
|
|
80
|
+
private var followsBackwardDirection = false
|
|
81
|
+
private var activeDevice: AVCaptureDevice?
|
|
82
|
+
private var lastPreviewSize: CGSize = .zero
|
|
83
|
+
/// Rear camera types the back camera captures from, in order of preference.
|
|
84
|
+
private static let rearDeviceTypes: [AVCaptureDevice.DeviceType] = [
|
|
85
|
+
.builtInWideAngleCamera,
|
|
86
|
+
.builtInTelephotoCamera,
|
|
87
|
+
.builtInUltraWideCamera,
|
|
88
|
+
.builtInDualCamera,
|
|
89
|
+
.builtInDualWideCamera,
|
|
90
|
+
.builtInTripleCamera
|
|
91
|
+
]
|
|
92
|
+
// AI-GENERATED END: backward-facing, upright camera when iPhone Duo folds or unfolds
|
|
93
|
+
|
|
73
94
|
// MARK: - Lifecycle
|
|
74
95
|
|
|
75
96
|
override init() {
|
|
@@ -87,9 +108,45 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
87
108
|
// crash class is closed by construction, on every device.
|
|
88
109
|
cameraPreview.session = session
|
|
89
110
|
cameraPreview.previewLayer.videoGravity = .resizeAspectFill
|
|
111
|
+
|
|
112
|
+
// AI-GENERATED START: start the direction coordinator only once the preview is onscreen
|
|
113
|
+
cameraPreview.onWindowChange = { [weak self] window in
|
|
114
|
+
guard let self else { return }
|
|
115
|
+
if window == nil {
|
|
116
|
+
self.directionObserver.stop()
|
|
117
|
+
} else {
|
|
118
|
+
// a rotation coordinator made while the layer was off-window reports a 0° preview for good
|
|
119
|
+
if let activeDevice = self.activeDevice {
|
|
120
|
+
self.trackRotation(of: activeDevice)
|
|
121
|
+
}
|
|
122
|
+
self.startDirectionObserverIfNeeded()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// AI-GENERATED END: start the direction coordinator only once the preview is onscreen
|
|
126
|
+
|
|
127
|
+
// AI-GENERATED START: re-read the preview angle on layout (rotation, iPhone Duo fold/unfold, display switch)
|
|
128
|
+
cameraPreview.onLayout = { [weak self] in
|
|
129
|
+
guard let self else { return }
|
|
130
|
+
if let angle = self.rotationTracker.previewAngle {
|
|
131
|
+
self.applyPreviewAngle(angle)
|
|
132
|
+
}
|
|
133
|
+
// metadataOutputRectConverted also depends on the layer's size
|
|
134
|
+
let size = self.cameraPreview.bounds.size
|
|
135
|
+
if size != self.lastPreviewSize {
|
|
136
|
+
self.lastPreviewSize = size
|
|
137
|
+
self.refreshRectOfInterest()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// AI-GENERATED END: re-read the preview angle on layout (rotation, iPhone Duo fold/unfold, display switch)
|
|
90
141
|
}
|
|
91
142
|
|
|
92
143
|
func cameraRemovedFromSuperview() {
|
|
144
|
+
// AI-GENERATED START: backward-facing, upright camera when iPhone Duo folds or unfolds
|
|
145
|
+
directionObserver.stop()
|
|
146
|
+
rotationTracker.stop()
|
|
147
|
+
// AI-GENERATED END: backward-facing, upright camera when iPhone Duo folds or unfolds
|
|
148
|
+
|
|
149
|
+
|
|
93
150
|
sessionQueue.async { [weak self] in
|
|
94
151
|
guard let self, self.setupResult == .success else { return }
|
|
95
152
|
|
|
@@ -106,6 +163,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
106
163
|
|
|
107
164
|
#if !targetEnvironment(macCatalyst)
|
|
108
165
|
motionManager?.stopAccelerometerUpdates()
|
|
166
|
+
// AI-GENERATED START: display-relative gravity for onOrientationChange (iPhone Duo)
|
|
167
|
+
motionManager?.stopDeviceMotionUpdates()
|
|
168
|
+
// AI-GENERATED END: display-relative gravity for onOrientationChange (iPhone Duo)
|
|
109
169
|
|
|
110
170
|
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
|
|
111
171
|
|
|
@@ -160,10 +220,17 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
160
220
|
// pure connection-property write (no session config transaction → no race
|
|
161
221
|
// with startRunning). The connection exists because configureSession has
|
|
162
222
|
// committed the video input above.
|
|
223
|
+
// AI-GENERATED START: rotation coordinator angle, which follows the display iPhone Duo uses
|
|
224
|
+
let videoDevice = self.videoDeviceInput?.device
|
|
163
225
|
DispatchQueue.main.async { [weak self] in
|
|
164
226
|
guard let self else { return }
|
|
165
|
-
|
|
227
|
+
if #available(iOS 17.0, *), let videoDevice {
|
|
228
|
+
self.followDevice(videoDevice, cameraType: cameraType)
|
|
229
|
+
} else {
|
|
230
|
+
self.setVideoOrientationToInterfaceOrientation()
|
|
231
|
+
}
|
|
166
232
|
}
|
|
233
|
+
// AI-GENERATED END: rotation coordinator angle, which follows the display iPhone Duo uses
|
|
167
234
|
|
|
168
235
|
// Order matches the previous behaviour: start, then observe, then torch.
|
|
169
236
|
self.startSessionIfNeeded()
|
|
@@ -197,6 +264,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
197
264
|
}
|
|
198
265
|
session.addInput(videoDeviceInput)
|
|
199
266
|
self.videoDeviceInput = videoDeviceInput
|
|
267
|
+
// AI-GENERATED START: remember the requested type, the input's position can differ on iPhone Duo
|
|
268
|
+
self.cameraType = cameraType
|
|
269
|
+
// AI-GENERATED END: remember the requested type, the input's position can differ on iPhone Duo
|
|
200
270
|
self.resetZoom(forDevice: videoDevice)
|
|
201
271
|
|
|
202
272
|
// 2. Preset — set inside the transaction with the device input already
|
|
@@ -428,9 +498,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
428
498
|
func update(cameraType: CameraType) {
|
|
429
499
|
sessionQueue.async { [weak self] in
|
|
430
500
|
guard let self else { return }
|
|
431
|
-
|
|
501
|
+
// AI-GENERATED START: compare the requested type, the input's position can differ on iPhone Duo
|
|
502
|
+
if self.cameraType == cameraType {
|
|
432
503
|
return
|
|
433
504
|
}
|
|
505
|
+
// AI-GENERATED END: compare the requested type, the input's position can differ on iPhone Duo
|
|
434
506
|
|
|
435
507
|
// Avoid chaining device inputs when camera input is denied by the user, since both front and rear vido input devices will be nil
|
|
436
508
|
guard self.setupResult == .success,
|
|
@@ -442,27 +514,131 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
442
514
|
|
|
443
515
|
self.removeObservers()
|
|
444
516
|
self.beginConfiguration()
|
|
445
|
-
defer { self.commitConfiguration() }
|
|
446
517
|
|
|
447
518
|
// Remove the existing device input first, since using the front and back camera simultaneously is not supported.
|
|
448
519
|
self.session.removeInput(currentViewDeviceInput)
|
|
449
520
|
|
|
450
|
-
|
|
521
|
+
// AI-GENERATED START: track the requested type and restore mirroring after an iPhone Duo swap
|
|
522
|
+
let didSwitch = self.session.canAddInput(videoDeviceInput)
|
|
523
|
+
if didSwitch {
|
|
451
524
|
self.session.addInput(videoDeviceInput)
|
|
452
525
|
self.resetZoom(forDevice: videoDevice)
|
|
453
526
|
self.videoDeviceInput = videoDeviceInput
|
|
527
|
+
self.cameraType = cameraType
|
|
528
|
+
self.applyVideoMirroring(isFrontFacingBackward: false)
|
|
454
529
|
} else {
|
|
455
530
|
// If it fails, put back current camera
|
|
456
531
|
self.session.addInput(currentViewDeviceInput)
|
|
457
532
|
}
|
|
458
533
|
|
|
459
534
|
self.addObservers()
|
|
535
|
+
// committed before the main queue reads the new preview connection
|
|
536
|
+
self.commitConfiguration()
|
|
460
537
|
|
|
461
538
|
// We need to reapply the configuration after reloading the camera
|
|
462
539
|
self.update(torchMode: self.torchMode)
|
|
540
|
+
|
|
541
|
+
guard didSwitch else { return }
|
|
542
|
+
DispatchQueue.main.async { [weak self] in
|
|
543
|
+
self?.followDevice(videoDevice, cameraType: cameraType)
|
|
544
|
+
}
|
|
545
|
+
// AI-GENERATED END: track the requested type and restore mirroring after an iPhone Duo swap
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// AI-GENERATED START: backward-facing, upright camera when iPhone Duo folds or unfolds
|
|
550
|
+
/// Follows `device`'s rotation and, for the back camera, which way it faces; call on the main queue.
|
|
551
|
+
private func followDevice(_ device: AVCaptureDevice, cameraType: CameraType) {
|
|
552
|
+
activeDevice = device
|
|
553
|
+
followsBackwardDirection = cameraType == .back
|
|
554
|
+
trackRotation(of: device)
|
|
555
|
+
if followsBackwardDirection {
|
|
556
|
+
startDirectionObserverIfNeeded()
|
|
557
|
+
} else {
|
|
558
|
+
directionObserver.stop()
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// Starts following which cameras face backward relative to the preview; call on the main queue.
|
|
563
|
+
private func startDirectionObserverIfNeeded() {
|
|
564
|
+
guard followsBackwardDirection, cameraPreview.window != nil else { return }
|
|
565
|
+
directionObserver.start(
|
|
566
|
+
previewView: cameraPreview,
|
|
567
|
+
deviceTypes: Self.rearDeviceTypes,
|
|
568
|
+
currentCameraID: { [weak self] in self?.activeDevice?.uniqueID },
|
|
569
|
+
onReplace: { [weak self] replacement in self?.switchCamera(to: replacement) }
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// Swaps the video input for a camera that faces backward again after iPhone Duo folded or unfolded.
|
|
574
|
+
private func switchCamera(to replacement: CameraDirectionObserver.Replacement) {
|
|
575
|
+
sessionQueue.async { [weak self] in
|
|
576
|
+
guard let self, self.setupResult == .success, self.cameraType == .back,
|
|
577
|
+
let currentInput = self.videoDeviceInput,
|
|
578
|
+
currentInput.device.uniqueID != replacement.uniqueID,
|
|
579
|
+
let device = AVCaptureDevice(uniqueID: replacement.uniqueID),
|
|
580
|
+
let newInput = try? AVCaptureDeviceInput(device: device) else {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
self.removeObservers()
|
|
585
|
+
self.beginConfiguration()
|
|
586
|
+
self.session.removeInput(currentInput)
|
|
587
|
+
// decide from a local flag, not by re-reading the shared input
|
|
588
|
+
let didSwitch = self.session.canAddInput(newInput)
|
|
589
|
+
if didSwitch {
|
|
590
|
+
self.session.addInput(newInput)
|
|
591
|
+
self.videoDeviceInput = newInput
|
|
592
|
+
// a front camera facing backward is shown and captured unmirrored, like a rear camera
|
|
593
|
+
self.applyVideoMirroring(isFrontFacingBackward: replacement.isFrontPosition)
|
|
594
|
+
} else if self.session.canAddInput(currentInput) {
|
|
595
|
+
self.session.addInput(currentInput)
|
|
596
|
+
}
|
|
597
|
+
self.addObservers()
|
|
598
|
+
self.commitConfiguration()
|
|
599
|
+
|
|
600
|
+
self.update(torchMode: self.torchMode)
|
|
601
|
+
guard didSwitch else { return }
|
|
602
|
+
self.resetZoom(forDevice: device)
|
|
603
|
+
DispatchQueue.main.async { [weak self] in
|
|
604
|
+
self?.followDevice(device, cameraType: .back)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// Follows the capture and preview angles of `device`; call on the main queue.
|
|
610
|
+
private func trackRotation(of device: AVCaptureDevice) {
|
|
611
|
+
rotationTracker.start(device: device, previewLayer: cameraPreview.previewLayer) { [weak self] angle in
|
|
612
|
+
self?.applyPreviewAngle(angle)
|
|
463
613
|
}
|
|
464
614
|
}
|
|
465
615
|
|
|
616
|
+
/// Rotates the preview connection to `angle` when it differs; call on the main queue.
|
|
617
|
+
private func applyPreviewAngle(_ angle: CGFloat) {
|
|
618
|
+
guard #available(iOS 17.0, *),
|
|
619
|
+
let connection = cameraPreview.previewLayer.connection,
|
|
620
|
+
connection.videoRotationAngle != angle,
|
|
621
|
+
connection.isVideoRotationAngleSupported(angle) else {
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
connection.videoRotationAngle = angle
|
|
625
|
+
// metadataOutputRectConverted depends on the preview connection's angle
|
|
626
|
+
refreshRectOfInterest()
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/// A front camera facing backward (iPhone Duo) stays unmirrored; every other camera gets the system
|
|
630
|
+
/// default back. MUST run on `sessionQueue`, inside a configuration transaction.
|
|
631
|
+
private func applyVideoMirroring(isFrontFacingBackward: Bool) {
|
|
632
|
+
assertOnSessionQueue()
|
|
633
|
+
session.connections.filter(\.isVideoMirroringSupported).forEach { connection in
|
|
634
|
+
connection.automaticallyAdjustsVideoMirroring = !isFrontFacingBackward
|
|
635
|
+
if isFrontFacingBackward {
|
|
636
|
+
connection.isVideoMirrored = false
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// AI-GENERATED END: backward-facing, upright camera when iPhone Duo folds or unfolds
|
|
641
|
+
|
|
466
642
|
func update(resizeMode: ResizeMode) {
|
|
467
643
|
DispatchQueue.main.async {
|
|
468
644
|
switch resizeMode {
|
|
@@ -524,10 +700,17 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
524
700
|
return
|
|
525
701
|
}
|
|
526
702
|
|
|
527
|
-
//
|
|
528
|
-
|
|
703
|
+
// AI-GENERATED START: rotation coordinator angle, which follows the display iPhone Duo uses
|
|
704
|
+
// Gravity alone can't tell which display iPhone Duo uses, so iOS 17+ takes the rotation
|
|
705
|
+
// coordinator's horizon-level angle; the accelerometer value only covers older systems.
|
|
706
|
+
if #available(iOS 17.0, *), let captureAngle = self.rotationTracker.captureAngle {
|
|
707
|
+
if photoOutputConnection.isVideoRotationAngleSupported(captureAngle) {
|
|
708
|
+
photoOutputConnection.videoRotationAngle = captureAngle
|
|
709
|
+
}
|
|
710
|
+
} else if let videoPreviewLayerOrientation {
|
|
529
711
|
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
|
530
712
|
}
|
|
713
|
+
// AI-GENERATED END: rotation coordinator angle, which follows the display iPhone Duo uses
|
|
531
714
|
|
|
532
715
|
// Pause OCR + barcode work on the session queue, right before capture.
|
|
533
716
|
self.pauseNonEssentialOutputs()
|
|
@@ -594,28 +777,49 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
594
777
|
guard self.scannerFrameSize != scannerFrameSize else { return }
|
|
595
778
|
self.sessionQueue.async {
|
|
596
779
|
self.scannerFrameSize = scannerFrameSize
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
780
|
+
// AI-GENERATED START: recompute the scan area when the frame or the preview angle changes
|
|
781
|
+
self.applyRectOfInterest()
|
|
782
|
+
}
|
|
783
|
+
}
|
|
600
784
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
785
|
+
/// Re-derives `rectOfInterest` from the scanner frame. Call whenever the frame or the preview
|
|
786
|
+
/// connection's angle changes: `metadataOutputRectConverted` depends on both.
|
|
787
|
+
private func refreshRectOfInterest() {
|
|
788
|
+
sessionQueue.async { [weak self] in
|
|
789
|
+
self?.applyRectOfInterest()
|
|
790
|
+
}
|
|
791
|
+
}
|
|
606
792
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
793
|
+
/// MUST run on `sessionQueue`.
|
|
794
|
+
private func applyRectOfInterest() {
|
|
795
|
+
assertOnSessionQueue()
|
|
796
|
+
if !session.isRunning {
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
let scannerFrameSize = self.scannerFrameSize
|
|
611
800
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
801
|
+
// metadataOutputRectConverted must be read on the main thread.
|
|
802
|
+
DispatchQueue.main.async {
|
|
803
|
+
var visibleRect: CGRect?
|
|
804
|
+
if let scannerFrameSize, scannerFrameSize != .zero {
|
|
805
|
+
visibleRect = self.cameraPreview.previewLayer
|
|
806
|
+
.metadataOutputRectConverted(fromLayerRect: scannerFrameSize)
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
self.sessionQueue.async {
|
|
810
|
+
// a photo capture pauses scanning; resumeNonEssentialOutputs restores the rect afterwards
|
|
811
|
+
guard self.inProgressPhotoCaptureDelegates.isEmpty,
|
|
812
|
+
self.metadataOutput.rectOfInterest != visibleRect else {
|
|
813
|
+
return
|
|
615
814
|
}
|
|
815
|
+
|
|
816
|
+
self.metadataOutput.rectOfInterest = visibleRect ?? CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
817
|
+
// We need to reapply the configuration after touching the metadataOutput
|
|
818
|
+
self.update(torchMode: self.torchMode)
|
|
616
819
|
}
|
|
617
820
|
}
|
|
618
821
|
}
|
|
822
|
+
// AI-GENERATED END: recompute the scan area when the frame or the preview angle changes
|
|
619
823
|
|
|
620
824
|
|
|
621
825
|
func isTextDetectionEnabled(_ isEnabled: Bool, onTextRead: ((String) -> Void)?) {
|
|
@@ -651,11 +855,19 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
651
855
|
if now.timeIntervalSince(lastTextProcess) < textThrottle { return }
|
|
652
856
|
lastTextProcess = now
|
|
653
857
|
|
|
858
|
+
// AI-GENERATED START: rotation coordinator angle, so OCR reads text in sideways or upside-down frames
|
|
859
|
+
let orientation = rotationTracker.captureAngle.map {
|
|
860
|
+
CaptureRotationTracker.imageOrientation(forCaptureAngle: $0, isMirrored: connection.isVideoMirrored)
|
|
861
|
+
} ?? .up
|
|
862
|
+
// AI-GENERATED END: rotation coordinator angle, so OCR reads text in sideways or upside-down frames
|
|
863
|
+
|
|
654
864
|
globalOCRQueue.async {
|
|
655
865
|
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
|
|
656
866
|
var requestOptions: [VNImageOption: Any] = [:]
|
|
657
867
|
|
|
658
|
-
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
|
|
868
|
+
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
|
|
869
|
+
orientation: orientation,
|
|
870
|
+
options: requestOptions)
|
|
659
871
|
do {
|
|
660
872
|
try handler.perform([request])
|
|
661
873
|
} catch {
|
|
@@ -713,6 +925,16 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
713
925
|
}
|
|
714
926
|
|
|
715
927
|
private func getBestDevice(for cameraType: CameraType) -> AVCaptureDevice? {
|
|
928
|
+
// AI-GENERATED START: same rear camera set the host scanner uses; the wide angle stays first
|
|
929
|
+
if cameraType == .back,
|
|
930
|
+
let device = AVCaptureDevice.DiscoverySession(
|
|
931
|
+
deviceTypes: Self.rearDeviceTypes,
|
|
932
|
+
mediaType: .video,
|
|
933
|
+
position: .back
|
|
934
|
+
).devices.first {
|
|
935
|
+
return device
|
|
936
|
+
}
|
|
937
|
+
// AI-GENERATED END: same rear camera set the host scanner uses; the wide angle stays first
|
|
716
938
|
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraType.avPosition) {
|
|
717
939
|
return device // single-lens/physical device
|
|
718
940
|
}
|
|
@@ -780,6 +1002,22 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
780
1002
|
motionManager?.accelerometerUpdateInterval = 0.2
|
|
781
1003
|
motionManager?.gyroUpdateInterval = 0.2
|
|
782
1004
|
|
|
1005
|
+
// AI-GENERATED START: display-relative gravity for onOrientationChange (iPhone Duo)
|
|
1006
|
+
// The accelerometer reports in the hardware frame, a quarter turn off from the UI on the iPhone Duo
|
|
1007
|
+
// inner display. Device motion referenced to the preview view arrives corrected for that display.
|
|
1008
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
1009
|
+
if #available(iOS 27.0, *), let motionManager, motionManager.isDeviceMotionAvailable {
|
|
1010
|
+
motionManager.deviceMotionUpdateInterval = 0.2
|
|
1011
|
+
motionManager.deviceMotionBody = cameraPreview
|
|
1012
|
+
motionManager.startDeviceMotionUpdates(to: OperationQueue()) { [weak self] motion, error in
|
|
1013
|
+
guard error == nil, let gravity = motion?.gravity else { return }
|
|
1014
|
+
self?.reportOrientation(from: gravity)
|
|
1015
|
+
}
|
|
1016
|
+
return
|
|
1017
|
+
}
|
|
1018
|
+
#endif
|
|
1019
|
+
// AI-GENERATED END: display-relative gravity for onOrientationChange (iPhone Duo)
|
|
1020
|
+
|
|
783
1021
|
motionManager?.startAccelerometerUpdates(to: OperationQueue(), withHandler: { [weak self] (accelerometerData, error) -> Void in
|
|
784
1022
|
guard let self else { return }
|
|
785
1023
|
guard error == nil else {
|
|
@@ -791,18 +1029,25 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
791
1029
|
return
|
|
792
1030
|
}
|
|
793
1031
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
let orientation = Orientation(from: newOrientation) else {
|
|
797
|
-
return
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
self.deviceOrientation = newOrientation
|
|
801
|
-
self.onOrientationChange?(["orientation": orientation.rawValue])
|
|
1032
|
+
// AI-GENERATED START: shared with the display-relative device motion path
|
|
1033
|
+
self.reportOrientation(from: accelerometerData.acceleration)
|
|
802
1034
|
})
|
|
803
1035
|
#endif
|
|
804
1036
|
}
|
|
805
1037
|
|
|
1038
|
+
/// Sends `onOrientationChange` when gravity points to a new orientation.
|
|
1039
|
+
private func reportOrientation(from acceleration: CMAcceleration) {
|
|
1040
|
+
guard let newOrientation = deviceOrientation(from: acceleration),
|
|
1041
|
+
newOrientation != deviceOrientation,
|
|
1042
|
+
let orientation = Orientation(from: newOrientation) else {
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
deviceOrientation = newOrientation
|
|
1047
|
+
onOrientationChange?(["orientation": orientation.rawValue])
|
|
1048
|
+
}
|
|
1049
|
+
// AI-GENERATED END: shared with the display-relative device motion path
|
|
1050
|
+
|
|
806
1051
|
private func deviceOrientation(from acceleration: CMAcceleration) -> UIDeviceOrientation? {
|
|
807
1052
|
let threshold = 0.75
|
|
808
1053
|
if acceleration.x >= threshold {
|
|
@@ -866,6 +1111,8 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
866
1111
|
resetFocus?()
|
|
867
1112
|
}
|
|
868
1113
|
|
|
1114
|
+
// AI-GENERATED START: legacy preview orientation, iOS 17+ uses the rotation coordinator (trackRotation)
|
|
1115
|
+
// AI-GENERATED END: legacy preview orientation, iOS 17+ uses the rotation coordinator (trackRotation)
|
|
869
1116
|
private func setVideoOrientationToInterfaceOrientation() {
|
|
870
1117
|
#if !targetEnvironment(macCatalyst)
|
|
871
1118
|
var interfaceOrientation: UIInterfaceOrientation
|
|
@@ -23,4 +23,25 @@ class RealPreviewView: UIView {
|
|
|
23
23
|
get { previewLayer.session }
|
|
24
24
|
set { previewLayer.session = newValue }
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
// AI-GENERATED START: report when the preview is onscreen, for the camera direction coordinator
|
|
28
|
+
/// Called on the main queue with the new window, or nil once the preview leaves it.
|
|
29
|
+
/// `CameraDirectionObserver` needs an onscreen preview view.
|
|
30
|
+
var onWindowChange: ((UIWindow?) -> Void)?
|
|
31
|
+
|
|
32
|
+
override func didMoveToWindow() {
|
|
33
|
+
super.didMoveToWindow()
|
|
34
|
+
onWindowChange?(window)
|
|
35
|
+
}
|
|
36
|
+
// AI-GENERATED END: report when the preview is onscreen, for the camera direction coordinator
|
|
37
|
+
|
|
38
|
+
// AI-GENERATED START: re-read the preview angle on layout (rotation, iPhone Duo fold/unfold, display switch)
|
|
39
|
+
/// Called on the main queue after every layout pass.
|
|
40
|
+
var onLayout: (() -> Void)?
|
|
41
|
+
|
|
42
|
+
override func layoutSubviews() {
|
|
43
|
+
super.layoutSubviews()
|
|
44
|
+
onLayout?()
|
|
45
|
+
}
|
|
46
|
+
// AI-GENERATED END: re-read the preview angle on layout (rotation, iPhone Duo fold/unfold, display switch)
|
|
26
47
|
}
|
|
@@ -45,14 +45,28 @@ class ScannerInterfaceView: UIView {
|
|
|
45
45
|
fatalError("init(coder:) has not been implemented")
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
// AI-GENERATED START: lay the frame out in the area clear of the iPhone Duo fold and camera punchout
|
|
49
|
+
/// Called on the main queue with the frame's rect, in this view's coordinates, after every layout.
|
|
50
|
+
var onFrameChange: ((CGRect) -> Void)?
|
|
51
|
+
|
|
52
|
+
// UIKit re-runs layout when the reserved regions change, so they are re-read here without polling.
|
|
53
|
+
override func layoutSubviews() {
|
|
54
|
+
super.layoutSubviews()
|
|
55
|
+
|
|
56
|
+
let area = ScannerReservedRegions.availableArea(in: self)
|
|
57
|
+
let size = CGSize(width: min(barcodeFrameWidth, area.width), height: min(barcodeFrameHeight, area.height))
|
|
58
|
+
let previousSize = frameView.bounds.size
|
|
59
|
+
frameView.frame = CGRect(origin: .zero, size: size)
|
|
60
|
+
frameView.center = CGPoint(x: area.midX, y: area.midY)
|
|
61
|
+
if previousSize != size {
|
|
62
|
+
// the corners and laser are laid out in ScannerFrameView.draw(_:)
|
|
63
|
+
frameView.setNeedsDisplay()
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
updateOverlaySize(frameView.frame)
|
|
67
|
+
onFrameChange?(frameView.frame)
|
|
55
68
|
}
|
|
69
|
+
// AI-GENERATED END: lay the frame out in the area clear of the iPhone Duo fold and camera punchout
|
|
56
70
|
|
|
57
71
|
// MARK: - Public
|
|
58
72
|
|
|
@@ -79,20 +93,75 @@ class ScannerInterfaceView: UIView {
|
|
|
79
93
|
func update(frameSize: CGSize) {
|
|
80
94
|
barcodeFrameWidth = frameSize.width
|
|
81
95
|
barcodeFrameHeight = frameSize.height
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
// AI-GENERATED START: the frame is laid out in layoutSubviews, not draw(_:)
|
|
97
|
+
setNeedsLayout()
|
|
98
|
+
// AI-GENERATED END: the frame is laid out in layoutSubviews, not draw(_:)
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
// MARK: - Private
|
|
87
102
|
|
|
103
|
+
// AI-GENERATED START: dim around a frame that is no longer always centered
|
|
88
104
|
private func updateOverlaySize(_ frameRect: CGRect) {
|
|
89
|
-
topOverlayView.frame = CGRect(x: 0, y: 0, width:
|
|
90
|
-
leftOverlayView.frame = CGRect(x: 0, y: frameRect.
|
|
91
|
-
rightOverlayView.frame = CGRect(
|
|
105
|
+
topOverlayView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: frameRect.minY)
|
|
106
|
+
leftOverlayView.frame = CGRect(x: 0, y: frameRect.minY, width: frameRect.minX, height: frameRect.height)
|
|
107
|
+
rightOverlayView.frame = CGRect(
|
|
108
|
+
x: frameRect.maxX,
|
|
109
|
+
y: frameRect.minY,
|
|
110
|
+
width: bounds.width - frameRect.maxX,
|
|
111
|
+
height: frameRect.height)
|
|
92
112
|
bottomOverlayView.frame = CGRect(
|
|
93
113
|
x: 0,
|
|
94
|
-
y: frameRect.
|
|
95
|
-
width:
|
|
96
|
-
height:
|
|
114
|
+
y: frameRect.maxY,
|
|
115
|
+
width: bounds.width,
|
|
116
|
+
height: bounds.height - frameRect.maxY)
|
|
117
|
+
}
|
|
118
|
+
// AI-GENERATED END: dim around a frame that is no longer always centered
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// AI-GENERATED START: keep the scanner frame clear of iPhone Duo reserved regions
|
|
122
|
+
/// The part of a scanner view the frame, laser and dimmed overlay are laid out in (iOS 27.1+, iPhone Duo):
|
|
123
|
+
/// - `.division`: while partially folded, the frame moves to the trailing (vertical fold) or bottom
|
|
124
|
+
/// (horizontal fold) part, like the host's `FoldPlacement`.
|
|
125
|
+
/// - `.occlusion`: a camera punchout the safe area leaves inside the area pushes only the nearest edge,
|
|
126
|
+
/// like the host's `QRScanReservedRegions`.
|
|
127
|
+
/// The whole view on every other device, and compiled out on SDKs older than iOS 27.1.
|
|
128
|
+
enum ScannerReservedRegions {
|
|
129
|
+
static func availableArea(in view: UIView) -> CGRect {
|
|
130
|
+
var area = view.bounds
|
|
131
|
+
#if canImport(UIKit, _version: 9127.0.85.28)
|
|
132
|
+
if #available(iOS 27.1, *) {
|
|
133
|
+
let folds = view.reservedRegions(kind: .division).filter(\.isActive).map(\.frame)
|
|
134
|
+
if let fold = folds.dropFirst().reduce(folds.first, { $0?.union($1) }) {
|
|
135
|
+
area = placement(avoiding: fold, in: area)
|
|
136
|
+
}
|
|
137
|
+
for region in view.reservedRegions(kind: .occlusion) where region.isActive {
|
|
138
|
+
area = clearing(region.frame, from: area)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
#endif
|
|
142
|
+
return area
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// The trailing part of a vertical fold, or the bottom part of a horizontal one.
|
|
146
|
+
static func placement(avoiding fold: CGRect, in area: CGRect) -> CGRect {
|
|
147
|
+
guard area.width > 0, area.height > 0 else { return area }
|
|
148
|
+
if fold.height > fold.width {
|
|
149
|
+
return area.divided(atDistance: min(max(fold.maxX - area.minX, 0), area.width), from: .minXEdge).remainder
|
|
150
|
+
}
|
|
151
|
+
return area.divided(atDistance: min(max(fold.maxY - area.minY, 0), area.height), from: .minYEdge).remainder
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Moves the one edge of `area` that clears `occlusion` with the least content lost.
|
|
155
|
+
static func clearing(_ occlusion: CGRect, from area: CGRect) -> CGRect {
|
|
156
|
+
guard area.width > 0, area.height > 0, occlusion.intersects(area) else { return area }
|
|
157
|
+
let candidates: [(cost: CGFloat, edge: CGRectEdge)] = [
|
|
158
|
+
(occlusion.maxY - area.minY, .minYEdge),
|
|
159
|
+
(area.maxY - occlusion.minY, .maxYEdge),
|
|
160
|
+
(occlusion.maxX - area.minX, .minXEdge),
|
|
161
|
+
(area.maxX - occlusion.minX, .maxXEdge),
|
|
162
|
+
]
|
|
163
|
+
guard let cut = candidates.min(by: { $0.cost < $1.cost }) else { return area }
|
|
164
|
+
return area.divided(atDistance: cut.cost, from: cut.edge).remainder
|
|
97
165
|
}
|
|
98
166
|
}
|
|
167
|
+
// AI-GENERATED END: keep the scanner frame clear of iPhone Duo reserved regions
|
package/package.json
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momo-kits/camerakit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.169.2",
|
|
6
4
|
"repository": {
|
|
7
5
|
"type": "git",
|
|
8
6
|
"url": "https://github.com/teslamotors/react-native-camera-kit.git"
|
|
9
7
|
},
|
|
10
|
-
"main": "./src/index.ts",
|
|
11
8
|
"publishConfig": {
|
|
12
9
|
"registry": "https://registry.npmjs.org/"
|
|
13
10
|
},
|
|
11
|
+
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
|
|
12
|
+
"nativePackage": true,
|
|
14
13
|
"scripts": {
|
|
15
14
|
"build": "echo",
|
|
16
15
|
"test": "jest",
|
|
17
16
|
"lint": "yarn eslint -c .eslintrc.js"
|
|
18
17
|
},
|
|
18
|
+
"main": "./src/index.ts",
|
|
19
19
|
"dependencies": {},
|
|
20
|
+
"license": "MIT",
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"react": "19.
|
|
22
|
-
"react-native": "0.
|
|
22
|
+
"react": "19.0.0",
|
|
23
|
+
"react-native": "0.80.1"
|
|
23
24
|
},
|
|
24
25
|
"peerDependencies": {
|
|
25
26
|
"react": "*",
|
|
@@ -28,7 +29,6 @@
|
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=18"
|
|
30
31
|
},
|
|
31
|
-
"packageManager": "yarn@1.22.22",
|
|
32
32
|
"codegenConfig": {
|
|
33
33
|
"name": "rncamerakit_specs",
|
|
34
34
|
"type": "all",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"javaPackageName": "com.rncamerakit"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"
|
|
40
|
+
"packageManager": "yarn@1.22.22"
|
|
41
41
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
import CameraKit from
|
|
1
|
+
import CameraKit from './Camera';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from
|
|
3
|
+
CameraType,
|
|
4
|
+
type CameraApi,
|
|
5
|
+
type CaptureData,
|
|
6
|
+
type FlashMode,
|
|
7
|
+
type FocusMode,
|
|
8
|
+
type TorchMode,
|
|
9
|
+
type ZoomMode,
|
|
10
|
+
type ResizeMode,
|
|
11
|
+
} from './types';
|
|
12
12
|
|
|
13
13
|
// Start with portrait/pointing up, increment while moving counter-clockwise
|
|
14
14
|
export const Orientation = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
PORTRAIT: 0, // ⬆️
|
|
16
|
+
LANDSCAPE_LEFT: 1, // ⬅️
|
|
17
|
+
PORTRAIT_UPSIDE_DOWN: 2, // ⬇️
|
|
18
|
+
LANDSCAPE_RIGHT: 3, // ➡️
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
CaptureData,
|
|
24
|
-
FlashMode,
|
|
25
|
-
FocusMode,
|
|
26
|
-
ResizeMode,
|
|
27
|
-
TorchMode,
|
|
28
|
-
ZoomMode,
|
|
29
|
-
};
|
|
30
|
-
export { CameraKit, CameraType };
|
|
21
|
+
export { CameraKit };
|
|
22
|
+
export type { CameraType, TorchMode, FlashMode, FocusMode, ZoomMode, CameraApi, CaptureData, ResizeMode };
|