replate-camera 0.3.2 → 0.4.1

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.
@@ -5,115 +5,216 @@ import AVFoundation
5
5
  import ImageIO
6
6
  import MobileCoreServices
7
7
 
8
+ // MARK: - RCTViewManager
8
9
  @objc(ReplateCameraViewManager)
9
10
  class ReplateCameraViewManager: RCTViewManager {
11
+ override func view() -> ReplateCameraView {
12
+ return ReplateCameraView()
13
+ }
10
14
 
11
- override func view() -> (ReplateCameraView) {
12
- let replCameraView = ReplateCameraView()
13
- return replCameraView
14
- }
15
-
16
- @objc override static func requiresMainQueueSetup() -> Bool {
17
- return false
18
- }
19
-
15
+ @objc override static func requiresMainQueueSetup() -> Bool {
16
+ return false
17
+ }
20
18
  }
21
19
 
20
+ // MARK: - UIImage Extension
22
21
  extension UIImage {
23
- func rotate(radians: Float) -> UIImage {
24
- var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
25
- // Trim off the extremely small float value to prevent core graphics from rounding it up
26
- newSize.width = floor(newSize.width)
27
- newSize.height = floor(newSize.height)
28
-
29
- UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
30
- let context = UIGraphicsGetCurrentContext()!
31
-
32
- // Move origin to middle
33
- context.translateBy(x: newSize.width / 2, y: newSize.height / 2)
34
- // Rotate around middle
35
- context.rotate(by: CGFloat(radians))
36
- // Draw the image at its center
37
- self.draw(in: CGRect(x: -self.size.width / 2, y: -self.size.height / 2, width: self.size.width, height: self.size.height))
38
-
39
- let newImage = UIGraphicsGetImageFromCurrentImageContext()!
40
- UIGraphicsEndImageContext()
41
-
42
- return newImage
43
- }
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
+ }
44
43
  }
45
44
 
45
+ // MARK: - ReplateCameraView
46
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)
47
50
 
51
+ // AR View and Core Components
48
52
  static var arView: ARView!
49
- static var anchorEntity: AnchorEntity? = nil
53
+ static var anchorEntity: AnchorEntity?
50
54
  static var model: Entity!
51
- static var spheresModels: [ModelEntity] = []
52
- static var upperSpheresSet: [Bool] = [Bool](repeating: false, count: 72)
53
- static var lowerSpheresSet: [Bool] = [Bool](repeating: false, count: 72)
54
- static var totalPhotosTaken: Int = 0
55
- static var photosFromDifferentAnglesTaken = 0
55
+ static var sessionId: UUID!
56
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)
57
67
  static var sphereRadius = Float(0.004)
58
68
  static var spheresRadius = Float(0.13)
59
69
  static var sphereAngle = Float(5)
60
70
  static var spheresHeight = Float(0.10)
61
- static var dragSpeed = CGFloat(7000)
62
- static var isPaused = false
63
- static var sessionId: UUID!
64
- static var focusModel: Entity!
65
71
  static var distanceBetweenCircles = Float(0.10)
66
- static var circleInFocus = 0 //0 for lower, 1 for upper
72
+
73
+ // Focus and Navigation
74
+ static var focusModel: Entity!
75
+ static var circleInFocus = 0
76
+ static var dragSpeed = CGFloat(7000)
67
77
  static var dotAnchors: [AnchorEntity] = []
68
- static var width = CGFloat(0)
69
- static var height = CGFloat(0)
70
- private var isResetting = false // Flag to prevent infinite loop
71
- private let resetSemaphore = DispatchSemaphore(value: 1) // Semaphore for synchronization
72
78
 
79
+ // Statistics
80
+ static var totalPhotosTaken = 0
81
+ static var photosFromDifferentAnglesTaken = 0
73
82
 
83
+ // Thread Safety
84
+ private static var isResetting = false
85
+
86
+ // MARK: - Initialization
74
87
  override init(frame: CGRect) {
75
88
  super.init(frame: frame)
76
- requestCameraPermissions()
77
- // setupAR()
78
- ReplateCameraView.INSTANCE = self
89
+ setupInitialState()
79
90
  }
80
91
 
81
92
  required init?(coder: NSCoder) {
82
93
  super.init(coder: coder)
94
+ setupInitialState()
95
+ }
96
+
97
+ private func setupInitialState() {
83
98
  requestCameraPermissions()
84
99
  ReplateCameraView.INSTANCE = self
85
- // setupAR()
86
100
  }
87
101
 
88
- static func addRecognizer() {
89
- let recognizer = UITapGestureRecognizer(target: ReplateCameraView.INSTANCE,
90
- action: #selector(ReplateCameraView.INSTANCE.viewTapped(_:)))
91
- ReplateCameraView.arView.addGestureRecognizer(recognizer)
92
- let panGestureRecognizer = UIPanGestureRecognizer(target: ReplateCameraView.INSTANCE, action: #selector(ReplateCameraView.INSTANCE.handlePan(_:)))
93
- ReplateCameraView.arView.addGestureRecognizer(panGestureRecognizer)
94
- let pinchGestureRecognizer = UIPinchGestureRecognizer(target: ReplateCameraView.INSTANCE, action: #selector(ReplateCameraView.INSTANCE.handlePinch(_:)))
95
- ReplateCameraView.arView.addGestureRecognizer(pinchGestureRecognizer)
96
- }
102
+ // MARK: - Layout
103
+ override func layoutSubviews() {
104
+ super.layoutSubviews()
105
+ Self.lock.lock()
106
+ defer { Self.lock.unlock() }
97
107
 
98
- func requestCameraPermissions() {
108
+ ReplateCameraView.width = frame.width
109
+ ReplateCameraView.height = frame.height
110
+ }
99
111
 
100
- if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
101
- print("Camera permissions already granted")
102
- } else {
103
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
104
- if granted {
105
- print("Camera permissions granted")
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()
106
156
  } else {
107
- print("Camera permissions denied")
157
+ configuration.videoFormat = highestResolutionFormat()
108
158
  }
109
- })
110
159
  }
111
- }
112
160
 
113
- override func layoutSubviews() {
114
- super.layoutSubviews()
115
- ReplateCameraView.width = self.frame.width
116
- ReplateCameraView.height = self.frame.height
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
+ @objc func viewTapped(_ recognizer: UITapGestureRecognizer) {
183
+ print("VIEW TAPPED")
184
+ let tapLocation: CGPoint = recognizer.location(in: ReplateCameraView.arView)
185
+ let estimatedPlane: ARRaycastQuery.Target = .estimatedPlane
186
+ let alignment: ARRaycastQuery.TargetAlignment = .horizontal
187
+
188
+ let result: [ARRaycastResult] = ReplateCameraView.arView.raycast(from: tapLocation,
189
+ allowing: estimatedPlane,
190
+ alignment: alignment)
191
+
192
+ guard let rayCast: ARRaycastResult = result.first
193
+ else {
194
+ return
195
+ }
196
+ let anchor = AnchorEntity(world: rayCast.worldTransform)
197
+ print("ANCHOR FOUND\n", anchor.transform)
198
+ let callback = ReplateCameraController.anchorSetCallback
199
+ if (callback != nil) {
200
+ callback!([])
201
+ ReplateCameraController.anchorSetCallback = nil
202
+ }
203
+ if (ReplateCameraView.model == nil && ReplateCameraView.anchorEntity == nil) {
204
+ DispatchQueue.main.async{
205
+ for dot in ReplateCameraView.dotAnchors {
206
+ dot.removeFromParent()
207
+ ReplateCameraView.arView.scene.removeAnchor(dot)
208
+ }
209
+ ReplateCameraView.dotAnchors = []
210
+ }
211
+ ReplateCameraView.anchorEntity = anchor
212
+ createSpheres(y: ReplateCameraView.spheresHeight)
213
+ createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
214
+ createFocusSphere()
215
+ guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
216
+ ReplateCameraView.arView.scene.anchors.append(anchorEntity)
217
+ }
117
218
  }
118
219
 
119
220
  @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
@@ -220,6 +321,82 @@ class ReplateCameraView: UIView, ARSessionDelegate {
220
321
  }
221
322
  }
222
323
 
