replate-camera 0.4.0 → 0.5.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.
@@ -1255,49 +1255,59 @@ class ReplateCameraController: NSObject {
1255
1255
  return context.createCGImage(ciImage, from: ciImage.extent)
1256
1256
  }
1257
1257
 
1258
- func saveImageAsJPEG(_ image: UIImage) -> URL? {
1259
- let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
1260
-
1261
- guard let imageData = image.jpegData(compressionQuality: 1),
1262
- let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
1263
- return nil
1264
- }
1265
-
1266
- let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
1267
- let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
1268
- let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
1269
-
1270
- guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
1271
- return nil
1272
- }
1273
-
1274
- var mutableMetadata = imageProperties
1275
- mutableMetadata[kCGImagePropertyExifDictionary] = [
1276
- kCGImagePropertyExifUserComment: cameraTransform
1277
- ]
1278
-
1279
- guard let destination = CGImageDestinationCreateWithURL(
1280
- fileURL as CFURL,
1281
- kUTTypeJPEG,
1282
- 1,
1283
- nil
1284
- ) else {
1285
- return nil
1286
- }
1287
-
1288
- CGImageDestinationAddImageFromSource(
1289
- destination,
1290
- source,
1291
- 0,
1292
- mutableMetadata as CFDictionary
1293
- )
1294
-
1295
- guard CGImageDestinationFinalize(destination) else {
1296
- return nil
1297
- }
1298
-
1299
- return fileURL
1258
+ func saveImageAsJPEG(_ image: UIImage) -> URL? {
1259
+ let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
1260
+ let gravityVector = getGravityVectorString(from: ReplateCameraView.arView.session)
1261
+
1262
+ guard let imageData = image.jpegData(compressionQuality: 1),
1263
+ let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
1264
+ return nil
1265
+ }
1266
+
1267
+ let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
1268
+ let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
1269
+ let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
1270
+
1271
+ guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
1272
+ return nil
1273
+ }
1274
+
1275
+ var mutableMetadata = imageProperties
1276
+ mutableMetadata[kCGImagePropertyExifDictionary] = [
1277
+ kCGImagePropertyExifUserComment: "\(cameraTransform ?? "")|\(gravityVector)"
1278
+ ]
1279
+
1280
+ guard let destination = CGImageDestinationCreateWithURL(
1281
+ fileURL as CFURL,
1282
+ kUTTypeJPEG,
1283
+ 1,
1284
+ nil
1285
+ ) else {
1286
+ return nil
1287
+ }
1288
+
1289
+ CGImageDestinationAddImageFromSource(
1290
+ destination,
1291
+ source,
1292
+ 0,
1293
+ mutableMetadata as CFDictionary
1294
+ )
1295
+
1296
+ guard CGImageDestinationFinalize(destination) else {
1297
+ return nil
1300
1298
  }
1299
+
1300
+ return fileURL
1301
+ }
1302
+
1303
+ func getGravityVectorString(from session: ARSession) -> String? {
1304
+ guard let currentFrame = session.currentFrame else {
1305
+ return nil
1306
+ }
1307
+
1308
+ let gravityVector = currentFrame.camera.eulerAngles // gravity vector is in Euler angles (x, y, z)
1309
+ return "\(gravityVector.x),\(gravityVector.y),\(gravityVector.z)"
1310
+ }
1301
1311
 