324
+ // MARK: - Entity Management
325
+
326
+ func createFocusSphere() {
327
+ DispatchQueue.main.async {
328
+ let sphereRadius = ReplateCameraView.sphereRadius * 1.5
329
+
330
+ // Generate the first sphere mesh
331
+ let sphereMesh1 = MeshResource.generateSphere(radius: sphereRadius)
332
+
333
+ // Create the first sphere entity with initial material
334
+ let sphereEntity1 = ModelEntity(mesh: sphereMesh1, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
335
+
336
+ // Set the position for the first sphere entity
337
+ sphereEntity1.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight, z: 0)
338
+
339
+ // Generate the second sphere mesh
340
+ let sphereMesh2 = MeshResource.generateSphere(radius: sphereRadius)
341
+
342
+ // Create the second sphere entity with initial material
343
+ let sphereEntity2 = ModelEntity(mesh: sphereMesh2, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
344
+
345
+ // Set the position for the second sphere entity
346
+ sphereEntity2.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight + ReplateCameraView.distanceBetweenCircles, z: 0)
347
+
348
+
349
+ // Update the material of the sphere entities
350
+ sphereEntity1.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
351
+ sphereEntity2.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
352
+
353
+ let baseOverlayEntity = self.loadModel(named: "center.obj")
354
+ baseOverlayEntity.scale *= 12
355
+ baseOverlayEntity.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.3), roughness: 1, isMetallic: false),
356
+ SimpleMaterial(color: .white.withAlphaComponent(0.7), roughness: 1, isMetallic: false),
357
+ SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)]
358
+
359
+ // Create a parent entity to hold both spheres
360
+ let parentEntity = Entity()
361
+ parentEntity.addChild(sphereEntity1)
362
+ parentEntity.addChild(sphereEntity2)
363
+ parentEntity.addChild(baseOverlayEntity)
364
+
365
+ // Set the focus model for the global state
366
+ ReplateCameraView.focusModel = parentEntity
367
+
368
+ // Safely add the parent entity to the anchor entity
369
+ ReplateCameraView.anchorEntity?.addChild(parentEntity)
370
+ }
371
+ }
372
+
373
+ func createSpheres(y: Float) {
374
+ DispatchQueue.main.async {
375
+ let radius = ReplateCameraView.spheresRadius
376
+
377
+ for i in 0..<72 {
378
+ let angle = Float(i) * (Float.pi / 180) * 5
379
+ let position = SIMD3(
380
+ radius * cos(angle),
381
+ y,
382
+ radius * sin(angle)
383
+ )
384
+
385
+ let sphere = self.createSphere(position: position)
386
+ ReplateCameraView.spheresModels.append(sphere)
387
+ ReplateCameraView.anchorEntity?.addChild(sphere)
388
+ }
389
+ }
390
+ }
391
+
392
+ func createSphere(position: SIMD3<Float>) -> ModelEntity {
393
+ let sphereMesh = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius)
394
+ let material = SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)
395
+ let sphere = ModelEntity(mesh: sphereMesh, materials: [material])
396
+ sphere.position = position
397
+ return sphere
398
+ }
399
+
223
400
  func addDots(to planeAnchor: ARPlaneAnchor) {
224
401
  print("Adding dots to plane anchor") // Debugging line
225
402
  let center = planeAnchor.center
@@ -268,213 +445,85 @@ class ReplateCameraView: UIView, ARSessionDelegate {
268
445
  return circleEntity
269
446
  }
270
447
 
271
- private func loadModel(named name: String) -> ModelEntity {
272
- do{
273
- return try ModelEntity.loadModel(named: name)
274
- }catch{
275
- print("Cannot load model \(name)")
276
- let baseOverlayMesh = MeshResource.generateBox(size: [ReplateCameraView.spheresRadius * 2, 0.01, ReplateCameraView.spheresRadius * 2], cornerRadius: 1)
277
- let baseOverlayEntity = ModelEntity(mesh: baseOverlayMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)])
278
-
279
- baseOverlayEntity.position = SIMD3(x: 0, y: 0.01, z: 0)
280
- return baseOverlayEntity
281
- }
282
- }
283
-
284
- @objc private func viewTapped(_ recognizer: UITapGestureRecognizer) {
285
- print("VIEW TAPPED")
286
- let tapLocation: CGPoint = recognizer.location(in: ReplateCameraView.arView)
287
- let estimatedPlane: ARRaycastQuery.Target = .estimatedPlane
288
- let alignment: ARRaycastQuery.TargetAlignment = .horizontal
448
+ // MARK: - Reset Functionality
449
+ @objc static func reset() {
450
+ arQueue.async {
451
+ Self.lock.lock()
452
+ defer { Self.lock.unlock() }
289
453
 
290
- let result: [ARRaycastResult] = ReplateCameraView.arView.raycast(from: tapLocation,
291
- allowing: estimatedPlane,
292
- alignment: alignment)
454
+ if isResetting { return }
455
+ isResetting = true
293
456
 
294
- guard let rayCast: ARRaycastResult = result.first
295
- else {
296
- return
297
- }
298
- let anchor = AnchorEntity(world: rayCast.worldTransform)
299
- // anchor.orientation = simd_quatf(ix: 0, iy: 0, iz: 0, r: 1)
300
- // anchor.transform.rotation = simd_quatf(ix: 0, iy: 0, iz: 0, r: 1)
301
- print("ANCHOR FOUND\n", anchor.transform)
302
- let callback = ReplateCameraController.anchorSetCallback
303
- if (callback != nil) {
304
- callback!([])
305
- ReplateCameraController.anchorSetCallback = nil
306
- }
307
- if (ReplateCameraView.model == nil && ReplateCameraView.anchorEntity == nil) {
308
- DispatchQueue.main.async{
309
- for dot in ReplateCameraView.dotAnchors {
310
- dot.removeFromParent()
311
- ReplateCameraView.arView.scene.removeAnchor(dot)
457
+ DispatchQueue.main.async {
458
+ tearDownARSession()
459
+ resetProperties()
460
+ setupNewARView()
461
+ isResetting = false
462
+ }
312
463
  }
313
- ReplateCameraView.dotAnchors = []
314
- }
315
- ReplateCameraView.anchorEntity = anchor
316
- createSpheres(y: ReplateCameraView.spheresHeight)
317
- createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
318
- createFocusSphere()
319
-
320
- //DEBUG MESHES
321
- // let xAxis = MeshResource.generateBox(width: 2, height: 0.001, depth: 0.01)
322
- // let xLineEntity = ModelEntity(mesh: xAxis)
323
- // xLineEntity.setPosition(SIMD3<Float>(0, 0, 0), relativeTo: ReplateCameraView.anchorEntity)
324
- // xLineEntity.model?.materials = [SimpleMaterial(color: .red, isMetallic: false)]
325
- // ReplateCameraView.anchorEntity.addChild(xLineEntity)
326
- // let xLineLeftSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
327
- // let xLineLeftSphereEntity = ModelEntity(mesh: xLineLeftSphere)
328
- // ReplateCameraView.anchorEntity.addChild(xLineLeftSphereEntity)
329
- // xLineLeftSphereEntity.setPosition(SIMD3<Float>(-1, 0.002, 0), relativeTo: ReplateCameraView.anchorEntity)
330
- // xLineLeftSphereEntity.model?.materials = [SimpleMaterial(color: .red, isMetallic: false)]
331
- // let xLineRightSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
332
- // let xLineRightSphereEntity = ModelEntity(mesh: xLineRightSphere)
333
- // ReplateCameraView.anchorEntity.addChild(xLineRightSphereEntity)
334
- // xLineRightSphereEntity.setPosition(SIMD3<Float>(1, 0.002, 0), relativeTo: ReplateCameraView.anchorEntity)
335
- // xLineRightSphereEntity.model?.materials = [SimpleMaterial(color: .yellow, isMetallic: false)]
336
- // let yAxis = MeshResource.generateBox(width: 0.01, height: 0.001, depth: 2)
337
- // let yLineEntity = ModelEntity(mesh: yAxis)
338
- // yLineEntity.setPosition(SIMD3<Float>(0, 0, 0), relativeTo: ReplateCameraView.anchorEntity)
339
- // let yLineLeftSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
340
- // let yLineLeftSphereEntity = ModelEntity(mesh: yLineLeftSphere)
341
- // ReplateCameraView.anchorEntity.addChild(yLineLeftSphereEntity)
342
- // yLineLeftSphereEntity.setPosition(SIMD3<Float>(0, 0.002, -1), relativeTo: ReplateCameraView.anchorEntity)
343
- // yLineLeftSphereEntity.model?.materials = [SimpleMaterial(color: .systemPink, isMetallic: false)]
344
- // let yLineRightSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
345
- // let yLineRightSphereEntity = ModelEntity(mesh: yLineRightSphere)
346
- // ReplateCameraView.anchorEntity.addChild(yLineRightSphereEntity)
347
- // yLineRightSphereEntity.setPosition(SIMD3<Float>(0, 0.002, 1), relativeTo: ReplateCameraView.anchorEntity)
348
- // yLineRightSphereEntity.model?.materials = [SimpleMaterial(color: .orange, isMetallic: false)]
349
- // yLineEntity.model?.materials = [SimpleMaterial(color: .purple, isMetallic: false)]
350
- // ReplateCameraView.anchorEntity.addChild(yLineEntity)
351
- // let circleEntity = ModelEntity(mesh: MeshResource.generateBox(size: 2, cornerRadius: 1))
352
- // circleEntity.setPosition(SIMD3<Float>(0, 0, 0), relativeTo: ReplateCameraView.anchorEntity)
353
- // circleEntity.model?.materials = [SimpleMaterial(color: .yellow, isMetallic: false)]
354
-
355
- guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
356
- ReplateCameraView.arView.scene.anchors.append(anchorEntity)
357
464
  }
358
- }
359
-
360
- func createFocusSphere() {
361
- DispatchQueue.main.async {
362
- let sphereRadius = ReplateCameraView.sphereRadius * 1.5
363
-
364
- // Generate the first sphere mesh
365
- let sphereMesh1 = MeshResource.generateSphere(radius: sphereRadius)
366
-
367
- // Create the first sphere entity with initial material
368
- let sphereEntity1 = ModelEntity(mesh: sphereMesh1, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
369
-
370
- // Set the position for the first sphere entity
371
- sphereEntity1.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight, z: 0)
372
-
373
- // Generate the second sphere mesh
374
- let sphereMesh2 = MeshResource.generateSphere(radius: sphereRadius)
375
-
376
- // Create the second sphere entity with initial material
377
- let sphereEntity2 = ModelEntity(mesh: sphereMesh2, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
378
-
379
- // Set the position for the second sphere entity
380
- sphereEntity2.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight + ReplateCameraView.distanceBetweenCircles, z: 0)
381
-
382
-
383
- // Update the material of the sphere entities
384
- sphereEntity1.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
385
- sphereEntity2.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
386
-
387
- let baseOverlayEntity = self.loadModel(named: "center.obj")
388
- baseOverlayEntity.scale *= 12
389
- baseOverlayEntity.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.3), roughness: 1, isMetallic: false),
390
- SimpleMaterial(color: .white.withAlphaComponent(0.7), roughness: 1, isMetallic: false),
391
- SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)]
392
-
393
- // Create a parent entity to hold both spheres
394
- let parentEntity = Entity()
395
- parentEntity.addChild(sphereEntity1)
396
- parentEntity.addChild(sphereEntity2)
397
- parentEntity.addChild(baseOverlayEntity)
398
465
 