1302
1312
  func getCameraTransformString(from session: ARSession) -> String? {
1303
1313
  guard let currentFrame = session.currentFrame else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replate-camera",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Camera component for Replate Manager",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -1,296 +0,0 @@
1
- private func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
2
- guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
3
-
4
- scaleFilter.setValue(image, forKey: kCIInputImageKey)
5
- scaleFilter.setValue(targetSize.width / image.extent.width, forKey: kCIInputScaleKey)
6
- scaleFilter.setValue(1.0, forKey: kCIInputAspectRatioKey)
7
-
8
- return scaleFilter.outputImage
9
- }
10
-
11
- // MARK: - Entity Management
12
- private func setOpacityToCircle(circleId: Int, opacity: Float) {
13
- DispatchQueue.main.async {
14
- for i in 0..<72 {
15
- let offset = circleId == 0 ? 0 : 72
16
- guard i + offset < ReplateCameraView.spheresModels.count else { continue }
17
-
18
- let entity = ReplateCameraView.spheresModels[i + offset]
19
- self.updateEntityMaterial(entity, opacity: opacity)
20
- }
21
- }
22
- }
23
-
24
- private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
25
- guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
26
-
27
- if #available(iOS 15.0, *) {
28
- let newMaterial = SimpleMaterial(
29
- color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
30
- roughness: 1,
31
- isMetallic: false
32
- )
33
- entity.model?.materials[0] = newMaterial
34
- }
35
- }
36
-
37
- private func updateSpheres(deviceTargetInfo: DeviceTargetInfo, cameraTransform: simd_float4x4, completion: @escaping (Bool) -> Void) {
38
- var completionCalled = false
39
-
40
- func safeCompletion(_ result: Bool) {
41
- guard !completionCalled else {
42
- print("Completion already called")
43
- return
44
- }
45
- completionCalled = true
46
- completion(result)
47
- }
48
-
49
- // Validate spheres initialization
50
- guard ReplateCameraView.spheresModels.count >= 144 else {
51
- print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
52
- safeCompletion(false)
53
- return
54
- }
55
-
56
- // Get anchor
57
- guard let anchorNode = ReplateCameraView.anchorEntity else {
58
- print("[updateSpheres] No anchor entity found.")
59
- safeCompletion(false)
60
- return
61
- }
62
-
63
- // Calculate camera metrics
64
- let cameraDistance = isCameraWithinRange(
65
- cameraTransform: cameraTransform,
66
- anchorEntity: anchorNode
67
- )
68
-
69
- // Calculate sphere index
70
- let angleDegrees = Self.angleBetweenAnchorXAndCamera(
71
- anchor: anchorNode,
72
- cameraTransform: cameraTransform
73
- )
74
- let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72
75
-
76
- DispatchQueue.main.async {
77
- self.processSphereUpdate(
78
- sphereIndex: sphereIndex,
79
- targetIndex: deviceTargetInfo.targetIndex,
80
- completion: safeCompletion
81
- )
82
- }
83
- }
84
-
85
- private func processSphereUpdate(sphereIndex: Int, targetIndex: Int, completion: @escaping (Bool) -> Void) {
86
- var mesh: ModelEntity?
87
- var newAngle = false
88
- var callback: RCTResponseSenderBlock?
89
-
90
- if targetIndex == 1 {
91
- guard sphereIndex < ReplateCameraView.upperSpheresSet.count else {
92
- print("[updateSpheres] Sphere index out of bounds")
93
- completion(false)
94
- return
95
- }
96
-
97
- if !ReplateCameraView.upperSpheresSet[sphereIndex] {
98
- ReplateCameraView.upperSpheresSet[sphereIndex] = true
99
- ReplateCameraView.photosFromDifferentAnglesTaken += 1
100
- newAngle = true
101
-
102
- guard 72 + sphereIndex < ReplateCameraView.spheresModels.count else {
103
- print("[updateSpheres] Upper spheresModels index out of range")
104
- completion(false)
105
- return
106
- }
107
-
108
- mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
109
-
110
- if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
111
- callback = ReplateCameraController.completedUpperSpheresCallback
112
- ReplateCameraController.completedUpperSpheresCallback = nil
113
- }
114
- }
115
- } else if targetIndex == 0 {
116
- guard sphereIndex < ReplateCameraView.lowerSpheresSet.count else {
117
- print("[updateSpheres] Lower sphere index out of range")
118
- completion(false)
119
- return
120
- }
121
-
122
- if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
123
- ReplateCameraView.lowerSpheresSet[sphereIndex] = true
124
- ReplateCameraView.photosFromDifferentAnglesTaken += 1
125
- newAngle = true
126
-
127
- guard sphereIndex < ReplateCameraView.spheresModels.count else {
128
- print("[updateSpheres] Lower spheresModels index out of range")
129
- completion(false)
130
- return
131
- }
132
-
133
- mesh = ReplateCameraView.spheresModels[sphereIndex]
134
-
135
- if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
136
- callback = ReplateCameraController.completedLowerSpheresCallback
137
- ReplateCameraController.completedLowerSpheresCallback = nil
138
- }
139
- }
140
- }
141
-
142
- if let mesh = mesh {
143
- let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
144
- mesh.model?.materials[0] = material
145
- ReplateCameraView.generateImpactFeedback(strength: .light)
146
- }
147
-
148
- callback?([])
149
- completion(newAngle)
150
- }
151
-
152
- // MARK: - Anchor Validation
153
- private func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
154
- var isValid = false
155
- DispatchQueue.main.sync {
156
- let transform = anchorNode.transform
157
- let position = transform.translation
158
- let rotation = transform.rotation
159
- let scale = transform.scale
160
-
161
- isValid = !position.isNaN &&
162
- !rotation.isNaN &&
163
- scale != SIMD3<Float>(0, 0, 0) &&
164
- abs(length(rotation.vector) - 1.0) < 0.0001
165
- }
166
- return isValid
167
- }
168
-
169
- // MARK: - Distance and Angle Calculations
170
- private func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity) -> Int {
171
- var distance: Float = 0
172
-
173
- DispatchQueue.main.sync {
174
- let cameraPosition = SIMD3<Float>(
175
- cameraTransform.columns.3.x,
176
- cameraTransform.columns.3.y,
177
- cameraTransform.columns.3.z
178
- )
179
- let anchorPosition = anchorEntity.transform.translation
180
- distance = distanceBetween(cameraPosition, anchorPosition)
181
- }
182
-
183
- return distance <= Self.MIN_DISTANCE ? -1 :
184
- distance >= Self.MAX_DISTANCE ? 1 : 0
185
- }
186
-
187
- private func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
188
- let difference = pos1 - pos2
189
- return sqrt(dot(difference, difference))
190
- }
191
-
192
- // MARK: - Static Utility Methods
193
- static func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4 {
194
- var relativeTransform: simd_float4x4!
195
- DispatchQueue.main.sync {
196
- let anchorTransform = anchor.transformMatrix(relativeTo: nil)
197
- relativeTransform = anchorTransform.inverse * cameraTransform
198
- }
199
- return relativeTransform
200
- }
201
-
202
- static func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
203
- var angleDegrees: Float = 0
204
-
205
- DispatchQueue.main.sync {
206
- let anchorTransform = anchor.transform.matrix
207
- let anchorPositionXZ = simd_float2(
208
- anchor.transform.translation.x,
209
- anchor.transform.translation.z
210
- )
211
- let relativeCameraPositionXZ = simd_float2(
212
- cameraTransform.columns.3.x,
213
- cameraTransform.columns.3.z
214
- )
215
-
216
- let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
217
- let anchorXAxisXZ = simd_float2(
218
- anchorTransform.columns.0.x,
219
- anchorTransform.columns.0.z
220
- )
221
-
222
- let angle = atan2(directionXZ.y, directionXZ.x) -
223
- atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
224
-
225
- angleDegrees = angle * (180.0 / .pi)
226
- if angleDegrees < 0 {
227
- angleDegrees += 360
228
- }
229
- }
230
-
231
- return angleDegrees
232
- }
233
-
234
- static func cgImage(from ciImage: CIImage) -> CGImage? {
235
- let context = CIContext(options: nil)
236
- return context.createCGImage(ciImage, from: ciImage.extent)
237
- }
238
-
239
- static func saveImageAsJPEG(_ image: UIImage) -> URL? {
240
- let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
241
-
242
- guard let imageData = image.jpegData(compressionQuality: 1),
243
- let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
244
- return nil
245
- }
246
-
247
- let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
248
- let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
249
- let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
250
-
251
- guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
252
- return nil
253
- }
254
-
255
- var mutableMetadata = imageProperties
256
- mutableMetadata[kCGImagePropertyExifDictionary] = [
257
- kCGImagePropertyExifUserComment: cameraTransform
258
- ]
259
-
260
- guard let destination = CGImageDestinationCreateWithURL(
261
- fileURL as CFURL,
262
- kUTTypeJPEG,
263
- 1,
264
- nil
265
- ) else {
266
- return nil
267
- }
268
-
269
- CGImageDestinationAddImageFromSource(
270
- destination,
271
- source,
272
- 0,
273
- mutableMetadata as CFDictionary
274
- )
275
-
276
- guard CGImageDestinationFinalize(destination) else {
277
- return nil
278
- }
279
-
280
- return fileURL
281
- }
282
-
283
- static func getCameraTransformString(from session: ARSession) -> String? {
284
- guard let currentFrame = session.currentFrame else {
285
- return nil
286
- }
287
-
288
- let transform = currentFrame.camera.transform
289
- return """
290
- \(transform.columns.0.x),\(transform.columns.0.y),\(transform.columns.0.z),\(transform.columns.0.w);
291
- \(transform.columns.1.x),\(transform.columns.1.y),\(transform.columns.1.z),\(transform.columns.1.w);
292
- \(transform.columns.2.x),\(transform.columns.2.y),\(transform.columns.2.z),\(transform.columns.2.w);
293
- \(transform.columns.3.x),\(transform.columns.3.y),\(transform.columns.3.z),\(transform.columns.3.w)
294
- """
295
- }
296
- }
@@ -1,363 +0,0 @@
1
- import ARKit
2
- import RealityKit
3
- import UIKit
4
- import AVFoundation
5
- import ImageIO
6
- import MobileCoreServices
7
-
8
- guard !hasCalledBack else {
9
- // MARK: - Supporting Types
10
- enum ARError: Error {
11
- case noAnchor
12
- case invalidAnchor
13
- case notInFocus
14
- case captureError
15
- case tooManyImages
16
- case processingError
17
- case savingError
18
- case transformError
19
- case lightingError
20
- case unknown
21
-
22
- var localizedDescription: String {
23
- switch self {
24
- case .noAnchor: return "[ReplateCameraController] No anchor set yet"
25
- case .invalidAnchor: return "[ReplateCameraController] AnchorNode is not valid"
26
- case .notInFocus: return "[ReplateCameraController] Object not in focus"
27
- case .captureError: return "[ReplateCameraController] Error capturing image"
28
- case .tooManyImages: return "[ReplateCameraController] Too many images and the last one's not from a new angle"
29
- case .processingError: return "[ReplateCameraController] Error processing image"
30
- case .savingError: return "[ReplateCameraController] Error saving photo"
31
- case .transformError: return "[ReplateCameraController] Camera transform data not available"
32
- case .lightingError: return "[ReplateCameraController] Image too dark"
33
- case .unknown: return "[ReplateCameraController] Unknown error occurred"
34
- }
35
- }
36
- }
37
-
38
- struct DeviceTargetInfo {
39
- let isInFocus: Bool
40
- let targetIndex: Int
41
- let transform: simd_float4x4
42
- let cameraPosition: SIMD3<Float>
43
- let deviceDirection: SIMD3<Float>
44
-
45
- var isValidTarget: Bool {
46
- return targetIndex != -1
47
- }
48
- }
49
-
50
- class SafeCallbackHandler {
51
- private let resolver: RCTPromiseResolveBlock
52
- private let rejecter: RCTPromiseRejectBlock
53
- private var hasCalledBack = false
54
- private let lock = NSLock()
55
-
56
- init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
57
- self.resolver = resolver
58
- self.rejecter = rejecter
59
- }
60
-
61
- func resolve(_ result: Any) {
62
- lock.lock()
63
- defer { lock.unlock() }
64
-
65
- print("resolver: Callback already invoked.")
66
- return
67
- }
68
- hasCalledBack = true
69
- resolver(result)
70
- }
71
-
72
- func reject(_ error: ARError) {
73
- lock.lock()
74
- defer { lock.unlock() }
75
-
76
- guard !hasCalledBack else {
77
- print("rejecter: Callback already invoked.")
78
- return
79
- }
80
- hasCalledBack = true
81
- rejecter(
82
- String(describing: error),
83
- error.localizedDescription,
84
- NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
85
- )
86
- }
87
- }
88
-
89
- // MARK: - Main Controller
90
- @objc(ReplateCameraController)
91
- class ReplateCameraController: NSObject {
92
- // MARK: - Static Properties
93
- private static let lock = NSLock()
94
- private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
95
-
96
- // Configuration Constants
97
- private static let MIN_DISTANCE: Float = 0.15
98
- private static let MAX_DISTANCE: Float = 0.45
99
- private static let ANGLE_THRESHOLD: Float = 0.6
100
- private static let TARGET_IMAGE_SIZE = CGSize(width: 1728, height: 1296)
101
- private static let MIN_AMBIENT_INTENSITY: CGFloat = 650
102
-
103
- // Callbacks
104
- static var completedTutorialCallback: RCTResponseSenderBlock?
105
- static var anchorSetCallback: RCTResponseSenderBlock?
106
- static var completedUpperSpheresCallback: RCTResponseSenderBlock?
107
- static var completedLowerSpheresCallback: RCTResponseSenderBlock?
108
- static var openedTutorialCallback: RCTResponseSenderBlock?
109
- static var tooCloseCallback: RCTResponseSenderBlock?
110
- static var tooFarCallback: RCTResponseSenderBlock?
111
-
112
- // MARK: - Callback Registration Methods
113
- @objc(registerOpenedTutorialCallback:)
114
- func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
115
- Self.lock.lock()
116
- defer { Self.lock.unlock() }
117
- ReplateCameraController.openedTutorialCallback = callback
118
- }
119
-
120
- @objc(registerCompletedTutorialCallback:)
121
- func registerCompletedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
122
- Self.lock.lock()
123
- defer { Self.lock.unlock() }
124
- ReplateCameraController.completedTutorialCallback = callback
125
- }
126
-
127
- @objc(registerAnchorSetCallback:)
128
- func registerAnchorSetCallback(_ callback: @escaping RCTResponseSenderBlock) {
129
- Self.lock.lock()
130
- defer { Self.lock.unlock() }
131
- ReplateCameraController.anchorSetCallback = callback
132
- }
133
-
134
- @objc(registerCompletedUpperSpheresCallback:)
135
- func registerCompletedUpperSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
136
- Self.lock.lock()
137
- defer { Self.lock.unlock() }
138
- ReplateCameraController.completedUpperSpheresCallback = callback
139
- }
140
-
141
- @objc(registerCompletedLowerSpheresCallback:)
142
- func registerCompletedLowerSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
143
- Self.lock.lock()
144
- defer { Self.lock.unlock() }
145
- ReplateCameraController.completedLowerSpheresCallback = callback
146
- }
147
-
148
- @objc(registerTooCloseCallback:)
149
- func registerTooCloseCallback(_ callback: @escaping RCTResponseSenderBlock) {
150
- Self.lock.lock()
151
- defer { Self.lock.unlock() }
152
- ReplateCameraController.tooCloseCallback = callback
153
- }
154
-
155
- @objc(registerTooFarCallback:)
156
- func registerTooFarCallback(_ callback: @escaping RCTResponseSenderBlock) {
157
- Self.lock.lock()
158
- defer { Self.lock.unlock() }
159
- ReplateCameraController.tooFarCallback = callback
160
- }
161
-
162
- // MARK: - Public Methods
163
- @objc(getPhotosCount:rejecter:)
164
- func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
165
- Self.lock.lock()
166
- let count = ReplateCameraView.totalPhotosTaken
167
- Self.lock.unlock()
168
- resolver(count)
169
- }
170
-
171
- @objc(isScanComplete:rejecter:)
172
- func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
173
- Self.lock.lock()
174
- let isComplete = ReplateCameraView.photosFromDifferentAnglesTaken == 144
175
- Self.lock.unlock()
176
- resolver(isComplete)
177
- }
178
-
179
- @objc(getRemainingAnglesToScan:rejecter:)
180
- func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
181
- Self.lock.lock()
182
- let remaining = 144 - ReplateCameraView.photosFromDifferentAnglesTaken
183
- Self.lock.unlock()
184
- resolver(remaining)
185
- }
186
-
187
- @objc
188
- func reset() {
189
- DispatchQueue.main.async {
190
- ReplateCameraView.INSTANCE.reset()
191
- }
192
- }
193
-
194
- // MARK: - Photo Capture and Processing
195
- @objc(takePhoto:resolver:rejecter:)
196
- func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
197
- Self.arQueue.async { [weak self] in
198
- guard let self = self else { return }
199
-
200
- let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
201
-
202
- do {
203
- try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
204
- } catch let error as ARError {
205
- callbackHandler.reject(error)
206
- } catch {
207
- callbackHandler.reject(.unknown)
208
- }
209
- }
210
- }
211
-
212
- private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
213
- let anchorEntity = try getValidAnchorEntity()
214
- let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
215
-
216
- if deviceTargetInfo.isValidTarget {
217
- try processTargetedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
218
- } else {
219
- throw ARError.notInFocus
220
- }
221
- }
222
-
223
- private func getValidAnchorEntity() throws -> AnchorEntity {
224
- var anchorEntity: AnchorEntity?
225
-
226
- DispatchQueue.main.sync {
227
- anchorEntity = ReplateCameraView.anchorEntity
228
- }
229
-
230
- guard let anchor = anchorEntity else {
231
- throw ARError.noAnchor
232
- }
233
-
234
- guard isAnchorNodeValid(anchor) else {
235
- throw ARError.invalidAnchor
236
- }
237
-
238
- return anchor
239
- }
240
-
241
- private func getDeviceTargetInfo(anchorEntity: AnchorEntity) throws -> DeviceTargetInfo {
242
- guard let frame = ReplateCameraView.arView.session.currentFrame else {
243
- throw ARError.transformError
244
- }
245
-
246
- let cameraTransform = frame.camera.transform
247
- let relativeCameraTransform = Self.getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
248
-
249
- var anchorPosition: SIMD3<Float>!
250
- DispatchQueue.main.sync {
251
- anchorPosition = anchorEntity.position(relativeTo: nil)
252
- }
253
-
254
- let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
255
- let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
256
- let directionToAnchor = normalize(anchorPosition - cameraPosition)
257
- let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
258
-
259
- let targetIndex = determineTargetIndex(angleToAnchor: angleToAnchor, relativeCameraTransform: relativeCameraTransform)
260
-
261
- return DeviceTargetInfo(
262
- isInFocus: angleToAnchor < Self.ANGLE_THRESHOLD,
263
- targetIndex: targetIndex,
264
- transform: relativeCameraTransform,
265
- cameraPosition: cameraPosition,
266
- deviceDirection: deviceDirection
267
- )
268
- }
269
-
270
- private func determineTargetIndex(angleToAnchor: Float, relativeCameraTransform: simd_float4x4) -> Int {
271
- guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
272
-
273
- let spheresHeight = ReplateCameraView.spheresHeight
274
- let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
275
- let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
276
- let cameraHeight = relativeCameraTransform.columns.3.y
277
-
278
- return cameraHeight < twoThirdsDistance ? 0 : 1
279
- }
280
-
281
- private func processTargetedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
282
- DispatchQueue.main.async { [weak self] in
283
- guard let self = self else { return }
284
-
285
- self.updateCircleFocus(targetIndex: deviceTargetInfo.targetIndex)
286
- self.checkCameraDistance(deviceTargetInfo: deviceTargetInfo)
287
-
288
- self.updateSpheres(
289
- deviceTargetInfo: deviceTargetInfo,
290
- cameraTransform: deviceTargetInfo.transform
291
- ) { success in
292
- if !unlimited && !success {
293
- callbackHandler.reject(.tooManyImages)
294
- return
295
- }
296
-
297
- self.captureAndProcessImage(callbackHandler: callbackHandler)
298
- }
299
- }
300
- }
301
-
302
- private func updateCircleFocus(targetIndex: Int) {
303
- if targetIndex != ReplateCameraView.circleInFocus {
304
- setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
305
- setOpacityToCircle(circleId: targetIndex, opacity: 1)
306
- ReplateCameraView.circleInFocus = targetIndex
307
- ReplateCameraView.generateImpactFeedback(strength: .heavy)
308
- }
309
- }
310
-
311
- private func checkCameraDistance(deviceTargetInfo: DeviceTargetInfo) {
312
- let distance = isCameraWithinRange(
313
- cameraTransform: deviceTargetInfo.transform,
314
- anchorEntity: ReplateCameraView.anchorEntity!
315
- )
316
-
317
- switch distance {
318
- case 1:
319
- ReplateCameraController.tooFarCallback?([])
320
- case -1:
321
- ReplateCameraController.tooCloseCallback?([])
322
- default:
323
- break
324
- }
325
- }
326
-
327
- // MARK: - Image Processing
328
- private func captureAndProcessImage(callbackHandler: SafeCallbackHandler) {
329
- guard let frame = ReplateCameraView.arView?.session.currentFrame else {
330
- callbackHandler.reject(.captureError)
331
- return
332
- }
333
-
334
- // Check lighting conditions
335
- if let lightEstimate = frame.lightEstimate {
336
- guard lightEstimate.ambientIntensity >= Self.MIN_AMBIENT_INTENSITY else {
337
- callbackHandler.reject(.lightingError)
338
- return
339
- }
340
- }
341
-
342
- processAndSaveImage(frame.capturedImage, callbackHandler: callbackHandler)
343
- }
344
-
345
- private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
346
- let ciImage = CIImage(cvImageBuffer: pixelBuffer)
347
-
348
- guard let resizedImage = resizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
349
- let cgImage = Self.cgImage(from: resizedImage),
350
- let rotatedImage = UIImage(cgImage: cgImage).rotate(radians: .pi / 2),
351
- let savedURL = Self.saveImageAsJPEG(rotatedImage) else {
352
- callbackHandler.reject(.processingError)
353
- return
354
- }
355
-
356
- callbackHandler.resolve(savedURL.absoluteString)
357
- }
358
-
359
- private func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
360
- guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
361
-
362
- scaleFilter.setValue(image, forKey: kCIInputImageKey)
363
- scaleFilter.setValue(targetSize.
@@ -1,213 +0,0 @@
1
- @objc(ReplateCameraController)
2
- class ReplateCameraController: NSObject {
3
- // MARK: - Static Properties
4
- private static let lock = NSLock()
5
- private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
6
-
7
- // Callback Properties
8
- static var completedTutorialCallback: RCTResponseSenderBlock?
9
- static var anchorSetCallback: RCTResponseSenderBlock?
10
- static var completedUpperSpheresCallback: RCTResponseSenderBlock?
11
- static var completedLowerSpheresCallback: RCTResponseSenderBlock?
12
- static var openedTutorialCallback: RCTResponseSenderBlock?
13
- static var tooCloseCallback: RCTResponseSenderBlock?
14
- static var tooFarCallback: RCTResponseSenderBlock?
15
-
16
- // Constants
17
- private static let MIN_DISTANCE: Float = 0.15
18
- private static let MAX_DISTANCE: Float = 0.45
19
- private static let ANGLE_THRESHOLD: Float = 0.6
20
-
21
- // MARK: - Callback Registration
22
- @objc(registerOpenedTutorialCallback:)
23
- func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
24
- Self.lock.lock()
25
- defer { Self.lock.unlock() }
26
- ReplateCameraController.openedTutorialCallback = callback
27
- }
28
-
29
- // Similar registrations for other callbacks...
30
-
31
- // MARK: - Public Methods
32
- @objc(getPhotosCount:rejecter:)
33
- func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
34
- Self.lock.lock()
35
- let count = ReplateCameraView.totalPhotosTaken
36
- Self.lock.unlock()
37
- resolver(count)
38
- }
39
-
40
- @objc(takePhoto:resolver:rejecter:)
41
- func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
42
- Self.arQueue.async { [weak self] in
43
- guard let self = self else { return }
44
-
45
- let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
46
-
47
- do {
48
- try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
49
- } catch let error as ARError {
50
- callbackHandler.reject(error)
51
- } catch {
52
- callbackHandler.reject(.unknown)
53
- }
54
- }
55
- }
56
-
57
- // MARK: - Private Methods
58
- private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
59
- guard let anchorEntity = getValidAnchorEntity() else {
60
- throw ARError.noAnchor
61
- }
62
-
63
- let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
64
-
65
- if deviceTargetInfo.isInFocus {
66
- handleFocusedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
67
- } else {
68
- throw ARError.notInFocus
69
- }
70
- }
71
-
72
- private func getValidAnchorEntity() -> AnchorEntity? {
73
- var anchorEntity: AnchorEntity?
74
- DispatchQueue.main.sync {
75
- anchorEntity = ReplateCameraView.anchorEntity
76
- if let anchor = anchorEntity {
77
- guard isAnchorNodeValid(anchor) else {
78
- return nil
79
- }
80
- }
81
- }
82
- return anchorEntity
83
- }
84
-
85
- private func handleFocusedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) {
86
- DispatchQueue.main.async {
87
- self.updateCircleFocus(deviceTargetInfo: deviceTargetInfo)
88
-
89
- self.updateSpheres(deviceTargetInfo: deviceTargetInfo) { success in
90
- if !unlimited && !success {
91
- callbackHandler.reject(.tooManyImages)
92
- return
93
- }
94
-
95
- self.captureAndProcessImage(callbackHandler: callbackHandler)
96
- }
97
- }
98
- }
99
-
100
- private func captureAndProcessImage(callbackHandler: SafeCallbackHandler) {
101
- guard let image = ReplateCameraView.arView?.session.currentFrame?.capturedImage else {
102
- callbackHandler.reject(.captureError)
103
- return
104
- }
105
-
106
- processAndSaveImage(image, callbackHandler: callbackHandler)
107
- }
108
-
109
- private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
110
- // Image processing implementation...
111
- }
112
-
113
- // MARK: - Entity Management
114
- private func setOpacityToCircle(circleId: Int, opacity: Float) {
115
- DispatchQueue.main.async {
116
- for i in 0..<72 {
117
- let offset = circleId == 0 ? 0 : 72
118
- guard let entity = ReplateCameraView.spheresModels[safe: i + offset] else { continue }
119
-
120
- self.updateEntityMaterial(entity, opacity: opacity)
121
- }
122
- }
123
- }
124
-
125
- private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
126
- guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
127
-
128
- if #available(iOS 15.0, *) {
129
- let newMaterial = SimpleMaterial(
130
- color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
131
- roughness: 1,
132
- isMetallic: false
133
- )
134
- entity.model?.materials[0] = newMaterial
135
- }
136
- }
137
-
138
- // MARK: - Utility Methods
139
- private func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
140
- var isValid = false
141
- DispatchQueue.main.sync {
142
- let transform = anchorNode.transform
143
- let position = transform.translation
144
- let rotation = transform.rotation
145
- let scale = transform.scale
146
-
147
- isValid = !position.isNaN &&
148
- !rotation.isNaN &&
149
- scale != SIMD3<Float>(0, 0, 0) &&
150
- abs(length(rotation.vector) - 1.0) < 0.0001
151
- }
152
- return isValid
153
- }
154
- }
155
-
156
- // MARK: - Supporting Types
157
- extension ReplateCameraController {
158
- struct DeviceTargetInfo {
159
- let isInFocus: Bool
160
- let targetIndex: Int
161
- let transform: simd_float4x4
162
- }
163
-
164
- enum ARError: Error {
165
- case noAnchor
166
- case invalidAnchor
167
- case notInFocus
168
- case captureError
169
- case tooManyImages
170
- case unknown
171
- }
172
-
173
- class SafeCallbackHandler {
174
- private let resolver: RCTPromiseResolveBlock
175
- private let rejecter: RCTPromiseRejectBlock
176
- private var hasCalledBack = false
177
- private let lock = NSLock()
178
-
179
- init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
180
- self.resolver = resolver
181
- self.rejecter = rejecter
182
- }
183
-
184
- func resolve(_ result: Any) {
185
- lock.lock()
186
- defer { lock.unlock() }
187
-
188
- guard !hasCalledBack else { return }
189
- hasCalledBack = true
190
- resolver(result)
191
- }
192
-
193
- func reject(_ error: ARError) {
194
- lock.lock()
195
- defer { lock.unlock() }
196
-
197
- guard !hasCalledBack else { return }
198
- hasCalledBack = true
199
- rejecter(
200
- String(describing: error),
201
- error.localizedDescription,
202
- NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
203
- )
204
- }
205
- }
206
- }
207
-
208
- // MARK: - Array Extension
209
- extension Array {
210
- subscript(safe index: Index) -> Element? {
211
- indices.contains(index) ? self[index] : nil
212
- }
213
- }
@@ -1,264 +0,0 @@
1
- import ARKit
2
- import RealityKit
3
- import UIKit
4
- import AVFoundation
5
- import ImageIO
6
- import MobileCoreServices
7
-
8
- // MARK: - RCTViewManager
9
- @objc(ReplateCameraViewManager)
10
- class ReplateCameraViewManager: RCTViewManager {
11
- override func view() -> ReplateCameraView {
12
- return ReplateCameraView()
13
- }
14
-
15
- @objc override static func requiresMainQueueSetup() -> Bool {
16
- return false
17
- }
18
- }
19
-
20
- // MARK: - UIImage Extension
21
- extension UIImage {
22
- func rotate(radians: Float) -> UIImage {
23
- var newSize = CGRect(origin: .zero, size: self.size)
24
- .applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
25
- newSize.width = floor(newSize.width)
26
- newSize.height = floor(newSize.height)
27
-
28
- UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
29
- guard let context = UIGraphicsGetCurrentContext() else { return self }
30
-
31
- context.translateBy(x: newSize.width / 2, y: newSize.height / 2)
32
- context.rotate(by: CGFloat(radians))
33
- self.draw(in: CGRect(x: -self.size.width / 2,
34
- y: -self.size.height / 2,
35
- width: self.size.width,
36
- height: self.size.height))
37
-
38
- let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
39
- UIGraphicsEndImageContext()
40
-
41
- return newImage
42
- }
43
- }
44
-
45
- // MARK: - ReplateCameraView
46
- class ReplateCameraView: UIView, ARSessionDelegate {
47
- // MARK: - Static Properties
48
- private static let lock = NSLock()
49
- private static let arQueue = DispatchQueue(label: "com.replate.ar", qos: .userInteractive)
50
-
51
- // AR View and Core Components
52
- static var arView: ARView!
53
- static var anchorEntity: AnchorEntity?
54
- static var model: Entity!
55
- static var sessionId: UUID!
56
- static var INSTANCE: ReplateCameraView!
57
-
58
- // Scene Configuration
59
- static var width = CGFloat(0)
60
- static var height = CGFloat(0)
61
- static var isPaused = false
62
-
63
- // Sphere Properties
64
- static var spheresModels: [ModelEntity] = []
65
- static var upperSpheresSet = [Bool](repeating: false, count: 72)
66
- static var lowerSpheresSet = [Bool](repeating: false, count: 72)
67
- static var sphereRadius = Float(0.004)
68
- static var spheresRadius = Float(0.13)
69
- static var sphereAngle = Float(5)
70
- static var spheresHeight = Float(0.10)
71
- static var distanceBetweenCircles = Float(0.10)
72
-
73
- // Focus and Navigation
74
- static var focusModel: Entity!
75
- static var circleInFocus = 0
76
- static var dragSpeed = CGFloat(7000)
77
- static var dotAnchors: [AnchorEntity] = []
78
-
79
- // Statistics
80
- static var totalPhotosTaken = 0
81
- static var photosFromDifferentAnglesTaken = 0
82
-
83
- // Thread Safety
84
- private static var isResetting = false
85
-
86
- // MARK: - Initialization
87
- override init(frame: CGRect) {
88
- super.init(frame: frame)
89
- setupInitialState()
90
- }
91
-
92
- required init?(coder: NSCoder) {
93
- super.init(coder: coder)
94
- setupInitialState()
95
- }
96
-
97
- private func setupInitialState() {
98
- requestCameraPermissions()
99
- ReplateCameraView.INSTANCE = self
100
- }
101
-
102
- // MARK: - Layout
103
- override func layoutSubviews() {
104
- super.layoutSubviews()
105
- Self.lock.lock()
106
- defer { Self.lock.unlock() }
107
-
108
- ReplateCameraView.width = frame.width
109
- ReplateCameraView.height = frame.height
110
- }
111
-
112
- // MARK: - AR Setup and Configuration
113
- static func setupAR() {
114
- arQueue.async {
115
- guard let instance = INSTANCE else { return }
116
-
117
- let configuration = ARWorldTrackingConfiguration()
118
- configuration.isLightEstimationEnabled = true
119
- configuration.planeDetection = .horizontal
120
-
121
- DispatchQueue.main.async {
122
- configureARView(configuration)
123
- addRecognizers()
124
- }
125
- }
126
- }
127
-
128
- private static func configureARView(_ configuration: ARWorldTrackingConfiguration) {
129
- configureRenderOptions()
130
- configureVideoFormat(configuration)
131
-
132
- arView.session.run(configuration)
133
- arView.addCoaching()
134
- sessionId = arView.session.identifier
135
- }
136
-
137
- private static func configureRenderOptions() {
138
- let renderOptions: [ARView.RenderOptions] = [
139
- .disableMotionBlur,
140
- .disableCameraGrain,
141
- .disableAREnvironmentLighting,
142
- .disableHDR,
143
- .disableFaceMesh,
144
- .disableGroundingShadows,
145
- .disablePersonOcclusion
146
- ]
147
-
148
- renderOptions.forEach { arView.renderOptions.insert($0) }
149
- }
150
-
151
- private static func configureVideoFormat(_ configuration: ARWorldTrackingConfiguration) {
152
- if #available(iOS 16.0, *) {
153
- configuration.videoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing
154
- ?? ARWorldTrackingConfiguration.recommendedVideoFormatFor4KResolution
155
- ?? highestResolutionFormat()
156
- } else {
157
- configuration.videoFormat = highestResolutionFormat()
158
- }
159
- }
160
-
161
- private static func highestResolutionFormat() -> ARConfiguration.VideoFormat {
162
- return ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
163
- let resolution1 = format1.imageResolution.width * format1.imageResolution.height
164
- let resolution2 = format2.imageResolution.width * format2.imageResolution.height
165
- return resolution1 < resolution2
166
- })!
167
- }
168
-
169
- // MARK: - Gesture Recognition
170
- static func addRecognizers() {
171
- guard let instance = INSTANCE else { return }
172
-
173
- let recognizers = [
174
- UITapGestureRecognizer(target: instance, action: #selector(instance.viewTapped)),
175
- UIPanGestureRecognizer(target: instance, action: #selector(instance.handlePan)),
176
- UIPinchGestureRecognizer(target: instance, action: #selector(instance.handlePinch))
177
- ]
178
-
179
- recognizers.forEach { arView.addGestureRecognizer($0) }
180
- }
181
-
182
- // MARK: - Entity Management
183
- func createSpheres(y: Float) {
184
- DispatchQueue.main.async {
185
- let radius = ReplateCameraView.spheresRadius
186
-
187
- for i in 0..<72 {
188
- let angle = Float(i) * (Float.pi / 180) * 5
189
- let position = SIMD3(
190
- radius * cos(angle),
191
- y,
192
- radius * sin(angle)
193
- )
194
-
195
- let sphere = self.createSphere(position: position)
196
- ReplateCameraView.spheresModels.append(sphere)
197
- ReplateCameraView.anchorEntity?.addChild(sphere)
198
- }
199
- }
200
- }
201
-
202
- func createSphere(position: SIMD3<Float>) -> ModelEntity {
203
- let sphereMesh = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius)
204
- let material = SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)
205
- let sphere = ModelEntity(mesh: sphereMesh, materials: [material])
206
- sphere.position = position
207
- return sphere
208
- }
209
-
210
- // MARK: - Reset Functionality
211
- @objc static func reset() {
212
- arQueue.async {
213
- Self.lock.lock()
214
- defer { Self.lock.unlock() }
215
-
216
- if isResetting { return }
217
- isResetting = true
218
-
219
- DispatchQueue.main.async {
220
- tearDownARSession()
221
- resetProperties()
222
- setupNewARView()
223
- isResetting = false
224
- }
225
- }
226
- }
227
-
228
- private static func tearDownARSession() {
229
- arView?.session.pause()
230
- arView?.session.delegate = nil
231
- arView?.scene.anchors.removeAll()
232
- arView?.removeFromSuperview()
233
- arView?.window?.resignKey()
234
- }
235
-
236
- private static func resetProperties() {
237
- anchorEntity = nil
238
- model = nil
239
- spheresModels.removeAll()
240
- upperSpheresSet = [Bool](repeating: false, count: 72)
241
- lowerSpheresSet = [Bool](repeating: false, count: 72)
242
- totalPhotosTaken = 0
243
- photosFromDifferentAnglesTaken = 0
244
- sphereRadius = 0.004
245
- spheresRadius = 0.13
246
- sphereAngle = 5
247
- spheresHeight = 0.10
248
- dragSpeed = 7000
249
- dotAnchors.removeAll()
250
- }
251
-
252
- private static func setupNewARView() {
253
- guard let instance = INSTANCE else { return }
254
-
255
- arView = ARView(frame: CGRect(x: 0, y: 0, width: width, height: height))
256
- arView.backgroundColor = instance.hexStringToUIColor(hexColor: "#32a852")
257
- instance.addSubview(arView)
258
- arView.session.delegate = instance
259
- setupAR()
260
- }
261
-
262
- // Continue with the rest of your implementation...
263
- // Including ARSessionDelegate methods, gesture handlers, and utility functions
264
- }