399
- // Set the focus model for the global state
400
- ReplateCameraView.focusModel = parentEntity
401
-
402
- // Safely add the parent entity to the anchor entity
403
- ReplateCameraView.anchorEntity?.addChild(parentEntity)
466
+ private static func tearDownARSession() {
467
+ arView?.session.pause()
468
+ arView?.session.delegate = nil
469
+ arView?.scene.anchors.removeAll()
470
+ arView?.removeFromSuperview()
471
+ arView?.window?.resignKey()
404
472
  }
405
- }
406
473
 
407
- func createSphere(position: SIMD3<Float>) -> ModelEntity {
408
- // Ensure execution on the main thread
409
- guard Thread.isMainThread else {
410
- return DispatchQueue.main.sync {
411
- return createSphere(position: position)
412
- }
474
+ private static func resetProperties() {
475
+ anchorEntity = nil
476
+ model = nil
477
+ spheresModels.removeAll()
478
+ upperSpheresSet = [Bool](repeating: false, count: 72)
479
+ lowerSpheresSet = [Bool](repeating: false, count: 72)
480
+ totalPhotosTaken = 0
481
+ photosFromDifferentAnglesTaken = 0
482
+ sphereRadius = 0.004
483
+ spheresRadius = 0.13
484
+ sphereAngle = 5
485
+ spheresHeight = 0.10
486
+ dragSpeed = 7000
487
+ dotAnchors.removeAll()
413
488
  }
414
489
 
415
- // Generate sphere mesh safely
416
- let sphereMesh = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius)
417
-
418
- // Create sphere entity with the specified material
419
- let sphereEntity = ModelEntity(mesh: sphereMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)])
490
+ private static func setupNewARView() {
491
+ guard let instance = INSTANCE else { return }
420
492
 
421
- // Set the position for the sphere entity
422
- sphereEntity.position = position
493
+ arView = ARView(frame: CGRect(x: 0, y: 0, width: width, height: height))
494
+ arView.backgroundColor = instance.hexStringToUIColor(hexColor: "#32a852")
495
+ instance.addSubview(arView)
496
+ arView.session.delegate = instance
497
+ setupAR()
498
+ }
423
499
 
424
- // Return the created sphere entity
425
- return sphereEntity
426
- }
500
+ // MARK: - Utilities functions
501
+ func requestCameraPermissions() {
427
502
 
428
- func createSpheres(y: Float) {
429
- let radius = ReplateCameraView.spheresRadius
430
- for i in 0..<72 {
431
- // Adjust the angle calculation so that the first sphere starts at 0 degrees
432
- let angle = Float(i) * (Float.pi / 180) * 5 // 5 degrees in radians
433
- let x = radius * cos(angle)
434
- let z = radius * sin(angle)
435
- let spherePosition = SIMD3<Float>(x, y, z)
436
- let sphereEntity = createSphere(position: spherePosition)
437
- // if (i == 0) {
438
- // let material = SimpleMaterial(color: .purple, isMetallic: true)
439
- // sphereEntity.model?.materials[0] = material
440
- // }
441
- ReplateCameraView.spheresModels.append(sphereEntity)
442
- ReplateCameraView.anchorEntity?.addChild(sphereEntity)
503
+ if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
504
+ print("Camera permissions already granted")
505
+ } else {
506
+ AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
507
+ if granted {
508
+ print("Camera permissions granted")
509
+ } else {
510
+ print("Camera permissions denied")
511
+ }
512
+ })
443
513
  }
444
514
  }
445
515
 
446
- func setupAR() {
447
- print("Setup AR")
448
- let configuration = ARWorldTrackingConfiguration()
449
- configuration.isLightEstimationEnabled = true
450
-
451
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disableMotionBlur)
452
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disableCameraGrain)
453
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disableAREnvironmentLighting)
454
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disableHDR)
455
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disableFaceMesh)
456
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disableGroundingShadows)
457
- ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disablePersonOcclusion)
458
- configuration.planeDetection = ARWorldTrackingConfiguration.PlaneDetection.horizontal
459
- if #available(iOS 16.0, *) {
460
- print("recommendedVideoFormatForHighResolutionFrameCapturing")
461
- configuration.videoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing ?? ARWorldTrackingConfiguration.recommendedVideoFormatFor4KResolution ?? ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
462
- let resolution1 = format1.imageResolution.width * format1.imageResolution.height
463
- let resolution2 = format2.imageResolution.width * format2.imageResolution.height
464
- return resolution1 < resolution2
465
- })!
466
- } else {
467
- print("Alternative high resolution method")
468
- let maxResolutionFormat = ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
469
- let resolution1 = format1.imageResolution.width * format1.imageResolution.height
470
- let resolution2 = format2.imageResolution.width * format2.imageResolution.height
471
- return resolution1 < resolution2
472
- })!
473
- configuration.videoFormat = maxResolutionFormat
474
- }
475
- ReplateCameraView.arView.session.run(configuration)
476
- ReplateCameraView.arView.addCoaching()
477
- ReplateCameraView.sessionId = ReplateCameraView.arView.session.identifier
516
+ func loadModel(named name: String) -> ModelEntity {
517
+ do{
518
+ return try ModelEntity.loadModel(named: name)
519
+ }catch{
520
+ print("Cannot load model \(name)")
521
+ let baseOverlayMesh = MeshResource.generateBox(size: [ReplateCameraView.spheresRadius * 2, 0.01, ReplateCameraView.spheresRadius * 2], cornerRadius: 1)
522
+ let baseOverlayEntity = ModelEntity(mesh: baseOverlayMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)])
523
+
524
+ baseOverlayEntity.position = SIMD3(x: 0, y: 0.01, z: 0)
525
+ return baseOverlayEntity
526
+ }
478
527
  }
479
528
 
480
529
  @objc var color: String = "" {
@@ -524,73 +573,7 @@ class ReplateCameraView: UIView, ARSessionDelegate {
524
573
  print("SESSION RESUMED")
525
574
  }
526
575
 
527
- func reset() {
528
- DispatchQueue.global().async {
529
- print("Entering reset. Waiting for semaphore.")
530
- self.resetSemaphore.wait() // Wait for the semaphore
531
- print("Obtained semaphore.")
532
- if self.isResetting {
533
- print("Signaling semaphore because reset is already in progress.")
534
- self.resetSemaphore.signal() // Release the semaphore
535
- return
536
- }
537
- self.isResetting = true
538
- self.resetSemaphore.signal() // Release the semaphore
539
- print("Resetting, releasing semaphore.")
540
-
541
- if (!Thread.isMainThread) {
542
- DispatchQueue.main.sync {
543
- print("PROOOOOVA1")
544
- // Pause the existing AR session
545
- ReplateCameraView.arView?.session.pause()
546
- ReplateCameraView.arView?.session.delegate = nil
547
- ReplateCameraView.arView?.scene.anchors.removeAll()
548
-
549
- // Perform UI updates and property resets on the main thread asynchronously
550
- // Remove the existing ARView from the superview
551
- ReplateCameraView.arView?.removeFromSuperview()
552
-
553
- // Resign key window
554
- ReplateCameraView.arView?.window?.resignKey()
555
- print("PROOOOOOVA2")
556
-
557
- // Reset the static properties
558
- ReplateCameraView.anchorEntity = nil
559
- ReplateCameraView.model = nil
560
- ReplateCameraView.spheresModels = []
561
- ReplateCameraView.upperSpheresSet = [Bool](repeating: false, count: 72)
562
- ReplateCameraView.lowerSpheresSet = [Bool](repeating: false, count: 72)
563
- ReplateCameraView.totalPhotosTaken = 0
564
- ReplateCameraView.photosFromDifferentAnglesTaken = 0
565
- ReplateCameraView.sphereRadius = Float(0.004)
566
- ReplateCameraView.spheresRadius = Float(0.13)
567
- ReplateCameraView.sphereAngle = Float(5)
568
- ReplateCameraView.spheresHeight = Float(0.10)
569
- ReplateCameraView.dragSpeed = CGFloat(7000)
570
-
571
-
572
- ReplateCameraView.arView = ARView(frame: CGRect(x: 0, y: 0, width: ReplateCameraView.width, height: ReplateCameraView.height))
573
- ReplateCameraView.arView.backgroundColor = self.hexStringToUIColor(hexColor: "#32a852")
574
- print("PROOOOOOVA3")
575
-
576
- // Add the new ARView to the view hierarchy
577
- self.addSubview(ReplateCameraView.arView)
578
-
579
- // Set the session delegate and run the session
580
- ReplateCameraView.arView?.session.delegate = self
581
- print("PROOOOOOVA")
582
- self.setupAR()
583
-
584
- self.resetSemaphore.wait() // Wait for the semaphore
585
- self.isResetting = false
586
- self.resetSemaphore.signal() // Release the semaphore
587
- }
588
- }
589
- }
590
- }
591
-
592
-
593
- static func generateImpactFeedback(strength: UIImpactFeedbackGenerator.FeedbackStyle) {
576
+ func generateImpactFeedback(strength: UIImpactFeedbackGenerator.FeedbackStyle) {
594
577
  do{
595
578
  let impactFeedbackGenerator = try UIImpactFeedbackGenerator(style: strength)
596
579
  impactFeedbackGenerator.prepare()
@@ -602,553 +585,733 @@ class ReplateCameraView: UIView, ARSessionDelegate {
602
585
 
603
586
  }
604
587
 
605
- @objc(ReplateCameraController)
606
- class ReplateCameraController: NSObject {
607
-
608
- static var completedTutorialCallback: RCTResponseSenderBlock?
609
- static var anchorSetCallback: RCTResponseSenderBlock?
610
- static var completedUpperSpheresCallback: RCTResponseSenderBlock?
611
- static var completedLowerSpheresCallback: RCTResponseSenderBlock?
612
- static var openedTutorialCallback: RCTResponseSenderBlock?
613
- static var tooCloseCallback: RCTResponseSenderBlock?
614
- static var tooFarCallback: RCTResponseSenderBlock?
615
-
616
- @objc(registerOpenedTutorialCallback:)
617
- func registerOpenedTutorialCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
618
- ReplateCameraController.openedTutorialCallback = myCallback
619
- }
620
-
621
- @objc(registerCompletedTutorialCallback:)
622
- func registerCompletedTutorialCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
623
- ReplateCameraController.completedTutorialCallback = myCallback
624
- }
625
588
 
626
- @objc(registerAnchorSetCallback:)
627
- func registerAnchorSetCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
628
- ReplateCameraController.anchorSetCallback = myCallback
629
- }
630
589
 
631
- @objc(registerCompletedUpperSpheresCallback:)
632
- func registerCompletedUpperSpheresCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
633
- ReplateCameraController.completedUpperSpheresCallback = myCallback
634
- }
590
+ // MARK: - Supporting Types
591
+ enum ARError: Error {
592
+ case noAnchor
593
+ case invalidAnchor
594
+ case notInFocus
595
+ case captureError
596
+ case tooManyImages
597
+ case processingError
598
+ case savingError
599
+ case transformError
600
+ case lightingError
601
+ case unknown
602
+
603
+ var localizedDescription: String {
604
+ switch self {
605
+ case .noAnchor: return "[ReplateCameraController] No anchor set yet"
606
+ case .invalidAnchor: return "[ReplateCameraController] AnchorNode is not valid"
607
+ case .notInFocus: return "[ReplateCameraController] Object not in focus"
608
+ case .captureError: return "[ReplateCameraController] Error capturing image"
609
+ case .tooManyImages: return "[ReplateCameraController] Too many images and the last one's not from a new angle"
610
+ case .processingError: return "[ReplateCameraController] Error processing image"
611
+ case .savingError: return "[ReplateCameraController] Error saving photo"
612
+ case .transformError: return "[ReplateCameraController] Camera transform data not available"
613
+ case .lightingError: return "[ReplateCameraController] Image too dark"
614
+ case .unknown: return "[ReplateCameraController] Unknown error occurred"
615
+ }
616
+ }
617
+ }
635
618
 
636
- @objc(registerCompletedLowerSpheresCallback:)
637
- func registerCompletedLowerSpheresCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
638
- ReplateCameraController.completedLowerSpheresCallback = myCallback
639
- }
619
+ struct DeviceTargetInfo {
620
+ let isInFocus: Bool
621
+ let targetIndex: Int
622
+ let transform: simd_float4x4
623
+ let cameraPosition: SIMD3<Float>
624
+ let deviceDirection: SIMD3<Float>
640
625
 
641
- @objc(registerTooCloseCallback:)
642
- func registerTooCloseCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
643
- ReplateCameraController.tooCloseCallback = myCallback
644
- }
626
+ var isValidTarget: Bool {
627
+ return targetIndex != -1
628
+ }
629
+ }
645
630
 
646
- @objc(registerTooFarCallback:)
647
- func registerTooFarCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
648
- ReplateCameraController.tooFarCallback = myCallback
649
- }
631
+ class SafeCallbackHandler {
632
+ private let resolver: RCTPromiseResolveBlock
633
+ private let rejecter: RCTPromiseRejectBlock
634
+ var hasCalledBack = false
635
+ private let lock = NSLock()
650
636
 
651
- @objc(getPhotosCount:rejecter:)
652
- func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) -> Void {
653
- resolver(ReplateCameraView.totalPhotosTaken)
654
- }
637
+ init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
638
+ self.resolver = resolver
639
+ self.rejecter = rejecter
640
+ }
655
641
 
656
- @objc(isScanComplete:rejecter:)
657
- func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) -> Void {
658
- resolver(ReplateCameraView.photosFromDifferentAnglesTaken == 144)
659
- }
642
+ func resolve(_ result: Any) {
643
+ lock.lock()
644
+ defer { lock.unlock() }
645
+ guard !hasCalledBack else {
646
+ print("rejecter: Callback already invoked.")
647
+ return
648
+ }
649
+ hasCalledBack = true
650
+ resolver(result)
651
+ }
660
652
 
661
- @objc(getRemainingAnglesToScan:rejecter:)
662
- func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) -> Void {
663
- resolver(144 - ReplateCameraView.photosFromDifferentAnglesTaken)
664
- }
653
+ func reject(_ error: ARError) {
654
+ lock.lock()
655
+ defer { lock.unlock() }
665
656
 
666
- @objc
667
- func reset(){
668
- ReplateCameraView.INSTANCE.reset()
669
- }
657
+ guard !hasCalledBack else {
658
+ print("rejecter: Callback already invoked.")
659
+ return
660
+ }
661
+ hasCalledBack = true
662
+ rejecter(
663
+ String(describing: error),
664
+ error.localizedDescription,
665
+ NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
666
+ )
667
+ }
668
+ }
670
669
 
671
- func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
672
- // Check if the transform is initialized
673
- let transform = anchorNode.transform
674
- if(transform == nil){
675
- print("Transform is not initialized.")
676
- return false
677
- }
670
+ // MARK: - Main Controller
671
+ @objc(ReplateCameraController)
672
+ class ReplateCameraController: NSObject {
673
+ // MARK: - Static Properties
674
+ private static let lock = NSLock()
675
+ private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
676
+
677
+ // Configuration Constants
678
+ private static let MIN_DISTANCE: Float = 0.15
679
+ private static let MAX_DISTANCE: Float = 0.45
680
+ private static let ANGLE_THRESHOLD: Float = 0.6
681
+ private static let TARGET_IMAGE_SIZE = CGSize(width: 1728, height: 1296)
682
+ private static let MIN_AMBIENT_INTENSITY: CGFloat = 650
683
+
684
+ // Callbacks
685
+ static var completedTutorialCallback: RCTResponseSenderBlock?
686
+ static var anchorSetCallback: RCTResponseSenderBlock?
687
+ static var completedUpperSpheresCallback: RCTResponseSenderBlock?
688
+ static var completedLowerSpheresCallback: RCTResponseSenderBlock?
689
+ static var openedTutorialCallback: RCTResponseSenderBlock?
690
+ static var tooCloseCallback: RCTResponseSenderBlock?
691
+ static var tooFarCallback: RCTResponseSenderBlock?
692
+
693
+ // MARK: - Callback Registration Methods
694
+ @objc(registerOpenedTutorialCallback:)
695
+ func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
696
+ Self.lock.lock()
697
+ defer { Self.lock.unlock() }
698
+ ReplateCameraController.openedTutorialCallback = callback
699
+ }
678
700
 
679
- // Check position, rotation, and scale
680
- let position = transform.translation
681
- let rotation = transform.rotation
682
- let scale = transform.scale
701
+ @objc(registerCompletedTutorialCallback:)
702
+ func registerCompletedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
703
+ Self.lock.lock()
704
+ defer { Self.lock.unlock() }
705
+ ReplateCameraController.completedTutorialCallback = callback
706
+ }
683
707
 
684
- // Validate position, rotation, and scale
685
- guard !position.isNaN else {
686
- print("Position contains NaN values.")
687
- return false
688
- }
708
+ @objc(registerAnchorSetCallback:)
709
+ func registerAnchorSetCallback(_ callback: @escaping RCTResponseSenderBlock) {
710
+ Self.lock.lock()
711
+ defer { Self.lock.unlock() }
712
+ ReplateCameraController.anchorSetCallback = callback
713
+ }
689
714
 
690
- guard !rotation.isNaN else {
691
- print("Rotation contains NaN values.")
692
- return false
693
- }
715
+ @objc(registerCompletedUpperSpheresCallback:)
716
+ func registerCompletedUpperSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
717
+ Self.lock.lock()
718
+ defer { Self.lock.unlock() }
719
+ ReplateCameraController.completedUpperSpheresCallback = callback
720
+ }
694
721
 
695
- guard scale != SIMD3<Float>(0, 0, 0) else {
696
- print("Scale is zero.")
697
- return false
698
- }
722
+ @objc(registerCompletedLowerSpheresCallback:)
723
+ func registerCompletedLowerSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
724
+ Self.lock.lock()
725
+ defer { Self.lock.unlock() }
726
+ ReplateCameraController.completedLowerSpheresCallback = callback
727
+ }
699
728
 
700
- // Additional check to ensure the rotation quaternion is normalized
701
- guard abs(length(rotation.vector) - 1.0) < 0.0001 else {
702
- print("Rotation quaternion is not normalized.")
703
- return false
704
- }
729
+ @objc(registerTooCloseCallback:)
730
+ func registerTooCloseCallback(_ callback: @escaping RCTResponseSenderBlock) {
731
+ Self.lock.lock()
732
+ defer { Self.lock.unlock() }
733
+ ReplateCameraController.tooCloseCallback = callback
734
+ }
705
735
 
706
- // All checks passed
707
- return true
708
- }
736
+ @objc(registerTooFarCallback:)
737
+ func registerTooFarCallback(_ callback: @escaping RCTResponseSenderBlock) {
738
+ Self.lock.lock()
739
+ defer { Self.lock.unlock() }
740
+ ReplateCameraController.tooFarCallback = callback
741
+ }
709
742
 
710
- @objc(takePhoto:resolver:rejecter:)
711
- func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
712
- var hasCalledBack = false
743
+ // MARK: - Public Methods
744
+ @objc(getPhotosCount:rejecter:)
745
+ func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
746
+ Self.lock.lock()
747
+ let count = ReplateCameraView.totalPhotosTaken
748
+ Self.lock.unlock()
749
+ resolver(count)
750
+ }
713
751
 
714
- func safeResolver(_ result: Any) {
715
- if !hasCalledBack {
716
- hasCalledBack = true
717
- resolver(result)
718
- } else {
719
- print("resolver: Callback already invoked.")
720
- }
752
+ @objc(isScanComplete:rejecter:)
753
+ func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
754
+ Self.lock.lock()
755
+ let isComplete = ReplateCameraView.photosFromDifferentAnglesTaken == 144
756
+ Self.lock.unlock()
757
+ resolver(isComplete)
721
758
  }
722
759
 
723
- func safeRejecter(_ code: String, _ message: String, _ error: NSError) {
724
- if !hasCalledBack {
725
- hasCalledBack = true
726
- rejecter(code, "{\"code\": \(code), \"message\": \"\(message)\"}", error)
727
- } else {
728
- print("rejecter: Callback already invoked.")
729
- }
760
+ @objc(getRemainingAnglesToScan:rejecter:)
761
+ func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
762
+ Self.lock.lock()
763
+ let remaining = 144 - ReplateCameraView.photosFromDifferentAnglesTaken
764
+ Self.lock.unlock()
765
+ resolver(remaining)
730
766
  }
731
767
 
732
- do {
733
- guard let anchorNode = ReplateCameraView.anchorEntity else {
734
- safeRejecter("001", "[ReplateCameraController] No anchor set yet", NSError(domain: "ReplateCameraController", code: 001, userInfo: nil))
735
- return
736
- }
737
- var anchorPosition: SIMD3<Float>? = nil
738
- if(!Thread.isMainThread){
739
- DispatchQueue.main.sync {
740
- if isAnchorNodeValid(anchorNode) {
741
- anchorPosition = anchorNode.position(relativeTo: nil)
742
- print("Anchor position in world space: \(anchorPosition)")
743
- } else {
744
- print("AnchorNode is not valid.")
745
- safeRejecter("001", "[ReplateCameraController] AnchorNode is not valid.", NSError(domain: "ReplateCameraController", code: 001, userInfo: nil))
746
- return
747
- }
768
+ @objc
769
+ func reset() {
770
+ DispatchQueue.main.async {
771
+ ReplateCameraView.reset()
748
772
  }
749
- }
750
- guard let anchorPosition else {
751
- safeRejecter("001", "[ReplateCameraController] No anchor set yet", NSError(domain: "ReplateCameraController", code: 001, userInfo: nil))
752
- return
753
- }
754
- let spheresHeight = ReplateCameraView.spheresHeight
755
- let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
756
- let point1Y = anchorPosition.y + spheresHeight
757
- let point2Y = anchorPosition.y + distanceBetweenCircles + spheresHeight
758
- let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
759
- var deviceTargetInFocus = -1
760
- let angleThreshold: Float = 0.6
761
- var relativeCameraTransform: simd_float4x4
762
- if let cameraTransform = ReplateCameraView.arView.session.currentFrame?.camera.transform {
763
- relativeCameraTransform = ReplateCameraController.getTransformRelativeToAnchor(anchor: anchorNode, cameraTransform: cameraTransform)
764
- let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
765
- let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
766
- let directionToAnchor = normalize(anchorPosition - cameraPosition)
767
- let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
773
+ }
768
774
 
769
- if angleToAnchor < angleThreshold {
770
- let cameraHeight = relativeCameraTransform.columns.3.y
771
- if cameraHeight < twoThirdsDistance {
772
- deviceTargetInFocus = 0
773
- print("Is pointing at first point")
774
- } else {
775
- deviceTargetInFocus = 1
776
- print("Is pointing at second point")
777
- }
778
- } else {
779
- print("Not pointing at anchor")
780
- }
781
- } else {
782
- print("Camera transform data not available")
783
- safeRejecter("002", "[ReplateCameraController] Camera transform data not available", NSError(domain: "ReplateCameraController", code: 002, userInfo: nil))
784
- return
785
- }
775
+ // MARK: - Photo Capture and Processing
776
+ @objc(takePhoto:resolver:rejecter:)
777
+ func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
778
+ Self.arQueue.async { [weak self] in
779
+ guard let self = self else { return }
786
780
 
787
- if deviceTargetInFocus != -1 {
781
+ let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
788
782
 
789
- func setOpacityToCircle(circleId: Int, opacity: Float) {
790
- DispatchQueue.main.async{
791
- for i in 0..<72 {
792
- let offset = circleId == 0 ? 0 : 72
793
- let entity = ReplateCameraView.spheresModels[i+offset]
794
- let material = entity.model?.materials[0]
795
- if (material is SimpleMaterial && material != nil){
796
- let simpleMaterial = material! as? SimpleMaterial
797
- if #available(iOS 15.0, *) {
798
- let newMaterial = SimpleMaterial(color: simpleMaterial?.color.tint.withAlphaComponent(CGFloat(opacity)) ?? SimpleMaterial.Color.white.withAlphaComponent(CGFloat(opacity)), roughness: 1, isMetallic: false)
799
- entity.model?.materials[0] = newMaterial
800
- } else {
801
- // Fallback on earlier versions
802
- }
803
- }
783
+ do {
784
+ try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
785
+ } catch let error as ARError {
786
+ callbackHandler.reject(error)
787
+ } catch {
788
+ callbackHandler.reject(.unknown)
804
789
  }
805
- }
806
- }
807
- if(deviceTargetInFocus != ReplateCameraView.circleInFocus){
808
- setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
809
- setOpacityToCircle(circleId: deviceTargetInFocus, opacity: 1)
810
- ReplateCameraView.circleInFocus = deviceTargetInFocus
811
- ReplateCameraView.generateImpactFeedback(strength: .heavy)
812
790
  }
813
- updateSpheres(deviceTargetInFocus: deviceTargetInFocus, cameraTransform: relativeCameraTransform) { result in
814
- if !unlimited && !result {
815
- safeRejecter("003", "[ReplateCameraController] Too many images and the last one's not from a new angle", NSError(domain: "ReplateCameraController", code: 003, userInfo: nil))
816
- return
817
- }
791
+ }
792
+
793
+ private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
794
+ let anchorEntity = try getValidAnchorEntity()
795
+ let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
818
796
 
819
- if let image = ReplateCameraView.arView?.session.currentFrame?.capturedImage {
820
- let ciImage = CIImage(cvImageBuffer: image)
797
+ if deviceTargetInfo.isValidTarget {
798
+ try processTargetedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
799
+ } else {
800
+ throw ARError.notInFocus
801
+ }
802
+ }
821
803
 
822
- // Define the target size for the reduced resolution
823
- let targetSize = CGSize(width: 1728, height: 1296) // Change this to your desired resolution
804
+ private func getValidAnchorEntity() throws -> AnchorEntity {
805
+ var anchorEntity: AnchorEntity?
806
+ if(Thread.isMainThread){
807
+ anchorEntity = ReplateCameraView.anchorEntity
824
808
 
825
- // Create a scaling filter to resize the image
826
- let scaleFilter = CIFilter(name: "CILanczosScaleTransform")!
827
- scaleFilter.setValue(ciImage, forKey: kCIInputImageKey)
828
- scaleFilter.setValue(targetSize.width / ciImage.extent.width, forKey: kCIInputScaleKey) // Scale factor
829
- scaleFilter.setValue(1.0, forKey: kCIInputAspectRatioKey) // Maintain aspect ratio
809
+ }else{
810
+ DispatchQueue.main.sync {
811
+ anchorEntity = ReplateCameraView.anchorEntity
812
+ }
813
+ }
830
814
 
831
- // Get the output CIImage
832
- guard let scaledCIImage = scaleFilter.outputImage else {
833
- safeRejecter("005", "[ReplateCameraController] Error scaling CIImage", NSError(domain: "ReplateCameraController", code: 005, userInfo: nil))
834
- return
835
- }
836
815
 
837
- // Convert the resized CIImage to CGImage
838
- guard let cgImage = ReplateCameraController.cgImage(from: scaledCIImage) else {
839
- safeRejecter("004", "[ReplateCameraController] Error converting CIImage to CGImage", NSError(domain: "ReplateCameraController", code: 004, userInfo: nil))
840
- return
841
- }
816
+ guard let anchor = anchorEntity else {
817
+ throw ARError.noAnchor
818
+ }
842
819
 
843
- // Create UIImage from CGImage
844
- let uiImage = UIImage(cgImage: cgImage)
820
+ guard isAnchorNodeValid(anchor) else {
821
+ throw ARError.invalidAnchor
822
+ }
845
823
 
846
- // Rotate the image if needed
847
- let finImage = uiImage.rotate(radians: .pi / 2) // Adjust radians as needed
824
+ return anchor
825
+ }
848
826
 
849
- // Check light estimate
850
- if let lightEstimate = ReplateCameraView.arView.session.currentFrame?.lightEstimate {
851
- let ambientIntensity = lightEstimate.ambientIntensity
852
- let ambientColorTemperature = lightEstimate.ambientColorTemperature
827
+ private func getDeviceTargetInfo(anchorEntity: AnchorEntity) throws -> DeviceTargetInfo {
828
+ guard let frame = ReplateCameraView.arView.session.currentFrame else {
829
+ throw ARError.transformError
830
+ }
853
831
 
854
- if ambientIntensity < 650 {
855
- safeRejecter("005", "[ReplateCameraController] Image too dark", NSError(domain: "ReplateCameraController", code: 005, userInfo: nil))
856
- return
857
- }
832
+ let cameraTransform = frame.camera.transform
833
+ let relativeCameraTransform = getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
858
834
 
859
- print("Ambient Intensity: \(ambientIntensity)")
860
- print("Color Temperature: \(ambientColorTemperature)")
861
- }
835
+ var anchorPosition: SIMD3<Float>!
836
+ if(Thread.isMainThread){
837
+ anchorPosition = anchorEntity.position(relativeTo: nil)
862
838
 
863
- // Save the final image
864
- if let url = ReplateCameraController.saveImageAsJPEG(finImage) {
865
- safeResolver(url.absoluteString)
866
- } else {
867
- safeRejecter("006", "[ReplateCameraController] Error saving photo", NSError(domain: "ReplateCameraController", code: 006, userInfo: nil))
868
- }
869
- }
839
+ }else{
840
+ DispatchQueue.main.sync {
841
+ anchorPosition = anchorEntity.position(relativeTo: nil)
870
842
  }
871
- } else {
872
- safeRejecter("007", "[ReplateCameraController] Object not in focus", NSError(domain: "ReplateCameraController", code: 007, userInfo: nil))
873
843
  }
874
- } catch {
875
- print("Unexpected error occurred")
876
- }
877
- }
878
844
 
879
845
 
880
- static func cgImage(from ciImage: CIImage) -> CGImage? {
881
- let context = CIContext(options: nil)
846
+ let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
847
+ let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
848
+ let directionToAnchor = normalize(anchorPosition - cameraPosition)
849
+ let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
882
850
 
883
- return context.createCGImage(ciImage, from: ciImage.extent)
884
- }
851
+ let targetIndex = determineTargetIndex(angleToAnchor: angleToAnchor, relativeCameraTransform: relativeCameraTransform)
885
852
 
886
- static func getCameraTransformString(from session: ARSession) -> String? {
887
- guard let currentFrame = session.currentFrame else {
888
- print("No current frame available")
889
- return nil
853
+ return DeviceTargetInfo(
854
+ isInFocus: angleToAnchor < Self.ANGLE_THRESHOLD,
855
+ targetIndex: targetIndex,
856
+ transform: relativeCameraTransform,
857
+ cameraPosition: cameraPosition,
858
+ deviceDirection: deviceDirection
859
+ )
890
860
  }
891
861
 
892
- // Extract the camera transform matrix
893
- let cameraTransform = currentFrame.camera.transform
862
+ private func determineTargetIndex(angleToAnchor: Float, relativeCameraTransform: simd_float4x4) -> Int {
863
+ guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
894
864
 
895
- // Serialize the transform matrix into a string
896
- let transformString = """
897
- \(cameraTransform.columns.0.x),\(cameraTransform.columns.0.y),\(cameraTransform.columns.0.z),\(cameraTransform.columns.0.w);
898
- \(cameraTransform.columns.1.x),\(cameraTransform.columns.1.y),\(cameraTransform.columns.1.z),\(cameraTransform.columns.1.w);
899
- \(cameraTransform.columns.2.x),\(cameraTransform.columns.2.y),\(cameraTransform.columns.2.z),\(cameraTransform.columns.2.w);
900
- \(cameraTransform.columns.3.x),\(cameraTransform.columns.3.y),\(cameraTransform.columns.3.z),\(cameraTransform.columns.3.w)
901
- """
865
+ let spheresHeight = ReplateCameraView.spheresHeight
866
+ let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
867
+ let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
868
+ let cameraHeight = relativeCameraTransform.columns.3.y
902
869
 
903
- return transformString
904
- }
870
+ return cameraHeight < twoThirdsDistance ? 0 : 1
871
+ }
905
872
 
906
- static func saveImageAsJPEG(_ image: UIImage) -> URL? {
907
- let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
908
- // Convert UIImage to Data with JPEG representation
909
- guard let imageData = image.jpegData(compressionQuality: 1) else {
910
- // Handle error if unable to convert to JPEG data
911
- print("Error converting UIImage to JPEG data")
912
- return nil
873
+ private func processTargetedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
874
+ DispatchQueue.main.async { [weak self] in
875
+ guard let self = self else { return }
876
+
877
+ self.updateCircleFocus(targetIndex: deviceTargetInfo.targetIndex)
878
+ self.checkCameraDistance(deviceTargetInfo: deviceTargetInfo)
879
+
880
+ self.updateSpheres(
881
+ deviceTargetInfo: deviceTargetInfo,
882
+ cameraTransform: deviceTargetInfo.transform
883
+ ) { success in
884
+ if !unlimited && !success {
885
+ callbackHandler.reject(.tooManyImages)
886
+ return
887
+ }
888
+
889
+ self.captureAndProcessImage(callbackHandler: callbackHandler)
890
+ }
891
+ }
913
892
  }
914
893
 
915
- // Create a CGImageSource from the image data
916
- guard let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
917
- print("Error creating image source")
918
- return nil
894
+ private func updateCircleFocus(targetIndex: Int) {
895
+ if targetIndex != ReplateCameraView.circleInFocus {
896
+ setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
897
+ setOpacityToCircle(circleId: targetIndex, opacity: 1)
898
+ ReplateCameraView.circleInFocus = targetIndex
899
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
900
+ }
919
901
  }
920
902
 
921
- // Get the temporary directory URL
922
- let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
923
- let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
924
- let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
903
+ private func checkCameraDistance(deviceTargetInfo: DeviceTargetInfo) {
904
+ let distance = isCameraWithinRange(
905
+ cameraTransform: deviceTargetInfo.transform,
906
+ anchorEntity: ReplateCameraView.anchorEntity!
907
+ )
908
+
909
+ switch distance {
910
+ case 1:
911
+ ReplateCameraController.tooFarCallback?([])
912
+ case -1:
913
+ ReplateCameraController.tooCloseCallback?([])
914
+ default:
915
+ break
916
+ }
917
+ }
925
918
 
926
- // Create a mutable copy of the metadata
927
- guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
928
- print("Error copying image properties")
929
- return nil
919
+ // MARK: - Image Processing
920
+ private func captureAndProcessImage(callbackHandler: SafeCallbackHandler) {
921
+ guard let frame = ReplateCameraView.arView?.session.currentFrame else {
922
+ callbackHandler.reject(.captureError)
923
+ return
924
+ }
925
+
926
+ // Check lighting conditions
927
+ if let lightEstimate = frame.lightEstimate {
928
+ guard lightEstimate.ambientIntensity >= Self.MIN_AMBIENT_INTENSITY else {
929
+ callbackHandler.reject(.lightingError)
930
+ return
931
+ }
932
+ }
933
+
934
+ processAndSaveImage(frame.capturedImage, callbackHandler: callbackHandler)
930
935
  }
931
- var mutableMetadata = imageProperties
932
936
 
933
- // Add the camera transform to the metadata
934
- mutableMetadata[kCGImagePropertyExifDictionary] = [
935
- kCGImagePropertyExifUserComment: cameraTransform
936
- ]
937
+ private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
938
+ let ciImage = CIImage(cvImageBuffer: pixelBuffer)
937
939
 
938
- // Create a CGImageDestination to write the image data with metadata
939
- guard let destination = CGImageDestinationCreateWithURL(fileURL as CFURL, kUTTypeJPEG, 1, nil) else {
940
- print("Error creating image destination")
941
- return nil
940
+ guard let resizedImage = resizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
941
+ let cgImage = cgImage(from: resizedImage) else {
942
+ callbackHandler.reject(.processingError)
943
+ return
942
944
  }
943
945
 
944
- // Add the image from the source to the destination with the updated metadata
945
- CGImageDestinationAddImageFromSource(destination, source, 0, mutableMetadata as CFDictionary)
946
+ let uiImage = UIImage(cgImage: cgImage)
947
+ let rotatedImage = uiImage.rotate(radians: .pi / 2)
946
948
 
947
- // Finalize the image destination
948
- if !CGImageDestinationFinalize(destination) {
949
- print("Error finalizing image destination")
950
- return nil
949
+ guard let savedURL = saveImageAsJPEG(rotatedImage) else {
950
+ callbackHandler.reject(.processingError)
951
+ return
951
952
  }
952
953
 
953
- print("Image saved at: \(fileURL.absoluteString)")
954
- return fileURL
954
+ callbackHandler.resolve(savedURL.absoluteString)
955
955
  }
956
956
 
957
- func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity, minDistance: Float = 0.15, maxDistance: Float = 0.45) -> Int {
958
- // Extract the camera's position from the cameraTransform (simd_float4x4)
959
- let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
960
957
 
961
- // Extract the anchor's position from its transform
962
- let anchorPosition = anchorEntity.transform.translation
958
+ func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
959
+ guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
963
960
 
964
- // Calculate the Euclidean distance between the camera and the anchor
965
- let distance = distanceBetween(cameraPosition, anchorPosition)
966
- print("Actual camera distance: \(distance)")
967
- // Check if the distance is within the range
968
- return distance <= minDistance ? -1 : distance >= maxDistance ? 1 : 0
969
- }
961
+ scaleFilter.setValue(image, forKey: kCIInputImageKey)
962
+ scaleFilter.setValue(targetSize.width / image.extent.width, forKey: kCIInputScaleKey)
963
+ scaleFilter.setValue(1.0, forKey: kCIInputAspectRatioKey)
970
964
 
971
- func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
972
- let dx = pos1.x - pos2.x
973
- let dy = pos1.y - pos2.y
974
- let dz = pos1.z - pos2.z
965
+ return scaleFilter.outputImage
966
+ }
975
967
 
976
- // Return the Euclidean distance
977
- return sqrt(dx * dx + dy * dy + dz * dz)
978
- }
968
+ // MARK: - Entity Management
969
+ func setOpacityToCircle(circleId: Int, opacity: Float) {
970
+ DispatchQueue.main.async {
971
+ for i in 0..<72 {
972
+ let offset = circleId == 0 ? 0 : 72
973
+ guard i + offset < ReplateCameraView.spheresModels.count else { continue }
979
974
 
980
- func updateSpheres(deviceTargetInFocus: Int, cameraTransform: simd_float4x4, completion: @escaping (Bool) -> Void) {
981
- // Ensure the function handles a single completion call
982
- var completionCalled = false
983
- func callCompletion(_ result: Bool) {
984
- if !completionCalled {
985
- completionCalled = true
986
- completion(result)
987
- } else {
988
- print("Completion already called")
989
- }
975
+ let entity = ReplateCameraView.spheresModels[i + offset]
976
+ self.updateEntityMaterial(entity, opacity: opacity)
977
+ }
978
+ }
990
979
  }
991
980
 
992
- // When the user pinches the screen, spheres are recreated,
993
- // we have to make sure all spheres have been recreated before proceeding
994
- if (ReplateCameraView.spheresModels.count < 144) {
995
- print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
996
- callCompletion(false)
997
- return
998
- }
981
+ private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
982
+ guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
999
983
 
1000
- guard let anchorNode = ReplateCameraView.anchorEntity else {
1001
- print("[updateSpheres] No anchor entity found.")
1002
- callCompletion(false)
1003
- return
984
+ if #available(iOS 15.0, *) {
985
+ let newMaterial = SimpleMaterial(
986
+ color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
987
+ roughness: 1,
988
+ isMetallic: false
989
+ )
990
+ entity.model?.materials[0] = newMaterial
991
+ }
1004
992
  }
1005
993
 
1006
- // Get the camera's pose
1007
- guard let frame = ReplateCameraView.arView.session.currentFrame else {
1008
- print("[updateSpheres] No current frame available.")
1009
- callCompletion(false)
1010
- return
1011
- }
1012
- let cameraDistance = isCameraWithinRange(cameraTransform: cameraTransform, anchorEntity: anchorNode)
1013
- print("Camera distance: \(cameraDistance)")
1014
- func showAlert(_ message: String) {
1015
- DispatchQueue.main.async {
1016
- let alert = UIAlertController(title: "Alert", message: message, preferredStyle: .alert)
1017
- alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
994
+ private func updateSpheres(deviceTargetInfo: DeviceTargetInfo, cameraTransform: simd_float4x4, completion: @escaping (Bool) -> Void) {
995
+ var completionCalled = false
1018
996
 
1019
- if let rootViewController = UIApplication.shared.keyWindow?.rootViewController {
1020
- rootViewController.present(alert, animated: true, completion: nil)
997
+ func safeCompletion(_ result: Bool) {
998
+ guard !completionCalled else {
999
+ print("Completion already called")
1000
+ return
1001
+ }
1002
+ completionCalled = true
1003
+ completion(result)
1021
1004
  }
1022
- }
1023
- }
1024
1005
 
1025
- switch cameraDistance{
1026
- case 1:
1027
- // showAlert("TOO FAR")
1028
- print("TOO FAR")
1029
- break;
1006
+ // Validate spheres initialization
1007
+ guard ReplateCameraView.spheresModels.count >= 144 else {
1008
+ print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
1009
+ safeCompletion(false)
1010
+ return
1011
+ }
1030
1012
 
1031
- case -1:
1032
- // showAlert("TOO CLOSE")
1033
- print("TOO CLOSE")
1034
- break;
1035
- default: break;
1036
- }
1013
+ // Get anchor
1014
+ guard let anchorNode = ReplateCameraView.anchorEntity else {
1015
+ print("[updateSpheres] No anchor entity found.")
1016
+ safeCompletion(false)
1017
+ return
1018
+ }
1037
1019
 
1038
- if(cameraDistance != 0){
1039
- if(cameraDistance == 1 && ReplateCameraController.tooFarCallback != nil){
1040
- ReplateCameraController.tooFarCallback!([])
1041
- }else if(cameraDistance == -1 && ReplateCameraController.tooCloseCallback != nil){
1042
- ReplateCameraController.tooCloseCallback!([])
1043
- }
1020
+ // Calculate camera metrics
1021
+ let cameraDistance = isCameraWithinRange(
1022
+ cameraTransform: cameraTransform,
1023
+ anchorEntity: anchorNode
1024
+ )
1025
+
1026
+ // Calculate sphere index
1027
+ let angleDegrees = angleBetweenAnchorXAndCamera(
1028
+ anchor: anchorNode,
1029
+ cameraTransform: cameraTransform
1030
+ )
1031
+ let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72
1032
+
1033
+ DispatchQueue.main.async {
1034
+ self.processSphereUpdate(
1035
+ sphereIndex: sphereIndex,
1036
+ targetIndex: deviceTargetInfo.targetIndex,
1037
+ completion: safeCompletion
1038
+ )
1039
+ }
1044
1040
  }
1045
1041
 
1046
- // Calculate the angle between the camera and the anchor
1047
- let angleDegrees = ReplateCameraController.angleBetweenAnchorXAndCamera(anchor: anchorNode,
1048
- cameraTransform: cameraTransform)
1049
- let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72 // Ensure sphereIndex stays within 0-71 bounds
1042
+ private func processSphereUpdate(sphereIndex: Int, targetIndex: Int, completion: @escaping (Bool) -> Void) {
1043
+ var mesh: ModelEntity?
1044
+ var newAngle = false
1045
+ var callback: RCTResponseSenderBlock?
1050
1046
 
1051
- var mesh: ModelEntity?
1052
- var newAngle = false
1053
- var callback: RCTResponseSenderBlock? = nil
1054
- print("Sphere index \(sphereIndex) - Spheres length \(ReplateCameraView.spheresModels.count)")
1047
+ if targetIndex == 1 {
1048
+ guard sphereIndex < ReplateCameraView.upperSpheresSet.count else {
1049
+ print("[updateSpheres] Sphere index out of bounds")
1050
+ completion(false)
1051
+ return
1052
+ }
1055
1053
 
1056
- if deviceTargetInFocus == 1 {
1057
- if sphereIndex >= ReplateCameraView.upperSpheresSet.count {
1058
- print("[updateSpheres] Sphere index out of range. Index: \(sphereIndex), Count: \(ReplateCameraView.upperSpheresSet.count)")
1059
- callCompletion(false)
1060
- return
1061
- }
1054
+ if !ReplateCameraView.upperSpheresSet[sphereIndex] {
1055
+ ReplateCameraView.upperSpheresSet[sphereIndex] = true
1056
+ ReplateCameraView.photosFromDifferentAnglesTaken += 1
1057
+ newAngle = true
1058
+
1059
+ guard 72 + sphereIndex < ReplateCameraView.spheresModels.count else {
1060
+ print("[updateSpheres] Upper spheresModels index out of range")
1061
+ completion(false)
1062
+ return
1063
+ }
1062
1064
 
1063
- if !ReplateCameraView.upperSpheresSet[sphereIndex] {
1064
- ReplateCameraView.upperSpheresSet[sphereIndex] = true
1065
- ReplateCameraView.photosFromDifferentAnglesTaken += 1
1066
- newAngle = true
1065
+ mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
1067
1066
 
1068
- if 72 + sphereIndex >= ReplateCameraView.spheresModels.count {
1069
- print("[updateSpheres] Upper spheresModels index out of range. Index: \(72 + sphereIndex), Count: \(ReplateCameraView.spheresModels.count)")
1070
- callCompletion(false)
1071
- return
1067
+ if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
1068
+ callback = ReplateCameraController.completedUpperSpheresCallback
1069
+ ReplateCameraController.completedUpperSpheresCallback = nil
1070
+ }
1071
+ }
1072
+ } else if targetIndex == 0 {
1073
+ guard sphereIndex < ReplateCameraView.lowerSpheresSet.count else {
1074
+ print("[updateSpheres] Lower sphere index out of range")
1075
+ completion(false)
1076
+ return
1077
+ }
1078
+
1079
+ if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
1080
+ ReplateCameraView.lowerSpheresSet[sphereIndex] = true
1081
+ ReplateCameraView.photosFromDifferentAnglesTaken += 1
1082
+ newAngle = true
1083
+
1084
+ guard sphereIndex < ReplateCameraView.spheresModels.count else {
1085
+ print("[updateSpheres] Lower spheresModels index out of range")
1086
+ completion(false)
1087
+ return
1088
+ }
1089
+
1090
+ mesh = ReplateCameraView.spheresModels[sphereIndex]
1091
+
1092
+ if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
1093
+ callback = ReplateCameraController.completedLowerSpheresCallback
1094
+ ReplateCameraController.completedLowerSpheresCallback = nil
1095
+ }
1096
+ }
1072
1097
  }
1073
- mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
1074
1098
 
1075
- if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
1076
- callback = ReplateCameraController.completedUpperSpheresCallback
1077
- ReplateCameraController.completedUpperSpheresCallback = nil
1099
+ if let mesh = mesh {
1100
+ let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
1101
+ mesh.model?.materials[0] = material
1102
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
1078
1103
  }
1079
- }
1080
- } else if deviceTargetInFocus == 0 {
1081
- if sphereIndex >= ReplateCameraView.lowerSpheresSet.count {
1082
- print("[updateSpheres] Lower sphere index out of range. Index: \(sphereIndex), Count: \(ReplateCameraView.lowerSpheresSet.count)")
1083
- callCompletion(false)
1084
- return
1085
- }
1086
1104
 
1087
- if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
1088
- ReplateCameraView.lowerSpheresSet[sphereIndex] = true
1089
- ReplateCameraView.photosFromDifferentAnglesTaken += 1
1090
- newAngle = true
1105
+ callback?([])
1106
+ completion(newAngle)
1107
+ }
1091
1108
 
1092
- if sphereIndex >= ReplateCameraView.spheresModels.count {
1093
- print("[updateSpheres] Lower spheresModels index out of range. Index: \(sphereIndex), Count: \(ReplateCameraView.spheresModels.count)")
1094
- callCompletion(false)
1095
- return
1109
+ // MARK: - Anchor Validation
1110
+ func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
1111
+ var isValid = false
1112
+ if(Thread.isMainThread){
1113
+ let transform = anchorNode.transform
1114
+ let position = transform.translation
1115
+ let rotation = transform.rotation
1116
+ let scale = transform.scale
1117
+
1118
+ isValid = !position.isNaN &&
1119
+ !rotation.isNaN &&
1120
+ scale != SIMD3<Float>(0, 0, 0) &&
1121
+ abs(length(rotation.vector) - 1.0) < 0.0001
1122
+ }else{
1123
+ DispatchQueue.main.sync {
1124
+ let transform = anchorNode.transform
1125
+ let position = transform.translation
1126
+ let rotation = transform.rotation
1127
+ let scale = transform.scale
1128
+
1129
+ isValid = !position.isNaN &&
1130
+ !rotation.isNaN &&
1131
+ scale != SIMD3<Float>(0, 0, 0) &&
1132
+ abs(length(rotation.vector) - 1.0) < 0.0001
1096
1133
  }
1097
- mesh = ReplateCameraView.spheresModels[sphereIndex]
1134
+ }
1098
1135
 
1099
- if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
1100
- callback = ReplateCameraController.completedLowerSpheresCallback
1101
- ReplateCameraController.completedLowerSpheresCallback = nil
1136
+ return isValid
1137
+ }
1138
+
1139
+ // MARK: - Distance and Angle Calculations
1140
+ func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity) -> Int {
1141
+ var distance: Float = 0
1142
+
1143
+ if(Thread.isMainThread){
1144
+ let cameraPosition = SIMD3<Float>(
1145
+ cameraTransform.columns.3.x,
1146
+ cameraTransform.columns.3.y,
1147
+ cameraTransform.columns.3.z
1148
+ )
1149
+ let anchorPosition = anchorEntity.transform.translation
1150
+ distance = distanceBetween(cameraPosition, anchorPosition)
1151
+ }else{
1152
+ if(Thread.isMainThread){
1153
+ let cameraPosition = SIMD3<Float>(
1154
+ cameraTransform.columns.3.x,
1155
+ cameraTransform.columns.3.y,
1156
+ cameraTransform.columns.3.z
1157
+ )
1158
+ let anchorPosition = anchorEntity.transform.translation
1159
+ distance = distanceBetween(cameraPosition, anchorPosition)
1160
+ }else{
1161
+ DispatchQueue.main.sync {
1162
+ let cameraPosition = SIMD3<Float>(
1163
+ cameraTransform.columns.3.x,
1164
+ cameraTransform.columns.3.y,
1165
+ cameraTransform.columns.3.z
1166
+ )
1167
+ let anchorPosition = anchorEntity.transform.translation
1168
+ distance = distanceBetween(cameraPosition, anchorPosition)
1169
+ }
1102
1170
  }
1171
+
1103
1172
  }
1173
+ return distance <= Self.MIN_DISTANCE ? -1 :
1174
+ distance >= Self.MAX_DISTANCE ? 1 : 0
1104
1175
  }
1105
1176
 
1106
- DispatchQueue.main.async {
1107
- if let mesh = mesh {
1108
- let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
1109
- mesh.model?.materials[0] = material
1110
- ReplateCameraView.generateImpactFeedback(strength: .light)
1111
- }
1177
+ func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
1178
+ let difference = pos1 - pos2
1179
+ return sqrt(dot(difference, difference))
1112
1180
  }
1113
1181
 
1114
- // Ensure callback execution doesn't interfere with array access
1115
- callback?([])
1116
- callCompletion(newAngle)
1117
- }
1182
+ // MARK: - Static Utility Methods
1183
+ func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4 {
1184
+ var relativeTransform: simd_float4x4!
1185
+ if(Thread.isMainThread){
1186
+ let anchorTransform = anchor.transformMatrix(relativeTo: nil)
1187
+ relativeTransform = anchorTransform.inverse * cameraTransform
1188
+ }else{
1189
+ DispatchQueue.main.sync {
1190
+ let anchorTransform = anchor.transformMatrix(relativeTo: nil)
1191
+ relativeTransform = anchorTransform.inverse * cameraTransform
1192
+ }
1193
+ }
1194
+ return relativeTransform
1195
+ }
1118
1196
 
1119
- static func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4{
1120
- // Transform the camera position to the anchor's local space
1121
- let anchorTransform = anchor.transformMatrix(relativeTo: nil)
1122
- let relativePosition = anchorTransform.inverse * cameraTransform
1123
- return relativePosition
1124
- }
1197
+ func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
1198
+ var angleDegrees: Float = 0
1199
+ if(Thread.isMainThread){
1200
+ let anchorTransform = anchor.transform.matrix
1201
+ let anchorPositionXZ = simd_float2(
1202
+ anchor.transform.translation.x,
1203
+ anchor.transform.translation.z
1204
+ )
1205
+ let relativeCameraPositionXZ = simd_float2(
1206
+ cameraTransform.columns.3.x,
1207
+ cameraTransform.columns.3.z
1208
+ )
1209
+
1210
+ let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
1211
+ let anchorXAxisXZ = simd_float2(
1212
+ anchorTransform.columns.0.x,
1213
+ anchorTransform.columns.0.z
1214
+ )
1215
+
1216
+ let angle = atan2(directionXZ.y, directionXZ.x) -
1217
+ atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
1218
+
1219
+ angleDegrees = angle * (180.0 / .pi)
1220
+ if angleDegrees < 0 {
1221
+ angleDegrees += 360
1222
+ }
1223
+ }else{
1224
+ DispatchQueue.main.sync {
1225
+ let anchorTransform = anchor.transform.matrix
1226
+ let anchorPositionXZ = simd_float2(
1227
+ anchor.transform.translation.x,
1228
+ anchor.transform.translation.z
1229
+ )
1230
+ let relativeCameraPositionXZ = simd_float2(
1231
+ cameraTransform.columns.3.x,
1232
+ cameraTransform.columns.3.z
1233
+ )
1234
+
1235
+ let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
1236
+ let anchorXAxisXZ = simd_float2(
1237
+ anchorTransform.columns.0.x,
1238
+ anchorTransform.columns.0.z
1239
+ )
1240
+
1241
+ let angle = atan2(directionXZ.y, directionXZ.x) -
1242
+ atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
1243
+
1244
+ angleDegrees = angle * (180.0 / .pi)
1245
+ if angleDegrees < 0 {
1246
+ angleDegrees += 360
1247
+ }
1248
+ }
1249
+ }
1250
+ return angleDegrees
1251
+ }
1252
+
1253
+ func cgImage(from ciImage: CIImage) -> CGImage? {
1254
+ let context = CIContext(options: nil)
1255
+ return context.createCGImage(ciImage, from: ciImage.extent)
1256
+ }
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
+ }
1125
1265
 
1126
- static func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
1127
- // Extract the position of the anchor and the camera from their transforms, ignoring the y-axis
1128
- let anchorTransform = anchor.transform.matrix
1129
- let anchorPositionXZ = simd_float2(anchor.transform.translation.x, anchor.transform.translation.z)
1130
- let relativeCameraPositionXZ = simd_float2(cameraTransform.columns.3.x, cameraTransform.columns.3.z)
1266
+ let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
1267
+ let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
1268
+ let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
1131
1269
 
1132
- // Calculate the direction vector from the anchor to the camera in the XZ plane
1133
- let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
1270
+ guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
1271
+ return nil
1272
+ }
1134
1273
 
1135
- // Extract the x-axis of the anchor's transform in the XZ plane
1136
- let anchorXAxisXZ = simd_float2(anchorTransform.columns.0.x, anchorTransform.columns.0.z)
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
+ }
1137
1287
 
1138
- // Use atan2 to calculate the angle between the anchor's x-axis and the direction vector in the XZ plane
1139
- let angle = atan2(directionXZ.y, directionXZ.x) - atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
1288
+ CGImageDestinationAddImageFromSource(
1289
+ destination,
1290
+ source,
1291
+ 0,
1292
+ mutableMetadata as CFDictionary
1293
+ )
1140
1294
 
1141
- // Convert the angle to degrees
1142
- var angleDegrees = angle * (180.0 / .pi)
1295
+ guard CGImageDestinationFinalize(destination) else {
1296
+ return nil
1297
+ }
1143
1298
 
1144
- // Ensure the angle is within the range [0, 360)
1145
- if angleDegrees < 0 {
1146
- angleDegrees += 360
1299
+ return fileURL
1147
1300
  }
1148
1301
 
1149
- return angleDegrees
1150
- }
1302
+ func getCameraTransformString(from session: ARSession) -> String? {
1303
+ guard let currentFrame = session.currentFrame else {
1304
+ return nil
1305
+ }
1151
1306
 
1307
+ let transform = currentFrame.camera.transform
1308
+ return """
1309
+ \(transform.columns.0.x),\(transform.columns.0.y),\(transform.columns.0.z),\(transform.columns.0.w);
1310
+ \(transform.columns.1.x),\(transform.columns.1.y),\(transform.columns.1.z),\(transform.columns.1.w);
1311
+ \(transform.columns.2.x),\(transform.columns.2.y),\(transform.columns.2.z),\(transform.columns.2.w);
1312
+ \(transform.columns.3.x),\(transform.columns.3.y),\(transform.columns.3.z),\(transform.columns.3.w)
1313
+ """
1314
+ }
1152
1315
  }
1153
1316
 
1154
1317
  extension ARView: ARCoachingOverlayViewDelegate {
@@ -1167,7 +1330,7 @@ extension ARView: ARCoachingOverlayViewDelegate {
1167
1330
  // Set the delegate for any callbacks
1168
1331
  coachingOverlay.delegate = self
1169
1332
  coachingOverlay.setActive(true, animated: true)
1170
- ReplateCameraView.generateImpactFeedback(strength: .light)
1333
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
1171
1334
  let callback = ReplateCameraController.openedTutorialCallback
1172
1335
  if (callback != nil) {
1173
1336
  callback!([])
@@ -1179,15 +1342,13 @@ extension ARView: ARCoachingOverlayViewDelegate {
1179
1342
  public func coachingOverlayViewDidDeactivate(
1180
1343
  _ coachingOverlayView: ARCoachingOverlayView
1181
1344
  ) {
1182
- print("DEACTIVATED")
1183
1345
  let callback = ReplateCameraController.completedTutorialCallback
1184
1346
  if (callback != nil) {
1185
1347
  callback!([])
1186
1348
  ReplateCameraController.completedTutorialCallback = nil
1187
1349
  }
1188
- ReplateCameraView.generateImpactFeedback(strength: .heavy)
1189
- ReplateCameraView.addRecognizer()
1190
- print("CRASHED")
1350
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
1351
+ ReplateCameraView.addRecognizers()
1191
1352
  }
1192
1353
  }
1193
1354