replate-camera 0.9.0 → 0.10.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.
@@ -12,7 +12,6 @@ class ReplateCameraViewManager: RCTViewManager {
12
12
  override func view() -> ReplateCameraView {
13
13
  return ReplateCameraView()
14
14
  }
15
-
16
15
  @objc override static func requiresMainQueueSetup() -> Bool {
17
16
  return false
18
17
  }
@@ -28,690 +27,327 @@ extension UIImage {
28
27
 
29
28
  UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
30
29
  guard let context = UIGraphicsGetCurrentContext() else { return self }
31
-
32
- context.translateBy(x: newSize.width / 2, y: newSize.height / 2)
30
+ context.translateBy(x: newSize.width/2, y: newSize.height/2)
33
31
  context.rotate(by: CGFloat(radians))
34
- self.draw(in: CGRect(x: -self.size.width / 2,
35
- y: -self.size.height / 2,
32
+ self.draw(in: CGRect(x: -self.size.width/2,
33
+ y: -self.size.height/2,
36
34
  width: self.size.width,
37
35
  height: self.size.height))
38
-
39
36
  let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
40
37
  UIGraphicsEndImageContext()
41
-
42
38
  return newImage
43
39
  }
44
40
  }
45
41
 
42
+ // MARK: - Safe Array Access
43
+ extension Array {
44
+ subscript(safe index: Index) -> Element? {
45
+ return indices.contains(index) ? self[index] : nil
46
+ }
47
+ }
48
+
46
49
  // MARK: - ReplateCameraView
47
50
  class ReplateCameraView: UIView, ARSessionDelegate {
48
- // MARK: - Static Properties
49
- private static let lock = NSLock()
50
- private static let arQueue = DispatchQueue(label: "com.replate.ar", qos: .userInteractive)
51
-
52
- // AR View and Core Components
53
- static var arView: ARView!
54
- static var anchorEntity: AnchorEntity?
55
- static var model: Entity!
56
- static var sessionId: UUID!
57
- static var motionManager: CMMotionManager!
58
- static var gravityVector: [String : Double] = [:]
59
- static var INSTANCE: ReplateCameraView!
60
-
61
- // Scene Configuration
62
- static var width = CGFloat(0)
63
- static var height = CGFloat(0)
64
- static var isPaused = false
65
-
66
- // Sphere Properties
67
- static var spheresModels: [ModelEntity] = []
68
- static var upperSpheresSet = [Bool](repeating: false, count: 72)
69
- static var lowerSpheresSet = [Bool](repeating: false, count: 72)
70
- static var sphereRadius = Float(0.004)
71
- static var spheresRadius = Float(0.13)
72
- static var sphereAngle = Float(5)
73
- static var spheresHeight = Float(0.10)
74
- static var distanceBetweenCircles = Float(0.10)
75
-
76
- // Focus and Navigation
77
- static var focusModel: Entity!
78
- static var circleInFocus = 0
79
- static var dragSpeed = CGFloat(7000)
80
- static var dotAnchors: [AnchorEntity] = []
81
-
82
- // Statistics
83
- static var totalPhotosTaken = 0
84
- static var photosFromDifferentAnglesTaken = 0
85
-
86
- // Thread Safety
87
- private static var isResetting = false
88
-
89
- // MARK: - Initialization
90
- override init(frame: CGRect) {
91
- super.init(frame: frame)
92
- setupInitialState()
93
- }
94
-
95
- required init?(coder: NSCoder) {
96
- super.init(coder: coder)
97
- setupInitialState()
98
- }
99
-
100
- private func setupInitialState() {
101
- requestCameraPermissions()
102
- ReplateCameraView.INSTANCE = self
103
- }
104
-
105
- // MARK: - Layout
51
+ // MARK: Static
52
+ private static let lock = NSLock()
53
+ private static let arQueue = DispatchQueue(label: "com.replate.ar", qos: .userInteractive)
54
+ static var arView: ARView!
55
+ static var anchorEntity: AnchorEntity?
56
+ static var sessionId: UUID!
57
+ static var motionManager: CMMotionManager!
58
+ static var gravityVector: [String: Double] = [:]
59
+ static var INSTANCE: ReplateCameraView!
60
+ static var width: CGFloat = 0
61
+ static var height: CGFloat = 0
62
+
63
+ // Sphere state
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 spheresRadius: Float = 0.13
68
+ static var spheresHeight: Float = 0.10
69
+ static var distanceBetweenCircles: Float = 0.10
70
+ static var dotAnchors: [AnchorEntity] = []
71
+
72
+ // Gesture throttle
73
+ private var lastPinchTime: TimeInterval = 0
74
+
75
+ // Cached mesh + material
76
+ private static let baseSphereMesh = MeshResource.generateSphere(radius: 0.004)
77
+ private static let baseSphereMaterial = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
78
+
79
+ // MARK: Init
80
+ override init(frame: CGRect) {
81
+ super.init(frame: frame)
82
+ setupInitialState()
83
+ }
84
+ required init?(coder: NSCoder) {
85
+ super.init(coder: coder)
86
+ setupInitialState()
87
+ }
88
+ private func setupInitialState() {
89
+ requestCameraPermissions()
90
+ ReplateCameraView.INSTANCE = self
91
+ }
92
+
93
+ // MARK: Layout
106
94
  override func layoutSubviews() {
107
95
  super.layoutSubviews()
108
- Self.lock.lock()
109
- defer { Self.lock.unlock() }
110
-
96
+ Self.lock.lock(); defer { Self.lock.unlock() }
111
97
  ReplateCameraView.width = frame.width
112
98
  ReplateCameraView.height = frame.height
113
99
  }
114
100
 
115
- // MARK: - AR Setup and Configuration
101
+ // MARK: AR Setup
116
102
  static func setupAR() {
117
103
  arQueue.async {
118
- guard let instance = INSTANCE else { return }
119
-
120
- let configuration = ARWorldTrackingConfiguration()
121
- configuration.isLightEstimationEnabled = true
122
- configuration.planeDetection = .horizontal
123
-
104
+ guard let inst = INSTANCE else { return }
105
+ let config = ARWorldTrackingConfiguration()
106
+ config.isLightEstimationEnabled = true
107
+ config.planeDetection = .horizontal
124
108
  DispatchQueue.main.async {
125
- configureARView(configuration)
109
+ configureARView(config)
126
110
  addRecognizers()
127
111
  }
128
112
  }
129
113
  }
130
-
131
- private static func configureARView(_ configuration: ARWorldTrackingConfiguration) {
132
- configureRenderOptions()
133
- configureVideoFormat(configuration)
134
-
135
- arView.session.run(configuration)
114
+ private static func configureARView(_ config: ARWorldTrackingConfiguration) {
115
+ arView.session.run(config)
136
116
  arView.addCoaching()
137
117
  sessionId = arView.session.identifier
138
118
  }
139
-
140
- private static func configureRenderOptions() {
141
- let renderOptions: [ARView.RenderOptions] = [
142
- .disableMotionBlur,
143
- .disableCameraGrain,
144
- .disableAREnvironmentLighting,
145
- .disableHDR,
146
- .disableFaceMesh,
147
- .disableGroundingShadows,
148
- .disablePersonOcclusion
149
- ]
150
-
151
- renderOptions.forEach { arView.renderOptions.insert($0) }
152
- }
153
-
154
- private static func configureVideoFormat(_ configuration: ARWorldTrackingConfiguration) {
155
- if #available(iOS 16.0, *) {
156
- configuration.videoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing
157
- ?? ARWorldTrackingConfiguration.recommendedVideoFormatFor4KResolution
158
- ?? highestResolutionFormat()
159
- } else {
160
- configuration.videoFormat = highestResolutionFormat()
161
- }
162
- }
163
-
164
- private static func highestResolutionFormat() -> ARConfiguration.VideoFormat {
165
- return ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
166
- let resolution1 = format1.imageResolution.width * format1.imageResolution.height
167
- let resolution2 = format2.imageResolution.width * format2.imageResolution.height
168
- return resolution1 < resolution2
169
- })!
170
- }
171
-
172
- // MARK: - Gesture Recognition
173
119
  static func addRecognizers() {
174
- guard let instance = INSTANCE else { return }
175
-
176
- let recognizers = [
177
- UITapGestureRecognizer(target: instance, action: #selector(instance.viewTapped)),
178
- UIPanGestureRecognizer(target: instance, action: #selector(instance.handlePan)),
179
- UIPinchGestureRecognizer(target: instance, action: #selector(instance.handlePinch))
180
- ]
181
-
182
- recognizers.forEach { arView.addGestureRecognizer($0) }
183
- }
184
-
185
- @objc func viewTapped(_ recognizer: UITapGestureRecognizer) {
186
- print("VIEW TAPPED")
187
- let tapLocation: CGPoint = recognizer.location(in: ReplateCameraView.arView)
188
- let estimatedPlane: ARRaycastQuery.Target = .estimatedPlane
189
- let alignment: ARRaycastQuery.TargetAlignment = .horizontal
190
-
191
- let result: [ARRaycastResult] = ReplateCameraView.arView.raycast(from: tapLocation,
192
- allowing: estimatedPlane,
193
- alignment: alignment)
194
-
195
- guard let rayCast: ARRaycastResult = result.first
196
- else {
197
- return
198
- }
199
- let anchor = AnchorEntity(world: rayCast.worldTransform)
200
- print("ANCHOR FOUND\n", anchor.transform)
201
- let callback = ReplateCameraController.anchorSetCallback
202
- if (callback != nil) {
203
- callback!([])
204
- ReplateCameraController.anchorSetCallback = nil
205
- }
206
- if (ReplateCameraView.model == nil && ReplateCameraView.anchorEntity == nil) {
207
- DispatchQueue.main.async{
208
- for dot in ReplateCameraView.dotAnchors {
209
- dot.removeFromParent()
210
- ReplateCameraView.arView.scene.removeAnchor(dot)
211
- }
212
- ReplateCameraView.dotAnchors = []
213
- }
214
- ReplateCameraView.anchorEntity = anchor
215
- createSpheres(y: ReplateCameraView.spheresHeight)
216
- createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
217
- createFocusSphere()
218
- guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
219
- ReplateCameraView.arView.scene.anchors.append(anchorEntity)
220
- }
221
- }
222
-
223
- @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
224
- print("handle pan")
225
- guard let sceneView = gestureRecognizer.view as? ARView else {
226
- return
227
- }
228
- guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
229
- let cameraTransform = sceneView.cameraTransform.matrix // Get the 4x4 matrix
230
- print("passed guard")
231
-
232
- if gestureRecognizer.state == .changed {
233
- print("triggered")
234
- let translation = gestureRecognizer.translation(in: sceneView)
235
- print(translation)
236
-
237
- // Extract forward and right vectors from the camera transform matrix
238
- let forward = SIMD3<Float>(-cameraTransform.columns.2.x, 0, -cameraTransform.columns.2.z) // Assuming Y is up
239
- let right = SIMD3<Float>(cameraTransform.columns.0.x, 0, cameraTransform.columns.0.z) // Assuming Y is up
240
-
241
- // Normalize the vectors
242
- let forwardNormalized = normalize(forward)
243
- let rightNormalized = normalize(right)
244
-
245
- // Calculate the adjusted movement based on user input and camera orientation
246
- // Invert the vertical translation (y-axis)
247
- let adjustedMovement = SIMD3<Float>(
248
- x: Float(translation.x) * rightNormalized.x + Float(translation.y) * forwardNormalized.x,
249
- y: 0, // Assuming you want to keep the movement in the horizontal plane
250
- z: -Float(translation.x) * rightNormalized.z - Float(translation.y) * forwardNormalized.z // Invert the z movement
251
- )
252
-
253
- let initialPosition = anchorEntity.position
254
- ReplateCameraView.anchorEntity?.position = initialPosition + adjustedMovement / Float(ReplateCameraView.dragSpeed)
255
-
256
- gestureRecognizer.setTranslation(.zero, in: sceneView)
257
- }
258
- }
259
-
260
- @objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
261
- guard let sceneView = gestureRecognizer.view as? ARView else {
262
- return
263
- }
264
- switch gestureRecognizer.state {
265
- case .changed:
266
- // Ensure execution on the main thread
267
- DispatchQueue.main.async {
268
- // Calculate the scale based on the gesture recognizer's scale
269
- let scale = Float(gestureRecognizer.scale)
270
-
271
- // Ensure anchor entity is not nil before proceeding
272
- guard let anchorEntity = ReplateCameraView.anchorEntity else {
273
- print("[handlePinch] Anchor entity is nil.")
274
- return
275
- }
276
-
277
- // Remove all child entities safely
278
- ReplateCameraView.spheresModels.forEach { entity in
279
- anchorEntity.removeChild(entity)
280
- }
281
- if let focusModel = ReplateCameraView.focusModel {
282
- anchorEntity.removeChild(focusModel)
283
- }
284
-
285
- // Clear spheres models array
286
- ReplateCameraView.spheresModels = []
287
-
288
- // Update the scales
289
- ReplateCameraView.sphereRadius *= scale
290
- ReplateCameraView.spheresRadius *= scale
291
- ReplateCameraView.sphereAngle *= scale
292
-
293
- // Recreate spheres and the focus sphere
294
- self.createSpheres(y: ReplateCameraView.spheresHeight)
295
- self.createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
296
- self.createFocusSphere()
297
-
298
- // Update the material of the spheres based on their state
299
- for i in 0..<72 {
300
- let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
301
- if ReplateCameraView.upperSpheresSet[i] {
302
- if 72 + i < ReplateCameraView.spheresModels.count {
303
- let entity = ReplateCameraView.spheresModels[72 + i]
304
- entity.model?.materials[0] = material
305
- } else {
306
- print("[handlePinch] Upper sphere index out of bounds: \(72 + i)")
120
+ guard let inst = INSTANCE else { return }
121
+ let tap = UITapGestureRecognizer(target: inst, action: #selector(inst.viewTapped(_:)))
122
+ let pan = UIPanGestureRecognizer(target: inst, action: #selector(inst.handlePan(_:)))
123
+ let pinch = UIPinchGestureRecognizer(target: inst, action: #selector(inst.handlePinch(_:)))
124
+ [tap, pan, pinch].forEach { arView.addGestureRecognizer($0) }
125
+ }
126
+
127
+ // MARK: Gestures
128
+ @objc private func viewTapped(_ g: UITapGestureRecognizer) {
129
+ let loc = g.location(in: ReplateCameraView.arView)
130
+ let results = ReplateCameraView.arView.raycast(from: loc,
131
+ allowing: .estimatedPlane,
132
+ alignment: .horizontal)
133
+ guard let hit = results.first else { return }
134
+ let anchor = AnchorEntity(world: hit.worldTransform)
135
+ DispatchQueue.main.async {
136
+ ReplateCameraView.dotAnchors.forEach {
137
+ $0.removeFromParent()
138
+ ReplateCameraView.arView.scene.removeAnchor($0)
307
139
  }
308
- }
309
- if ReplateCameraView.lowerSpheresSet[i] {
310
- if i < ReplateCameraView.spheresModels.count {
311
- let entity = ReplateCameraView.spheresModels[i]
312
- entity.model?.materials[0] = material
313
- } else {
314
- print("[handlePinch] Lower sphere index out of bounds: \(i)")
140
+ ReplateCameraView.dotAnchors = []
141
+ ReplateCameraView.anchorEntity = anchor
142
+ createSpheres(y: spheresHeight)
143
+ createSpheres(y: distanceBetweenCircles + spheresHeight)
144
+ ReplateCameraView.arView.scene.anchors.append(anchor)
145
+ }
146
+ }
147
+
148
+ @objc private func handlePan(_ g: UIPanGestureRecognizer) {
149
+ guard let v = g.view as? ARView,
150
+ let a = ReplateCameraView.anchorEntity else { return }
151
+ if g.state == .changed {
152
+ let t = g.translation(in: v)
153
+ let m = v.cameraTransform.matrix
154
+ let forward = normalize(SIMD3(-m.columns.2.x, 0, -m.columns.2.z))
155
+ let right = normalize(SIMD3( m.columns.0.x, 0, m.columns.0.z))
156
+ let move = SIMD3<Float>(
157
+ Float( t.x)*right.x + Float( t.y)*forward.x,
158
+ 0,
159
+ -Float( t.x)*right.z - Float( t.y)*forward.z
160
+ ) / 7000
161
+ a.position += move
162
+ g.setTranslation(.zero, in: v)
163
+ }
164
+ }
165
+
166
+ @objc private func handlePinch(_ g: UIPinchGestureRecognizer) {
167
+ let now = CACurrentMediaTime()
168
+ guard now - lastPinchTime > 0.05, let a = ReplateCameraView.anchorEntity else { return }
169
+ lastPinchTime = now
170
+ if g.state == .changed {
171
+ let s = Float(g.scale)
172
+ a.scale *= SIMD3(repeating: s)
173
+ g.scale = 1.0
174
+ }
175
+ }
176
+
177
+ // MARK: Sphere Creation
178
+ private static func createSpheres(y: Float) {
179
+ guard let anchor = anchorEntity else { return }
180
+ var batch: [ModelEntity] = []
181
+ for i in 0..<72 {
182
+ let angle = Float(i) * (Float.pi/36)
183
+ let pos = SIMD3(
184
+ spheresRadius * cos(angle),
185
+ y,
186
+ spheresRadius * sin(angle)
187
+ )
188
+ let sphere = ModelEntity(mesh: baseSphereMesh,
189
+ materials: [baseSphereMaterial])
190
+ sphere.position = pos
191
+ batch.append(sphere)
192
+ }
193
+ spheresModels.append(contentsOf: batch)
194
+ batch.forEach { anchor.addChild($0) }
195
+ }
196
+
197
+ // MARK: Dot Grid
198
+ func addDots(to planeAnchor: ARPlaneAnchor) {
199
+ let center = planeAnchor.center
200
+ let extent = planeAnchor.extent
201
+ let spacing: Float = 0.05
202
+ var positions: [SIMD3<Float>] = []
203
+ for x in stride(from: -extent.x/2, through: extent.x/2, by: spacing) {
204
+ for z in stride(from: -extent.z/2, through: extent.z/2, by: spacing) {
205
+ positions.append(SIMD3(center.x + x, 0, center.z + z))
315
206
  }
316
- }
317
207
  }
318
-
319
- // Reset the gesture recognizer's scale to 1 to avoid cumulative scaling
320
- gestureRecognizer.scale = 1.0
321
- }
322
- default:
323
- break
324
- }
325
- }
326
-
327
- // MARK: - Entity Management
328
-
329
- func createFocusSphere() {
330
- DispatchQueue.main.async {
331
- let sphereRadius = ReplateCameraView.sphereRadius * 1.5
332
-
333
- // Generate the first sphere mesh
334
- let sphereMesh1 = MeshResource.generateSphere(radius: sphereRadius)
335
-
336
- // Create the first sphere entity with initial material
337
- let sphereEntity1 = ModelEntity(mesh: sphereMesh1, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
338
-
339
- // Set the position for the first sphere entity
340
- sphereEntity1.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight, z: 0)
341
-
342
- // Generate the second sphere mesh
343
- let sphereMesh2 = MeshResource.generateSphere(radius: sphereRadius)
344
-
345
- // Create the second sphere entity with initial material
346
- let sphereEntity2 = ModelEntity(mesh: sphereMesh2, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
347
-
348
- // Set the position for the second sphere entity
349
- sphereEntity2.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight + ReplateCameraView.distanceBetweenCircles, z: 0)
350
-
351
-
352
- // Update the material of the sphere entities
353
- sphereEntity1.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
354
- sphereEntity2.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
355
-
356
- let baseOverlayEntity = self.loadModel(named: "center.obj")
357
- baseOverlayEntity.scale *= 12
358
- baseOverlayEntity.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.3), roughness: 1, isMetallic: false),
359
- SimpleMaterial(color: .white.withAlphaComponent(0.7), roughness: 1, isMetallic: false),
360
- SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)]
361
-
362
- // Create a parent entity to hold both spheres
363
- let parentEntity = Entity()
364
- parentEntity.addChild(sphereEntity1)
365
- parentEntity.addChild(sphereEntity2)
366
- parentEntity.addChild(baseOverlayEntity)
367
-
368
- // Set the focus model for the global state
369
- ReplateCameraView.focusModel = parentEntity
370
-
371
- // Safely add the parent entity to the anchor entity
372
- ReplateCameraView.anchorEntity?.addChild(parentEntity)
373
- }
374
- }
375
-
376
- func createSpheres(y: Float) {
377
- DispatchQueue.main.async {
378
- let radius = ReplateCameraView.spheresRadius
379
-
380
- for i in 0..<72 {
381
- let angle = Float(i) * (Float.pi / 180) * 5
382
- let position = SIMD3(
383
- radius * cos(angle),
384
- y,
385
- radius * sin(angle)
386
- )
387
-
388
- let sphere = self.createSphere(position: position)
389
- ReplateCameraView.spheresModels.append(sphere)
390
- ReplateCameraView.anchorEntity?.addChild(sphere)
208
+ DispatchQueue.main.asyncAfter(deadline: .now()+1.0) {
209
+ for pos in positions {
210
+ let dotAnchor = AnchorEntity(world: planeAnchor.transform)
211
+ let dot = self.createDot(at: pos)
212
+ dotAnchor.addChild(dot)
213
+ ReplateCameraView.arView.scene.addAnchor(dotAnchor)
214
+ ReplateCameraView.dotAnchors.append(dotAnchor)
391
215
  }
392
216
  }
393
217
  }
394
-
395
- func createSphere(position: SIMD3<Float>) -> ModelEntity {
396
- let sphereMesh = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius)
397
- let material = SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)
398
- let sphere = ModelEntity(mesh: sphereMesh, materials: [material])
399
- sphere.position = position
400
- return sphere
218
+ func createDot(at position: SIMD3<Float>) -> ModelEntity {
219
+ let size: Float = 0.005
220
+ let mesh = MeshResource.generateBox(size: [size, 0.0001, size], cornerRadius: size/2)
221
+ let mat = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
222
+ let e = ModelEntity(mesh: mesh, materials: [mat])
223
+ e.position = position
224
+ return e
401
225
  }
402
226
 
403
- func addDots(to planeAnchor: ARPlaneAnchor) {
404
- print("Adding dots to plane anchor") // Debugging line
405
- let center = planeAnchor.center
406
- let extent = planeAnchor.extent
407
-
408
- var dotPositions: [SIMD3<Float>] = []
409
- let dotSpacing: Float = 0.05 // Adjust the spacing of dots (smaller value for more dots)
410
-
411
- for x in stride(from: -extent.x / 2, through: extent.x / 2, by: dotSpacing) {
412
- for z in stride(from: -extent.z / 2, through: extent.z / 2, by: dotSpacing) {
413
- let position = SIMD3<Float>(x + center.x, 0, z + center.z)
414
- dotPositions.append(position)
415
- }
416
- }
417
-
418
- DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: DispatchTimeInterval.milliseconds(1000)), execute: {
419
- // Add the dots to the ARView
420
- for position in dotPositions {
421
- let dotAnchor = AnchorEntity(world: planeAnchor.transform)
422
- let dot = self.createDot(at: position) // Assuming you're using the circle function
423
- dot.position.y = 0 // Ensure the dot position matches the plane's height
424
- dotAnchor.addChild(dot)
425
- ReplateCameraView.arView.scene.addAnchor(dotAnchor)
426
- ReplateCameraView.dotAnchors.append(dotAnchor)
427
- }
428
- })
429
- }
430
-
431
-
432
- func createDot(at position: SIMD3<Float>) -> ModelEntity {
433
- // Define the dimensions of the box
434
- let width: Float = 0.005 // 1 cm width
435
- let height: Float = 0.0001 // Very small height to make it almost flat
436
- let depth: Float = width // 1 cm depth
437
- let cornerRadius: Float = width/2.0 // Half of the width to make it look like a circle
438
-
439
- // Generate a box with rounded corners
440
- let cylinderMesh = MeshResource.generateBox(size: [width, height, depth], cornerRadius: 1)
441
-
442
- // Create the material
443
- let material = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
444
-
445
- // Create the entity
446
- let circleEntity = ModelEntity(mesh: cylinderMesh, materials: [material])
447
- circleEntity.position = position
448
- return circleEntity
449
- }
450
-
451
- // MARK: - Reset Functionality
227
+ // MARK: Reset
452
228
  @objc static func reset() {
453
229
  arQueue.async {
454
- Self.lock.lock()
455
- defer { Self.lock.unlock() }
456
-
457
- if isResetting { return }
458
- isResetting = true
459
-
230
+ lock.lock(); defer { lock.unlock() }
460
231
  DispatchQueue.main.async {
461
- tearDownARSession()
462
- resetProperties()
463
- setupNewARView()
464
- isResetting = false
232
+ arView.session.pause()
233
+ arView.session.delegate = nil
234
+ arView.scene.anchors.removeAll()
235
+ arView.removeFromSuperview()
236
+ anchorEntity = nil
237
+ spheresModels.removeAll()
238
+ upperSpheresSet = [Bool](repeating: false, count: 72)
239
+ lowerSpheresSet = [Bool](repeating: false, count: 72)
240
+ dotAnchors.removeAll()
241
+ INSTANCE.setupNewARView()
465
242
  }
466
243
  }
467
244
  }
468
-
469
- private static func tearDownARSession() {
470
- arView?.session.pause()
471
- arView?.session.delegate = nil
472
- arView?.scene.anchors.removeAll()
473
- arView?.removeFromSuperview()
474
- arView?.window?.resignKey()
475
- }
476
-
477
- private static func resetProperties() {
478
- anchorEntity = nil
479
- model = nil
480
- spheresModels.removeAll()
481
- upperSpheresSet = [Bool](repeating: false, count: 72)
482
- lowerSpheresSet = [Bool](repeating: false, count: 72)
483
- totalPhotosTaken = 0
484
- photosFromDifferentAnglesTaken = 0
485
- sphereRadius = 0.004
486
- spheresRadius = 0.13
487
- sphereAngle = 5
488
- spheresHeight = 0.10
489
- dragSpeed = 7000
490
- dotAnchors.removeAll()
491
- gravityVector = [:]
492
- }
493
-
494
- private static func setupNewARView() {
495
- guard let instance = INSTANCE else { return }
496
-
497
- arView = ARView(frame: CGRect(x: 0, y: 0, width: width, height: height))
498
- arView.backgroundColor = instance.hexStringToUIColor(hexColor: "#32a852")
499
- instance.addSubview(arView)
500
- arView.session.delegate = instance
501
- motionManager = CMMotionManager()
502
- if motionManager.isDeviceMotionAvailable {
503
- ReplateCameraView.INSTANCE.startDeviceMotionUpdates()
504
- } else {
505
- print("Device motion is not available")
245
+ private func setupNewARView() {
246
+ ReplateCameraView.arView = ARView(frame: bounds)
247
+ addSubview(ReplateCameraView.arView)
248
+ ReplateCameraView.arView.session.delegate = self
249
+ ReplateCameraView.motionManager = CMMotionManager()
250
+ if ReplateCameraView.motionManager.isDeviceMotionAvailable {
251
+ startDeviceMotionUpdates()
506
252
  }
507
- setupAR()
253
+ ReplateCameraView.setupAR()
508
254
  }
509
255
 
510
- // MARK: - Utilities functions
511
- func requestCameraPermissions() {
512
-
513
- if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
514
- print("Camera permissions already granted")
515
- } else {
516
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
517
- if granted {
518
- print("Camera permissions granted")
519
- } else {
520
- print("Camera permissions denied")
256
+ // MARK: Permissions & Motion
257
+ func requestCameraPermissions() {
258
+ if AVCaptureDevice.authorizationStatus(for: .video) != .authorized {
259
+ AVCaptureDevice.requestAccess(for: .video) { _ in }
521
260
  }
522
- })
523
- }
524
- }
525
-
526
- func loadModel(named name: String) -> ModelEntity {
527
- do{
528
- return try ModelEntity.loadModel(named: name)
529
- }catch{
530
- print("Cannot load model \(name)")
531
- let baseOverlayMesh = MeshResource.generateBox(size: [ReplateCameraView.spheresRadius * 2, 0.01, ReplateCameraView.spheresRadius * 2], cornerRadius: 1)
532
- let baseOverlayEntity = ModelEntity(mesh: baseOverlayMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)])
533
-
534
- baseOverlayEntity.position = SIMD3(x: 0, y: 0.01, z: 0)
535
- return baseOverlayEntity
536
- }
537
- }
538
-
539
- @objc var color: String = "" {
540
- didSet {
541
- self.backgroundColor = hexStringToUIColor(hexColor: color)
542
- }
543
- }
544
-
545
- func hexStringToUIColor(hexColor: String) -> UIColor {
546
- let stringScanner = Scanner(string: hexColor)
547
-
548
- if (hexColor.hasPrefix("#")) {
549
- stringScanner.scanLocation = 1
550
261
  }
551
- var color: UInt32 = 0
552
- stringScanner.scanHexInt32(&color)
553
-
554
- let r = CGFloat(Int(color >> 16) & 0x000000FF)
555
- let g = CGFloat(Int(color >> 8) & 0x000000FF)
556
- let b = CGFloat(Int(color) & 0x000000FF)
557
-
558
- return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: 1)
559
- }
560
-
561
- internal func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
562
- print("Planes detected: \(anchors.count)") // Debugging line
563
- for anchor in anchors {
564
- if let planeAnchor = anchor as? ARPlaneAnchor {
565
- print("Adding dots to plane")
566
- if (ReplateCameraView.spheresModels.isEmpty && ReplateCameraView.dotAnchors.isEmpty) {
567
- addDots(to: planeAnchor)
262
+ func startDeviceMotionUpdates() {
263
+ ReplateCameraView.motionManager.deviceMotionUpdateInterval = 0.1
264
+ ReplateCameraView.motionManager.startDeviceMotionUpdates(to: .main) { m, _ in
265
+ if let g = m?.gravity {
266
+ ReplateCameraView.gravityVector = ["x": g.x, "y": g.y, "z": g.z]
267
+ }
568
268
  }
569
- }
570
- }
571
- }
572
-
573
- func session(_ session: ARSession, didUpdate frame: ARFrame) {
574
- // Handle AR frame updates
575
- // You can perform actions here, such as updating the AR content based on the camera frame
576
- }
577
-
578
- func sessionWasInterrupted(_ session: ARSession) {
579
- print("SESSION INTERRUPTED")
580
- ReplateCameraView.motionManager.stopDeviceMotionUpdates()
581
- }
582
-
583
- func sessionInterruptionEnded(_ session: ARSession) {
584
- print("SESSION RESUMED")
585
- ReplateCameraView.INSTANCE.startDeviceMotionUpdates()
586
- }
587
-
588
- func generateImpactFeedback(strength: UIImpactFeedbackGenerator.FeedbackStyle) {
589
- do{
590
- let impactFeedbackGenerator = try UIImpactFeedbackGenerator(style: strength)
591
- impactFeedbackGenerator.prepare()
592
- impactFeedbackGenerator.impactOccurred()
593
- }catch{
594
- print("Error when sending feedback")
595
- }
596
- }
597
-
598
- func startDeviceMotionUpdates() {
599
- ReplateCameraView.motionManager.deviceMotionUpdateInterval = 0.1 // Update interval in seconds
600
- ReplateCameraView.motionManager.startDeviceMotionUpdates(to: .main) { (deviceMotion, error) in
601
- if let deviceMotion = deviceMotion {
602
- let gravity = deviceMotion.gravity
603
- ReplateCameraView.gravityVector = [
604
- "x": gravity.x,
605
- "y": gravity.y,
606
- "z": gravity.z
607
- ]
608
- print("Gravity vector: x = \(gravity.x), y = \(gravity.y), z = \(gravity.z)")
609
- }
610
269
  }
611
- }
612
-
613
270
  }
614
271
 
615
-
616
-
617
- // MARK: - Supporting Types
272
+ // MARK: - ARError
618
273
  enum ARError: Error {
619
- case noAnchor
620
- case invalidAnchor
621
- case notInFocus
622
- case captureError
623
- case tooManyImages
624
- case processingError
625
- case savingError
626
- case transformError
627
- case lightingError
628
- case notInRange
629
- case unknown
630
-
274
+ case noAnchor, invalidAnchor, notInFocus, captureError
275
+ case tooManyImages, processingError, savingError
276
+ case transformError, lightingError, notInRange, unknown
631
277
  var localizedDescription: String {
632
278
  switch self {
633
279
  case .noAnchor: return "[ReplateCameraController] No anchor set yet"
634
280
  case .invalidAnchor: return "[ReplateCameraController] AnchorNode is not valid"
635
281
  case .notInFocus: return "[ReplateCameraController] Object not in focus"
636
282
  case .captureError: return "[ReplateCameraController] Error capturing image"
637
- case .tooManyImages: return "[ReplateCameraController] Too many images and the last one's not from a new angle"
283
+ case .tooManyImages: return "[ReplateCameraController] Too many images / angle repeat"
638
284
  case .processingError: return "[ReplateCameraController] Error processing image"
639
285
  case .savingError: return "[ReplateCameraController] Error saving photo"
640
- case .transformError: return "[ReplateCameraController] Camera transform data not available"
286
+ case .transformError: return "[ReplateCameraController] Camera transform unavailable"
641
287
  case .lightingError: return "[ReplateCameraController] Image too dark"
642
288
  case .notInRange: return "[ReplateCameraController] Camera not in range"
643
- case .unknown: return "[ReplateCameraController] Unknown error occurred"
289
+ case .unknown: return "[ReplateCameraController] Unknown error"
644
290
  }
645
291
  }
646
292
  }
647
293
 
294
+ // MARK: - DeviceTargetInfo
648
295
  struct DeviceTargetInfo {
649
296
  let isInFocus: Bool
650
297
  let targetIndex: Int
651
298
  let transform: simd_float4x4
652
299
  let cameraPosition: SIMD3<Float>
653
300
  let deviceDirection: SIMD3<Float>
654
-
655
- var isValidTarget: Bool {
656
- return targetIndex != -1
657
- }
301
+ let distance: Float
302
+ var isValidTarget: Bool { targetIndex != -1 }
658
303
  }
659
304
 
305
+ // MARK: - SafeCallbackHandler
660
306
  class SafeCallbackHandler {
661
307
  private let resolver: RCTPromiseResolveBlock
662
308
  private let rejecter: RCTPromiseRejectBlock
663
- var hasCalledBack = false
309
+ private var hasCalledBack = false
664
310
  private let lock = NSLock()
665
-
666
- init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
311
+ init(resolver: @escaping RCTPromiseResolveBlock,
312
+ rejecter: @escaping RCTPromiseRejectBlock) {
667
313
  self.resolver = resolver
668
314
  self.rejecter = rejecter
669
315
  }
670
-
671
316
  func resolve(_ result: Any) {
672
- lock.lock()
673
- defer { lock.unlock() }
674
- guard !hasCalledBack else {
675
- print("rejecter: Callback already invoked.")
676
- return
677
- }
317
+ lock.lock(); defer { lock.unlock() }
318
+ guard !hasCalledBack else { return }
678
319
  hasCalledBack = true
679
320
  resolver(result)
680
321
  }
681
-
682
322
  func reject(_ error: ARError) {
683
- lock.lock()
684
- defer { lock.unlock() }
685
-
686
- guard !hasCalledBack else {
687
- print("rejecter: Callback already invoked.")
688
- return
689
- }
323
+ lock.lock(); defer { lock.unlock() }
324
+ guard !hasCalledBack else { return }
690
325
  hasCalledBack = true
691
- rejecter(
692
- String(describing: error),
693
- error.localizedDescription,
694
- NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
695
- )
326
+ rejecter(String(describing: error),
327
+ error.localizedDescription,
328
+ NSError(domain: "ReplateCameraController", code: 0))
696
329
  }
697
330
  }
698
331
 
699
- // MARK: - Main Controller
332
+ // MARK: - ReplateCameraController
700
333
  @objc(ReplateCameraController)
701
334
  class ReplateCameraController: NSObject {
702
- // MARK: - Static Properties
703
335
  private static let lock = NSLock()
704
336
  private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
337
+ private static let imageProcessingQueue = DispatchQueue(label: "com.replate.ar.image", qos: .userInitiated, attributes: .concurrent)
705
338
 
706
- // Configuration Constants
707
339
  private static let MIN_DISTANCE: Float = 0.25
708
340
  private static let MAX_DISTANCE: Float = 0.55
709
341
  private static let ANGLE_THRESHOLD: Float = 0.6
710
- // private static let TARGET_IMAGE_SIZE = CGSize(width: 2048, height: 1556)
711
342
  private static let TARGET_IMAGE_SIZE = CGSize(width: 3072, height: 2304)
712
343
  private static let MIN_AMBIENT_INTENSITY: CGFloat = 650
713
344
 
714
- // Callbacks
345
+ private var lastCaptureTime: TimeInterval = 0
346
+ private var captureCount: Int = 0
347
+ private lazy var imageProcessingContext: CIContext = {
348
+ CIContext(options: [.useSoftwareRenderer: false, .priorityRequestLow: true])
349
+ }()
350
+
715
351
  static var completedTutorialCallback: RCTResponseSenderBlock?
716
352
  static var anchorSetCallback: RCTResponseSenderBlock?
717
353
  static var completedUpperSpheresCallback: RCTResponseSenderBlock?
@@ -720,846 +356,522 @@ class ReplateCameraController: NSObject {
720
356
  static var tooCloseCallback: RCTResponseSenderBlock?
721
357
  static var tooFarCallback: RCTResponseSenderBlock?
722
358
 
723
- // MARK: - Callback Registration Methods
724
- @objc(registerOpenedTutorialCallback:)
725
- func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
726
- Self.lock.lock()
727
- defer { Self.lock.unlock() }
728
- ReplateCameraController.openedTutorialCallback = callback
359
+ override init() {
360
+ super.init()
729
361
  }
730
362
 
731
- @objc(registerCompletedTutorialCallback:)
732
- func registerCompletedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
733
- Self.lock.lock()
734
- defer { Self.lock.unlock() }
735
- ReplateCameraController.completedTutorialCallback = callback
363
+ // MARK: Callback Registration
364
+ @objc func registerOpenedTutorialCallback(_ cb: @escaping RCTResponseSenderBlock) {
365
+ Self.lock.lock(); defer { Self.lock.unlock() }
366
+ Self.openedTutorialCallback = cb
736
367
  }
737
-
738
- @objc(registerAnchorSetCallback:)
739
- func registerAnchorSetCallback(_ callback: @escaping RCTResponseSenderBlock) {
740
- Self.lock.lock()
741
- defer { Self.lock.unlock() }
742
- ReplateCameraController.anchorSetCallback = callback
368
+ @objc func registerCompletedTutorialCallback(_ cb: @escaping RCTResponseSenderBlock) {
369
+ Self.lock.lock(); defer { Self.lock.unlock() }
370
+ Self.completedTutorialCallback = cb
743
371
  }
744
-
745
- @objc(registerCompletedUpperSpheresCallback:)
746
- func registerCompletedUpperSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
747
- Self.lock.lock()
748
- defer { Self.lock.unlock() }
749
- ReplateCameraController.completedUpperSpheresCallback = callback
372
+ @objc func registerAnchorSetCallback(_ cb: @escaping RCTResponseSenderBlock) {
373
+ Self.lock.lock(); defer { Self.lock.unlock() }
374
+ Self.anchorSetCallback = cb
750
375
  }
751
-
752
- @objc(registerCompletedLowerSpheresCallback:)
753
- func registerCompletedLowerSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
754
- Self.lock.lock()
755
- defer { Self.lock.unlock() }
756
- ReplateCameraController.completedLowerSpheresCallback = callback
376
+ @objc func registerCompletedUpperSpheresCallback(_ cb: @escaping RCTResponseSenderBlock) {
377
+ Self.lock.lock(); defer { Self.lock.unlock() }
378
+ Self.completedUpperSpheresCallback = cb
757
379
  }
758
-
759
- @objc(registerTooCloseCallback:)
760
- func registerTooCloseCallback(_ callback: @escaping RCTResponseSenderBlock) {
761
- Self.lock.lock()
762
- defer { Self.lock.unlock() }
763
- ReplateCameraController.tooCloseCallback = callback
380
+ @objc func registerCompletedLowerSpheresCallback(_ cb: @escaping RCTResponseSenderBlock) {
381
+ Self.lock.lock(); defer { Self.lock.unlock() }
382
+ Self.completedLowerSpheresCallback = cb
764
383
  }
765
-
766
- @objc(registerTooFarCallback:)
767
- func registerTooFarCallback(_ callback: @escaping RCTResponseSenderBlock) {
768
- Self.lock.lock()
769
- defer { Self.lock.unlock() }
770
- ReplateCameraController.tooFarCallback = callback
384
+ @objc func registerTooCloseCallback(_ cb: @escaping RCTResponseSenderBlock) {
385
+ Self.lock.lock(); defer { Self.lock.unlock() }
386
+ Self.tooCloseCallback = cb
771
387
  }
772
-
773
- // MARK: - Public Methods
774
- @objc(getPhotosCount:rejecter:)
775
- func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
776
- Self.lock.lock()
777
- let count = ReplateCameraView.totalPhotosTaken
778
- Self.lock.unlock()
779
- resolver(count)
388
+ @objc func registerTooFarCallback(_ cb: @escaping RCTResponseSenderBlock) {
389
+ Self.lock.lock(); defer { Self.lock.unlock() }
390
+ Self.tooFarCallback = cb
780
391
  }
781
392
 
782
- @objc(isScanComplete:rejecter:)
783
- func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
784
- Self.lock.lock()
785
- let isComplete = ReplateCameraView.photosFromDifferentAnglesTaken == 144
786
- Self.lock.unlock()
787
- resolver(isComplete)
393
+ // MARK: Status Queries
394
+ @objc func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
395
+ Self.lock.lock(); let c = ReplateCameraView.totalPhotosTaken; Self.lock.unlock()
396
+ resolver(c)
788
397
  }
789
-
790
- @objc(getRemainingAnglesToScan:rejecter:)
791
- func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
792
- Self.lock.lock()
793
- let remaining = 144 - ReplateCameraView.photosFromDifferentAnglesTaken
794
- Self.lock.unlock()
795
- resolver(remaining)
398
+ @objc func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
399
+ Self.lock.lock(); let done = ReplateCameraView.photosFromDifferentAnglesTaken == 144; Self.lock.unlock()
400
+ resolver(done)
796
401
  }
797
-
798
- @objc
799
- func reset() {
800
- DispatchQueue.main.async {
801
- ReplateCameraView.reset()
802
- }
402
+ @objc func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
403
+ Self.lock.lock(); let rem = 144 - ReplateCameraView.photosFromDifferentAnglesTaken; Self.lock.unlock()
404
+ resolver(rem)
405
+ }
406
+ @objc func reset() {
407
+ DispatchQueue.main.async { ReplateCameraView.reset() }
803
408
  }
804
409
 
805
- // MARK: - Photo Capture and Processing
806
- @objc(takePhoto:resolver:rejecter:)
807
- func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
410
+ // MARK: Photo Capture
411
+ @objc func takePhoto(_ unlimited: Bool = false,
412
+ resolver: @escaping RCTPromiseResolveBlock,
413
+ rejecter: @escaping RCTPromiseRejectBlock) {
808
414
  Self.arQueue.async { [weak self] in
809
415
  guard let self = self else { return }
810
-
811
- let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
812
-
416
+ let cb = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
417
+
418
+ var pixelBuffer: CVPixelBuffer?
419
+ var camTransform: simd_float4x4?
420
+ var lightEst: ARLightEstimate?
421
+ DispatchQueue.main.sync {
422
+ if let frame = ReplateCameraView.arView.session.currentFrame {
423
+ pixelBuffer = frame.capturedImage
424
+ camTransform = frame.camera.transform
425
+ lightEst = frame.lightEstimate
426
+ }
427
+ }
428
+ guard let buf = pixelBuffer, let camT = camTransform else {
429
+ cb.reject(.captureError); return
430
+ }
813
431
  do {
814
- try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
815
- } catch let error as ARError {
816
- callbackHandler.reject(error)
432
+ try self.validateAndProcessPhotoWithData(
433
+ pixelBuffer: buf,
434
+ transform: camT,
435
+ lightEstimate: lightEst,
436
+ unlimited: unlimited,
437
+ callback: cb
438
+ )
439
+ } catch let e as ARError {
440
+ cb.reject(e)
817
441
  } catch {
818
- callbackHandler.reject(.unknown)
442
+ cb.reject(.unknown)
819
443
  }
820
444
  }
821
445
  }
822
446
 
823
- private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
824
- let anchorEntity = try getValidAnchorEntity()
825
- let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
826
-
827
- if deviceTargetInfo.isValidTarget {
828
- try processTargetedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
829
- } else {
830
- throw ARError.notInFocus
831
- }
832
- }
833
-
834
- private func getValidAnchorEntity() throws -> AnchorEntity {
835
- var anchorEntity: AnchorEntity?
836
- if(Thread.isMainThread){
837
- anchorEntity = ReplateCameraView.anchorEntity
838
-
839
- }else{
840
- DispatchQueue.main.sync {
841
- anchorEntity = ReplateCameraView.anchorEntity
447
+ private func validateAndProcessPhotoWithData(pixelBuffer: CVPixelBuffer,
448
+ transform camT: simd_float4x4,
449
+ lightEstimate le: ARLightEstimate?,
450
+ unlimited: Bool,
451
+ callback cb: SafeCallbackHandler) throws {
452
+ let anchor = try getValidAnchorEntity()
453
+ let info = try getDeviceTargetInfoFromTransform(anchorEntity: anchor,
454
+ cameraTransform: camT)
455
+ guard info.isValidTarget else { throw ARError.notInFocus }
456
+ if let light = le, light.ambientIntensity < Self.MIN_AMBIENT_INTENSITY {
457
+ throw ARError.lightingError
842
458
  }
843
- }
844
-
845
-
846
- guard let anchor = anchorEntity else {
847
- throw ARError.noAnchor
848
- }
849
-
850
- guard isAnchorNodeValid(anchor) else {
851
- throw ARError.invalidAnchor
852
- }
853
-
854
- return anchor
855
- }
856
-
857
- private func getDeviceTargetInfo(anchorEntity: AnchorEntity) throws -> DeviceTargetInfo {
858
- guard let frame = ReplateCameraView.arView.session.currentFrame else {
859
- throw ARError.transformError
459
+ if info.distance < Self.MIN_DISTANCE {
460
+ DispatchQueue.main.async {
461
+ Self.tooCloseCallback?([]); Self.tooCloseCallback = nil
462
+ }
463
+ throw ARError.notInRange
860
464
  }
861
-
862
- let cameraTransform = frame.camera.transform
863
- let relativeCameraTransform = getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
864
-
865
- var anchorPosition: SIMD3<Float>!
866
- if(Thread.isMainThread){
867
- anchorPosition = anchorEntity.position(relativeTo: nil)
868
-
869
- }else{
870
- DispatchQueue.main.sync {
871
- anchorPosition = anchorEntity.position(relativeTo: nil)
465
+ if info.distance > Self.MAX_DISTANCE {
466
+ DispatchQueue.main.async {
467
+ Self.tooFarCallback?([]); Self.tooFarCallback = nil
468
+ }
469
+ throw ARError.notInRange
470
+ }
471
+
472
+ // compute sphereIndex & newAngle
473
+ let angle = angleBetweenAnchorXAndCamera(anchor: anchor, cameraTransform: camT)
474
+ let sphereIndex = max(Int(round(angle/5.0)),0) % 72
475
+ let newAngle = (info.targetIndex==1
476
+ ? !ReplateCameraView.upperSpheresSet[sphereIndex]
477
+ : !ReplateCameraView.lowerSpheresSet[sphereIndex])
478
+
479
+ if !unlimited && !newAngle { throw ARError.tooManyImages }
480
+
481
+ Self.imageProcessingQueue.async {
482
+ self.processAndSaveImageConcurrently(buf, callback: cb) { success in
483
+ guard success else { return }
484
+ DispatchQueue.main.async {
485
+ self.updateCircleFocus(targetIndex: info.targetIndex)
486
+ self.updateSpheresAfterCapture(
487
+ targetIndex: info.targetIndex,
488
+ sphereIndex: sphereIndex,
489
+ isNewAngle: newAngle
490
+ )
491
+ }
492
+ }
872
493
  }
873
- }
874
-
875
-
876
- let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
877
- let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
878
- let directionToAnchor = normalize(anchorPosition - cameraPosition)
879
- let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
880
-
881
- let targetIndex = determineTargetIndex(angleToAnchor: angleToAnchor, relativeCameraTransform: relativeCameraTransform)
882
-
883
- return DeviceTargetInfo(
884
- isInFocus: angleToAnchor < Self.ANGLE_THRESHOLD,
885
- targetIndex: targetIndex,
886
- transform: relativeCameraTransform,
887
- cameraPosition: cameraPosition,
888
- deviceDirection: deviceDirection
889
- )
890
494
  }
891
495
 
892
- private func determineTargetIndex(angleToAnchor: Float, relativeCameraTransform: simd_float4x4) -> Int {
893
- guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
894
-
895
- let spheresHeight = ReplateCameraView.spheresHeight
896
- let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
897
- let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
898
- let cameraHeight = relativeCameraTransform.columns.3.y
899
-
900
- return cameraHeight < twoThirdsDistance ? 0 : 1
901
- }
902
-
903
- private func processTargetedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
904
- DispatchQueue.main.async { [weak self] in
905
- guard let self = self else { return }
906
-
907
- self.updateCircleFocus(targetIndex: deviceTargetInfo.targetIndex)
908
- let isInRange = self.checkCameraDistance(deviceTargetInfo: deviceTargetInfo)
909
- if !isInRange {
910
- callbackHandler.reject(.notInRange)
911
- return
496
+ private func processAndSaveImageConcurrently(_ buf: CVPixelBuffer,
497
+ callback cb: SafeCallbackHandler,
498
+ completion: @escaping (Bool)->Void) {
499
+ autoreleasepool {
500
+ let ciImage = CIImage(cvImageBuffer: buf)
501
+ let context = imageProcessingContext
502
+ guard let resized = optimizedResizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
503
+ let cg = context.createCGImage(resized, from: resized.extent) else {
504
+ cb.reject(.processingError); completion(false); return
912
505
  }
913
-
914
- guard let frame = ReplateCameraView.arView?.session.currentFrame else {
915
- callbackHandler.reject(.captureError)
916
- return
506
+ let ui = UIImage(cgImage: cg).rotate(radians: .pi/2)
507
+ guard let url = optimizedSaveImage(ui, quality: getImageQuality()) else {
508
+ cb.reject(.savingError); completion(false); return
917
509
  }
918
-
919
- // Check lighting conditions
920
- if let lightEstimate = frame.lightEstimate {
921
- guard lightEstimate.ambientIntensity >= Self.MIN_AMBIENT_INTENSITY else {
922
- callbackHandler.reject(.lightingError)
923
- return
924
- }
510
+ DispatchQueue.global(qos: .background).async {
511
+ self.addMetadataToImage(at: url)
925
512
  }
926
-
927
- self.updateSpheres(
928
- deviceTargetInfo: deviceTargetInfo,
929
- cameraTransform: deviceTargetInfo.transform
930
- ) { success in
931
- if !unlimited && !success {
932
- callbackHandler.reject(.tooManyImages)
933
- return
934
- }
935
-
936
- self.processAndSaveImage(frame.capturedImage, callbackHandler: callbackHandler)
513
+ DispatchQueue.main.async {
514
+ cb.resolve(url.absoluteString)
515
+ completion(true)
937
516
  }
938
517
  }
939
518
  }
940
519
 
941
- private func updateCircleFocus(targetIndex: Int) {
942
- if targetIndex != ReplateCameraView.circleInFocus {
943
- setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
944
- setOpacityToCircle(circleId: targetIndex, opacity: 1)
945
- ReplateCameraView.circleInFocus = targetIndex
946
- ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
947
- }
948
- }
949
-
950
- private func checkCameraDistance(deviceTargetInfo: DeviceTargetInfo) -> Bool {
951
- let distance = isCameraWithinRange(
952
- cameraTransform: deviceTargetInfo.transform,
953
- anchorEntity: ReplateCameraView.anchorEntity!
954
- )
955
-
956
- switch distance {
957
- case 1:
958
- ReplateCameraController.tooFarCallback?([])
959
- ReplateCameraController.tooFarCallback = nil
960
- return false
961
- case -1:
962
- ReplateCameraController.tooCloseCallback?([])
963
- ReplateCameraController.tooCloseCallback = nil
964
- return false
965
- default:
966
- return true
967
- }
520
+ private func optimizedResizeImage(_ image: CIImage, to target: CGSize) -> CIImage? {
521
+ guard let f = CIFilter(name: "CILanczosScaleTransform") else { return nil }
522
+ let s = target.width / image.extent.width
523
+ f.setValue(image, forKey: kCIInputImageKey)
524
+ f.setValue(s, forKey: kCIInputScaleKey)
525
+ f.setValue(1.0, forKey: kCIInputAspectRatioKey)
526
+ return f.outputImage
968
527
  }
969
528
 
970
- private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
971
- let ciImage = CIImage(cvImageBuffer: pixelBuffer)
972
-
973
- guard let resizedImage = resizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
974
- let cgImage = cgImage(from: resizedImage) else {
975
- callbackHandler.reject(.processingError)
976
- return
977
- }
978
-
979
- let uiImage = UIImage(cgImage: cgImage)
980
- let rotatedImage = uiImage.rotate(radians: .pi / 2)
981
-
982
- // Save the image in the background
983
- DispatchQueue.global(qos: .userInitiated).async {
984
- guard let savedURL = self.saveImageAsPNG(rotatedImage) else {
985
- DispatchQueue.main.async {
986
- callbackHandler.reject(.processingError)
987
- }
988
- return
989
- }
990
-
991
- // Call the callback on the main thread
992
- DispatchQueue.main.async {
993
- callbackHandler.resolve(savedURL.absoluteString)
994
- }
529
+ private func optimizedSaveImage(_ image: UIImage, quality q: CGFloat) -> URL? {
530
+ guard let data = image.jpegData(compressionQuality: q) else { return nil }
531
+ let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
532
+ let url = tmp.appendingPathComponent("image_\(Date().timeIntervalSince1970).jpg")
533
+ do { try data.write(to: url, options: .atomic); return url }
534
+ catch { return nil }
995
535
  }
996
- }
997
536
 
998
-
999
-
1000
- func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
1001
- guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
1002
-
1003
- scaleFilter.setValue(image, forKey: kCIInputImageKey)
1004
- scaleFilter.setValue(targetSize.width / image.extent.width, forKey: kCIInputScaleKey)
1005
- scaleFilter.setValue(1.0, forKey: kCIInputAspectRatioKey)
1006
-
1007
- return scaleFilter.outputImage
537
+ private func addMetadataToImage(at url: URL) {
538
+ guard let src = CGImageSourceCreateWithURL(url as CFURL, nil),
539
+ let md = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [CFString:Any],
540
+ let dst = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil)
541
+ else { return }
542
+ var m = md
543
+ m[kCGImagePropertyExifDictionary] = [
544
+ kCGImagePropertyExifUserComment: getOptimizedTransformJSON()
545
+ ]
546
+ CGImageDestinationAddImageFromSource(dst, src, 0, m as CFDictionary)
547
+ CGImageDestinationFinalize(dst)
1008
548
  }
1009
549
 
1010
- // MARK: - Entity Management
1011
- func setOpacityToCircle(circleId: Int, opacity: Float) {
1012
- DispatchQueue.main.async {
1013
- for i in 0..<72 {
1014
- let offset = circleId == 0 ? 0 : 72
1015
- guard i + offset < ReplateCameraView.spheresModels.count else { continue }
1016
-
1017
- let entity = ReplateCameraView.spheresModels[i + offset]
1018
- self.updateEntityMaterial(entity, opacity: opacity)
1019
- }
550
+ private func getImageQuality() -> CGFloat {
551
+ let now = Date().timeIntervalSince1970
552
+ let delta = now - lastCaptureTime
553
+ lastCaptureTime = now
554
+ if delta < 0.5 {
555
+ captureCount += 1
556
+ return max(0.7, 1.0 - (min(captureCount,5)*0.06))
557
+ } else {
558
+ captureCount = max(0, captureCount-1)
559
+ return 0.9
1020
560
  }
1021
561
  }
1022
562
 
1023
- private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
1024
- guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
1025
-
1026
- if #available(iOS 15.0, *) {
1027
- let newMaterial = SimpleMaterial(
1028
- color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
1029
- roughness: 1,
1030
- isMetallic: false
1031
- )
1032
- entity.model?.materials[0] = newMaterial
563
+ private func getOptimizedTransformJSON() -> String {
564
+ guard let frm = ReplateCameraView.arView.session.currentFrame else { return "{}" }
565
+ let t = frm.camera.transform
566
+ let obj: [String:Any] = [
567
+ "position": ["x":t.columns.3.x, "y":t.columns.3.y, "z":t.columns.3.z],
568
+ "gravityVector": ReplateCameraView.gravityVector
569
+ ]
570
+ if let d = try? JSONSerialization.data(withJSONObject: obj),
571
+ let s = String(data: d, encoding: .utf8) {
572
+ return s
1033
573
  }
574
+ return "{}"
1034
575
  }
1035
576
 
1036
- private func updateSpheres(deviceTargetInfo: DeviceTargetInfo, cameraTransform: simd_float4x4, completion: @escaping (Bool) -> Void) {
1037
- var completionCalled = false
1038
-
1039
- func safeCompletion(_ result: Bool) {
1040
- guard !completionCalled else {
1041
- print("Completion already called")
1042
- return
1043
- }
1044
- completionCalled = true
1045
- completion(result)
1046
- }
1047
-
1048
- // Validate spheres initialization
1049
- guard ReplateCameraView.spheresModels.count >= 144 else {
1050
- print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
1051
- safeCompletion(false)
1052
- return
1053
- }
1054
-
1055
- // Get anchor
1056
- guard let anchorNode = ReplateCameraView.anchorEntity else {
1057
- print("[updateSpheres] No anchor entity found.")
1058
- safeCompletion(false)
1059
- return
1060
- }
1061
-
1062
- // Calculate camera metrics
1063
- let cameraDistance = isCameraWithinRange(
1064
- cameraTransform: cameraTransform,
1065
- anchorEntity: anchorNode
1066
- )
1067
-
1068
- // Calculate sphere index
1069
- let angleDegrees = angleBetweenAnchorXAndCamera(
1070
- anchor: anchorNode,
1071
- cameraTransform: cameraTransform
1072
- )
1073
- let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72
1074
-
1075
- DispatchQueue.main.async {
1076
- self.processSphereUpdate(
1077
- sphereIndex: sphereIndex,
1078
- targetIndex: deviceTargetInfo.targetIndex,
1079
- completion: safeCompletion
1080
- )
577
+ private func updateCircleFocus(targetIndex: Int) {
578
+ if targetIndex != ReplateCameraView.circleInFocus {
579
+ setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
580
+ setOpacityToCircle(circleId: targetIndex, opacity: 1.0)
581
+ ReplateCameraView.circleInFocus = targetIndex
582
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
1081
583
  }
1082
584
  }
1083
585
 
1084
- private func processSphereUpdate(sphereIndex: Int, targetIndex: Int, completion: @escaping (Bool) -> Void) {
586
+ private func updateSpheresAfterCapture(targetIndex: Int, sphereIndex: Int, isNewAngle: Bool) {
1085
587
  var mesh: ModelEntity?
1086
- var newAngle = false
1087
- var callback: RCTResponseSenderBlock?
1088
-
1089
- if targetIndex == 1 {
1090
- guard sphereIndex < ReplateCameraView.upperSpheresSet.count else {
1091
- print("[updateSpheres] Sphere index out of bounds")
1092
- completion(false)
1093
- return
1094
- }
1095
-
1096
- if !ReplateCameraView.upperSpheresSet[sphereIndex] {
1097
- ReplateCameraView.upperSpheresSet[sphereIndex] = true
1098
- ReplateCameraView.photosFromDifferentAnglesTaken += 1
1099
- newAngle = true
1100
-
1101
- guard 72 + sphereIndex < ReplateCameraView.spheresModels.count else {
1102
- print("[updateSpheres] Upper spheresModels index out of range")
1103
- completion(false)
1104
- return
1105
- }
1106
-
1107
- mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
1108
-
1109
- if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
1110
- callback = ReplateCameraController.completedUpperSpheresCallback
1111
- ReplateCameraController.completedUpperSpheresCallback = nil
1112
- }
588
+ var cb: RCTResponseSenderBlock?
589
+ if targetIndex==1 && isNewAngle {
590
+ ReplateCameraView.upperSpheresSet[sphereIndex]=true
591
+ ReplateCameraView.photosFromDifferentAnglesTaken+=1
592
+ mesh = ReplateCameraView.spheresModels[safe:72+sphereIndex]
593
+ if ReplateCameraView.upperSpheresSet.allSatisfy({$0}) {
594
+ cb = Self.completedUpperSpheresCallback; Self.completedUpperSpheresCallback=nil
1113
595
  }
1114
- } else if targetIndex == 0 {
1115
- guard sphereIndex < ReplateCameraView.lowerSpheresSet.count else {
1116
- print("[updateSpheres] Lower sphere index out of range")
1117
- completion(false)
1118
- return
596
+ } else if targetIndex==0 && isNewAngle {
597
+ ReplateCameraView.lowerSpheresSet[sphereIndex]=true
598
+ ReplateCameraView.photosFromDifferentAnglesTaken+=1
599
+ mesh = ReplateCameraView.spheresModels[safe:sphereIndex]
600
+ if ReplateCameraView.lowerSpheresSet.allSatisfy({$0}) {
601
+ cb = Self.completedLowerSpheresCallback; Self.completedLowerSpheresCallback=nil
1119
602
  }
1120
-
1121
- if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
1122
- ReplateCameraView.lowerSpheresSet[sphereIndex] = true
1123
- ReplateCameraView.photosFromDifferentAnglesTaken += 1
1124
- newAngle = true
1125
-
1126
- guard sphereIndex < ReplateCameraView.spheresModels.count else {
1127
- print("[updateSpheres] Lower spheresModels index out of range")
1128
- completion(false)
1129
- return
1130
- }
1131
-
1132
- mesh = ReplateCameraView.spheresModels[sphereIndex]
1133
-
1134
- if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
1135
- callback = ReplateCameraController.completedLowerSpheresCallback
1136
- ReplateCameraController.completedLowerSpheresCallback = nil
1137
- }
1138
- }
1139
- }
1140
-
1141
- if let mesh = mesh {
1142
- let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
1143
- mesh.model?.materials[0] = material
1144
- ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
1145
603
  }
1146
-
1147
- callback?([])
1148
- completion(newAngle)
1149
- }
1150
-
1151
- // MARK: - Anchor Validation
1152
- func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
1153
- var isValid = false
1154
- if(Thread.isMainThread){
1155
- let transform = anchorNode.transform
1156
- let position = transform.translation
1157
- let rotation = transform.rotation
1158
- let scale = transform.scale
1159
-
1160
- isValid = !position.isNaN &&
1161
- !rotation.isNaN &&
1162
- scale != SIMD3<Float>(0, 0, 0) &&
1163
- abs(length(rotation.vector) - 1.0) < 0.0001
1164
- }else{
1165
- DispatchQueue.main.sync {
1166
- let transform = anchorNode.transform
1167
- let position = transform.translation
1168
- let rotation = transform.rotation
1169
- let scale = transform.scale
1170
-
1171
- isValid = !position.isNaN &&
1172
- !rotation.isNaN &&
1173
- scale != SIMD3<Float>(0, 0, 0) &&
1174
- abs(length(rotation.vector) - 1.0) < 0.0001
604
+ if let m = mesh {
605
+ m.model?.materials[0] = SimpleMaterial(color:.green, roughness:1, isMetallic:false)
606
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength:.light)
1175
607
  }
1176
- }
1177
-
1178
- return isValid
608
+ cb?([])
1179
609
  }
1180
610
 
1181
- // MARK: - Distance and Angle Calculations
1182
- func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity) -> Int {
1183
- var distance: Float = 0
1184
-
1185
- if(Thread.isMainThread){
1186
- let cameraPosition = SIMD3<Float>(
1187
- cameraTransform.columns.3.x,
1188
- cameraTransform.columns.3.y,
1189
- cameraTransform.columns.3.z
1190
- )
1191
- let anchorPosition = anchorEntity.transform.translation
1192
- distance = distanceBetween(cameraPosition, anchorPosition)
1193
- }else{
1194
- if(Thread.isMainThread){
1195
- let cameraPosition = SIMD3<Float>(
1196
- cameraTransform.columns.3.x,
1197
- cameraTransform.columns.3.y,
1198
- cameraTransform.columns.3.z
1199
- )
1200
- let anchorPosition = anchorEntity.transform.translation
1201
- distance = distanceBetween(cameraPosition, anchorPosition)
1202
- }else{
1203
- DispatchQueue.main.sync {
1204
- let cameraPosition = SIMD3<Float>(
1205
- cameraTransform.columns.3.x,
1206
- cameraTransform.columns.3.y,
1207
- cameraTransform.columns.3.z
1208
- )
1209
- let anchorPosition = anchorEntity.transform.translation
1210
- distance = distanceBetween(cameraPosition, anchorPosition)
1211
- }
611
+ private func setOpacityToCircle(circleId: Int, opacity: Float) {
612
+ DispatchQueue.main.async {
613
+ let mat = SimpleMaterial(color: UIColor.white.withAlphaComponent(CGFloat(opacity)),
614
+ roughness:1, isMetallic:false)
615
+ for i in 0..<72 {
616
+ let idx = circleId==0 ? i : 72+i
617
+ ReplateCameraView.spheresModels[safe:idx]?.model?.materials[0]=mat
618
+ }
1212
619
  }
1213
-
1214
- }
1215
- return distance <= Self.MIN_DISTANCE ? -1 :
1216
- distance >= Self.MAX_DISTANCE ? 1 : 0
1217
- }
1218
-
1219
- func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
1220
- let difference = pos1 - pos2
1221
- return sqrt(dot(difference, difference))
1222
- }
1223
-
1224
- // MARK: - Static Utility Methods
1225
- func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4 {
1226
- var relativeTransform: simd_float4x4!
1227
- if(Thread.isMainThread){
1228
- let anchorTransform = anchor.transformMatrix(relativeTo: nil)
1229
- relativeTransform = anchorTransform.inverse * cameraTransform
1230
- }else{
1231
- DispatchQueue.main.sync {
1232
- let anchorTransform = anchor.transformMatrix(relativeTo: nil)
1233
- relativeTransform = anchorTransform.inverse * cameraTransform
1234
- }
1235
- }
1236
- return relativeTransform
1237
- }
1238
-
1239
- func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
1240
- var angleDegrees: Float = 0
1241
- if(Thread.isMainThread){
1242
- let anchorTransform = anchor.transform.matrix
1243
- let anchorPositionXZ = simd_float2(
1244
- anchor.transform.translation.x,
1245
- anchor.transform.translation.z
1246
- )
1247
- let relativeCameraPositionXZ = simd_float2(
1248
- cameraTransform.columns.3.x,
1249
- cameraTransform.columns.3.z
1250
- )
1251
-
1252
- let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
1253
- let anchorXAxisXZ = simd_float2(
1254
- anchorTransform.columns.0.x,
1255
- anchorTransform.columns.0.z
1256
- )
1257
-
1258
- let angle = atan2(directionXZ.y, directionXZ.x) -
1259
- atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
1260
-
1261
- angleDegrees = angle * (180.0 / .pi)
1262
- if angleDegrees < 0 {
1263
- angleDegrees += 360
1264
- }
1265
- }else{
1266
- DispatchQueue.main.sync {
1267
- let anchorTransform = anchor.transform.matrix
1268
- let anchorPositionXZ = simd_float2(
1269
- anchor.transform.translation.x,
1270
- anchor.transform.translation.z
1271
- )
1272
- let relativeCameraPositionXZ = simd_float2(
1273
- cameraTransform.columns.3.x,
1274
- cameraTransform.columns.3.z
1275
- )
1276
-
1277
- let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
1278
- let anchorXAxisXZ = simd_float2(
1279
- anchorTransform.columns.0.x,
1280
- anchorTransform.columns.0.z
1281
- )
1282
-
1283
- let angle = atan2(directionXZ.y, directionXZ.x) -
1284
- atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
1285
-
1286
- angleDegrees = angle * (180.0 / .pi)
1287
- if angleDegrees < 0 {
1288
- angleDegrees += 360
1289
- }
1290
- }
1291
- }
1292
- return angleDegrees
1293
- }
1294
-
1295
- func cgImage(from ciImage: CIImage) -> CGImage? {
1296
- let context = CIContext(options: nil)
1297
- return context.createCGImage(ciImage, from: ciImage.extent)
1298
- }
1299
-
1300
- func saveImageAsJPEG(_ image: UIImage) -> URL? {
1301
- guard let imageData = image.jpegData(compressionQuality: 1),
1302
- let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
1303
- return nil
1304
- }
1305
-
1306
- let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
1307
- let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
1308
- let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
1309
-
1310
- guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
1311
- return nil
1312
- }
1313
-
1314
- var mutableMetadata = imageProperties
1315
- mutableMetadata[kCGImagePropertyExifDictionary] = [
1316
- kCGImagePropertyExifUserComment: getTransformJSON(session: ReplateCameraView.arView.session)
1317
- ]
1318
-
1319
- guard let destination = CGImageDestinationCreateWithURL(
1320
- fileURL as CFURL,
1321
- kUTTypeJPEG,
1322
- 1,
1323
- nil
1324
- ) else {
1325
- return nil
1326
- }
1327
-
1328
- CGImageDestinationAddImageFromSource(
1329
- destination,
1330
- source,
1331
- 0,
1332
- mutableMetadata as CFDictionary
1333
- )
1334
-
1335
- guard CGImageDestinationFinalize(destination) else {
1336
- return nil
1337
620
  }
1338
621
 
1339
- return fileURL
1340
- }
1341
-
1342
- func saveImageAsPNG(_ image: UIImage) -> URL? {
1343
- // Convert UIImage to PNG data
1344
- guard let imageData = image.pngData(),
1345
- let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
1346
- return nil
622
+ // MARK: Anchor & Transform Helpers
623
+ private func getValidAnchorEntity() throws -> AnchorEntity {
624
+ var a: AnchorEntity?
625
+ if Thread.isMainThread { a = ReplateCameraView.anchorEntity }
626
+ else { DispatchQueue.main.sync { a = ReplateCameraView.anchorEntity } }
627
+ guard let anchor = a else { throw ARError.noAnchor }
628
+ guard isAnchorNodeValid(anchor) else { throw ARError.invalidAnchor }
629
+ return anchor
1347
630
  }
1348
631
 
1349
- // Define temporary file URL
1350
- let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
1351
- let uniqueFilename = "image_\(Date().timeIntervalSince1970).png"
1352
- let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
1353
-
1354
- // Retrieve existing image properties
1355
- guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
1356
- return nil
632
+ private func getDeviceTargetInfoFromTransform(anchorEntity: AnchorEntity,
633
+ cameraTransform: simd_float4x4) throws -> DeviceTargetInfo {
634
+ let camPos = SIMD3<Float>(cameraTransform.columns.3.x,
635
+ cameraTransform.columns.3.y,
636
+ cameraTransform.columns.3.z)
637
+ var anchorPos: SIMD3<Float>!
638
+ if Thread.isMainThread { anchorPos = anchorEntity.position(relativeTo:nil) }
639
+ else { DispatchQueue.main.sync { anchorPos = anchorEntity.position(relativeTo:nil) } }
640
+ let devDir = normalize(SIMD3(-cameraTransform.columns.2.x,
641
+ -cameraTransform.columns.2.y,
642
+ -cameraTransform.columns.2.z))
643
+ let toAnchor = normalize(anchorPos - camPos)
644
+ let angle = acos(dot(devDir, toAnchor))
645
+ let dist = distanceBetween(camPos, anchorPos)
646
+ let relT = getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
647
+ let ti = determineTargetIndex(angleToAnchor: angle, relativeCameraTransform: relT)
648
+ return DeviceTargetInfo(isInFocus: angle<Self.ANGLE_THRESHOLD,
649
+ targetIndex: ti,
650
+ transform: relT,
651
+ cameraPosition: camPos,
652
+ deviceDirection: devDir,
653
+ distance: dist)
654
+ }
655
+
656
+ private func determineTargetIndex(angleToAnchor: Float,
657
+ relativeCameraTransform: simd_float4x4) -> Int {
658
+ guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
659
+ let twoThirds = ReplateCameraView.spheresHeight
660
+ + ReplateCameraView.distanceBetweenCircles
661
+ + ReplateCameraView.distanceBetweenCircles/5
662
+ let camH = relativeCameraTransform.columns.3.y
663
+ return camH < twoThirds ? 0 : 1
664
+ }
665
+
666
+ private func isAnchorNodeValid(_ anchor: AnchorEntity) -> Bool {
667
+ var ok = false
668
+ if Thread.isMainThread {
669
+ let t = anchor.transform
670
+ ok = !t.translation.isNaN &&
671
+ !t.rotation.isNaN &&
672
+ t.scale != SIMD3<Float>(repeating:0) &&
673
+ abs(length(t.rotation.vector)-1.0)<0.0001
674
+ } else {
675
+ DispatchQueue.main.sync {
676
+ let t = anchor.transform
677
+ ok = !t.translation.isNaN &&
678
+ !t.rotation.isNaN &&
679
+ t.scale != SIMD3<Float>(repeating:0) &&
680
+ abs(length(t.rotation.vector)-1.0)<0.0001
681
+ }
682
+ }
683
+ return ok
1357
684
  }
1358
685
 
1359
- // Add metadata (including a user comment with transform JSON)
1360
- var mutableMetadata = imageProperties
1361
- mutableMetadata[kCGImagePropertyPNGDictionary] = [
1362
- kCGImagePropertyPNGComment: getTransformJSON(session: ReplateCameraView.arView.session)
1363
- ]
1364
-
1365
- // Create destination for PNG file
1366
- guard let destination = CGImageDestinationCreateWithURL(
1367
- fileURL as CFURL,
1368
- kUTTypePNG,
1369
- 1,
1370
- nil
1371
- ) else {
1372
- return nil
686
+ private func distanceBetween(_ p1: SIMD3<Float>, _ p2: SIMD3<Float>) -> Float {
687
+ sqrt(dot(p1-p2, p1-p2))
1373
688
  }
1374
689
 
1375
- // Add image with metadata to destination
1376
- CGImageDestinationAddImageFromSource(
1377
- destination,
1378
- source,
1379
- 0,
1380
- mutableMetadata as CFDictionary
1381
- )
1382
-
1383
- // Finalize image creation
1384
- guard CGImageDestinationFinalize(destination) else {
1385
- return nil
690
+ private func getTransformRelativeToAnchor(anchor: AnchorEntity,
691
+ cameraTransform: simd_float4x4) -> simd_float4x4 {
692
+ var rel: simd_float4x4!
693
+ if Thread.isMainThread {
694
+ rel = anchor.transformMatrix(relativeTo:nil).inverse * cameraTransform
695
+ } else {
696
+ DispatchQueue.main.sync {
697
+ rel = anchor.transformMatrix(relativeTo:nil).inverse * cameraTransform
698
+ }
699
+ }
700
+ return rel
701
+ }
702
+
703
+ private func angleBetweenAnchorXAndCamera(anchor: AnchorEntity,
704
+ cameraTransform: simd_float4x4) -> Float {
705
+ var deg: Float = 0
706
+ if Thread.isMainThread {
707
+ let at = anchor.transform.matrix
708
+ let ap = simd_float2(anchor.transform.translation.x,
709
+ anchor.transform.translation.z)
710
+ let cp = simd_float2(cameraTransform.columns.3.x,
711
+ cameraTransform.columns.3.z)
712
+ let dir = cp - ap
713
+ let xAxis = simd_float2(at.columns.0.x, at.columns.0.z)
714
+ let a = atan2(dir.y, dir.x) - atan2(xAxis.y, xAxis.x)
715
+ deg = a * (180.0/.pi)
716
+ if deg<0 { deg+=360 }
717
+ } else {
718
+ DispatchQueue.main.sync {
719
+ let at = anchor.transform.matrix
720
+ let ap = simd_float2(anchor.transform.translation.x,
721
+ anchor.transform.translation.z)
722
+ let cp = simd_float2(cameraTransform.columns.3.x,
723
+ cameraTransform.columns.3.z)
724
+ let dir = cp - ap
725
+ let xAxis = simd_float2(at.columns.0.x, at.columns.0.z)
726
+ let a = atan2(dir.y, dir.x) - atan2(xAxis.y, xAxis.x)
727
+ deg = a * (180.0/.pi)
728
+ if deg<0 { deg+=360 }
729
+ }
730
+ }
731
+ return deg
732
+ }
733
+
734
+ // Legacy for compatibility
735
+ func saveImageAsJPEG(_ image: UIImage) -> URL? {
736
+ guard let data = image.jpegData(compressionQuality:1),
737
+ let src = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
738
+ let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory:true)
739
+ let url = tmp.appendingPathComponent("image_\(Date().timeIntervalSince1970).jpg")
740
+ guard let props = CGImageSourceCopyPropertiesAtIndex(src,0,nil) as? [CFString:Any],
741
+ let dst = CGImageDestinationCreateWithURL(url as CFURL,
742
+ kUTTypeJPEG,1,nil)
743
+ else { return nil }
744
+ var m = props
745
+ m[kCGImagePropertyExifDictionary] = [
746
+ kCGImagePropertyExifUserComment: getOptimizedTransformJSON()
747
+ ]
748
+ CGImageDestinationAddImageFromSource(dst,src,0,m as CFDictionary)
749
+ guard CGImageDestinationFinalize(dst) else { return nil }
750
+ return url
751
+ }
752
+
753
+ func saveImageAsPNG(_ image: UIImage) -> URL? {
754
+ guard let data = image.pngData(),
755
+ let src = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
756
+ let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory:true)
757
+ let url = tmp.appendingPathComponent("image_\(Date().timeIntervalSince1970).png")
758
+ guard let props = CGImageSourceCopyPropertiesAtIndex(src,0,nil) as? [CFString:Any],
759
+ let dst = CGImageDestinationCreateWithURL(url as CFURL,
760
+ kUTTypePNG,1,nil)
761
+ else { return nil }
762
+ var m = props
763
+ m[kCGImagePropertyPNGDictionary] = [
764
+ kCGImagePropertyPNGComment: getOptimizedTransformJSON()
765
+ ]
766
+ CGImageDestinationAddImageFromSource(dst,src,0,m as CFDictionary)
767
+ guard CGImageDestinationFinalize(dst) else { return nil }
768
+ return url
1386
769
  }
1387
770
 
1388
- return fileURL
1389
- }
1390
-
1391
-
1392
- func getTransformJSON(session: ARSession) -> String {
1393
- let transform = session.currentFrame?.camera.transform ?? simd_float4x4()
1394
- // Extract translation
1395
- let translation = [
1396
- "x": transform.columns.3.x,
1397
- "y": transform.columns.3.y,
1398
- "z": transform.columns.3.z
1399
- ]
1400
-
1401
- // Extract scale by calculating the length of each column vector
1402
- let scale = [
1403
- "x": simd_length(simd_float3(transform.columns.0.x, transform.columns.0.y, transform.columns.0.z)),
1404
- "y": simd_length(simd_float3(transform.columns.1.x, transform.columns.1.y, transform.columns.1.z)),
1405
- "z": simd_length(simd_float3(transform.columns.2.x, transform.columns.2.y, transform.columns.2.z))
1406
- ]
1407
-
1408
- // Extract rotation by normalizing each axis and converting it into a 3x3 rotation matrix
1409
- let rotationMatrix = simd_float3x3(columns: (
1410
- simd_float3(transform.columns.0.x / scale["x"]!, transform.columns.0.y / scale["x"]!, transform.columns.0.z / scale["x"]!),
1411
- simd_float3(transform.columns.1.x / scale["y"]!, transform.columns.1.y / scale["y"]!, transform.columns.1.z / scale["y"]!),
1412
- simd_float3(transform.columns.2.x / scale["z"]!, transform.columns.2.y / scale["z"]!, transform.columns.2.z / scale["z"]!)
1413
- ))
1414
-
1415
- // Convert rotation matrix to quaternion
1416
- let quaternion = simd_quatf(rotationMatrix)
1417
- let rotation = [
1418
- "x": quaternion.vector.x,
1419
- "y": quaternion.vector.y,
1420
- "z": quaternion.vector.z,
1421
- "w": quaternion.vector.w
1422
- ]
1423
-
1424
- // Format as JSON
1425
- let jsonObject: [String: Any] = [
1426
- "translation": translation,
1427
- "rotation": rotation,
1428
- "scale": scale,
1429
- "gravityVector": ReplateCameraView.gravityVector
1430
- ]
1431
-
1432
- // Convert dictionary to JSON string
1433
- if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
1434
- let jsonString = String(data: jsonData, encoding: .utf8) {
1435
- return jsonString
1436
- } else {
1437
- return "{}" // Return empty JSON if conversion fails
771
+ func getTransformJSON(session: ARSession) -> String {
772
+ let t = session.currentFrame?.camera.transform ?? simd_float4x4()
773
+ let translation = [
774
+ "x": t.columns.3.x,
775
+ "y": t.columns.3.y,
776
+ "z": t.columns.3.z
777
+ ]
778
+ let scale = [
779
+ "x": simd_length(simd_float3(t.columns.0.x,
780
+ t.columns.0.y,
781
+ t.columns.0.z)),
782
+ "y": simd_length(simd_float3(t.columns.1.x,
783
+ t.columns.1.y,
784
+ t.columns.1.z)),
785
+ "z": simd_length(simd_float3(t.columns.2.x,
786
+ t.columns.2.y,
787
+ t.columns.2.z))
788
+ ]
789
+ let rotMat = simd_float3x3(columns: (
790
+ simd_float3(t.columns.0.x/scale["x"]!,t.columns.0.y/scale["x"]!,t.columns.0.z/scale["x"]!),
791
+ simd_float3(t.columns.1.x/scale["y"]!,t.columns.1.y/scale["y"]!,t.columns.1.z/scale["y"]!),
792
+ simd_float3(t.columns.2.x/scale["z"]!,t.columns.2.y/scale["z"]!,t.columns.2.z/scale["z"]!)
793
+ ))
794
+ let q = simd_quatf(rotMat)
795
+ let rotation = ["x":q.vector.x,"y":q.vector.y,"z":q.vector.z,"w":q.vector.w]
796
+ let obj: [String:Any] = [
797
+ "translation": translation,
798
+ "rotation": rotation,
799
+ "scale": scale,
800
+ "gravityVector": ReplateCameraView.gravityVector
801
+ ]
802
+ if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted),
803
+ let s = String(data: d, encoding: .utf8) {
804
+ return s
805
+ }
806
+ return "{}"
1438
807
  }
1439
- }
1440
808
  }
1441
809
 
810
+ // MARK: - ARView Coaching Extension
1442
811
  extension ARView: ARCoachingOverlayViewDelegate {
1443
- func addCoaching() {
1444
- print("ADD COACHING")
1445
- // Create a ARCoachingOverlayView object
1446
- let coachingOverlay = ARCoachingOverlayView()
1447
- // Make sure it rescales if the device orientation changes
1448
- coachingOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
1449
- self.addSubview(coachingOverlay)
1450
- coachingOverlay.center = self.convert(self.center, from: self.superview)
1451
- // Set the Augmented Reality goal
1452
- coachingOverlay.goal = .horizontalPlane
1453
- // Set the ARSession
1454
- coachingOverlay.session = self.session
1455
- // Set the delegate for any callbacks
1456
- coachingOverlay.delegate = self
1457
- coachingOverlay.setActive(true, animated: true)
1458
- ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
1459
- let callback = ReplateCameraController.openedTutorialCallback
1460
- if (callback != nil) {
1461
- callback!([])
1462
- ReplateCameraController.openedTutorialCallback = nil
1463
- }
1464
- }
1465
-
1466
- // Example callback for the delegate object
1467
- public func coachingOverlayViewDidDeactivate(
1468
- _ coachingOverlayView: ARCoachingOverlayView
1469
- ) {
1470
- let callback = ReplateCameraController.completedTutorialCallback
1471
- if (callback != nil) {
1472
- callback!([])
1473
- ReplateCameraController.completedTutorialCallback = nil
812
+ func addCoaching() {
813
+ let overlay = ARCoachingOverlayView()
814
+ overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
815
+ addSubview(overlay)
816
+ overlay.center = convert(center, from: superview)
817
+ overlay.goal = .horizontalPlane
818
+ overlay.session = session
819
+ overlay.delegate = self
820
+ overlay.setActive(true, animated: true)
821
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
822
+ if let cb = ReplateCameraController.openedTutorialCallback {
823
+ cb([]); ReplateCameraController.openedTutorialCallback = nil
824
+ }
825
+ }
826
+ public func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
827
+ if let cb = ReplateCameraController.completedTutorialCallback {
828
+ cb([]); ReplateCameraController.completedTutorialCallback = nil
829
+ }
830
+ ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
831
+ ReplateCameraView.addRecognizers()
1474
832
  }
1475
- ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
1476
- ReplateCameraView.addRecognizers()
1477
- }
1478
833
  }
1479
834
 
835
+ // MARK: - UIImage averageColor
1480
836
  extension UIImage {
1481
- func averageColor() -> UIColor? {
1482
- // Convert UIImage to CGImage
1483
- guard let cgImage = self.cgImage else {
1484
- return nil
1485
- }
1486
-
1487
- // Get width and height of the image
1488
- let width = cgImage.width
1489
- let height = cgImage.height
1490
-
1491
- // Create a data provider from CGImage
1492
- guard let dataProvider = cgImage.dataProvider else {
1493
- return nil
1494
- }
1495
-
1496
- // Access pixel data
1497
- guard let pixelData = dataProvider.data else {
1498
- return nil
1499
- }
1500
-
1501
- // Create a pointer to the pixel data
1502
- let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
1503
-
1504
- var totalRed: CGFloat = 0
1505
- var totalGreen: CGFloat = 0
1506
- var totalBlue: CGFloat = 0
1507
-
1508
- // Loop through each pixel and calculate sum of RGB values
1509
- for y in 0..<height {
1510
- for x in 0..<width {
1511
- let pixelInfo: Int = ((width * y) + x) * 4
1512
- let red = CGFloat(data[pixelInfo]) / 255.0
1513
- let green = CGFloat(data[pixelInfo + 1]) / 255.0
1514
- let blue = CGFloat(data[pixelInfo + 2]) / 255.0
1515
-
1516
- totalRed += red
1517
- totalGreen += green
1518
- totalBlue += blue
1519
- }
837
+ func averageColor() -> UIColor? {
838
+ guard let cg = cgImage,
839
+ let dataProv = cg.dataProvider,
840
+ let data = dataProv.data else { return nil }
841
+ let ptr = CFDataGetBytePtr(data)
842
+ let w = cg.width, h = cg.height
843
+ var r: CGFloat=0, g: CGFloat=0, b: CGFloat=0
844
+ for y in 0..<h {
845
+ for x in 0..<w {
846
+ let idx = ((w*y)+x)*4
847
+ r += CGFloat(ptr[idx]) /255
848
+ g += CGFloat(ptr[idx+1]) /255
849
+ b += CGFloat(ptr[idx+2]) /255
850
+ }
851
+ }
852
+ let count = CGFloat(w*h)
853
+ return UIColor(red: r/count, green: g/count, blue: b/count, alpha: 1)
1520
854
  }
1521
-
1522
- // Calculate average RGB values
1523
- let count = CGFloat(width * height)
1524
- let averageRed = totalRed / count
1525
- let averageGreen = totalGreen / count
1526
- let averageBlue = totalBlue / count
1527
-
1528
- // Create and return average color
1529
- return UIColor(red: averageRed, green: averageGreen, blue: averageBlue, alpha: 1.0)
1530
- }
1531
855
  }
1532
856
 
857
+ // MARK: - UIColor RGB Components
1533
858
  extension UIColor {
1534
- func getRGBComponents() -> (red: CGFloat, green: CGFloat, blue: CGFloat)? {
1535
- var red: CGFloat = 0
1536
- var green: CGFloat = 0
1537
- var blue: CGFloat = 0
1538
- var alpha: CGFloat = 0
1539
-
1540
- // Check if the color can be converted to RGB
1541
- guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
1542
- return nil
859
+ func getRGBComponents() -> (red:CGFloat, green:CGFloat, blue:CGFloat)? {
860
+ var r:CGFloat=0, g:CGFloat=0, b:CGFloat=0, a:CGFloat=0
861
+ guard getRed(&r, green:&g, blue:&b, alpha:&a) else { return nil }
862
+ return (r,g,b)
1543
863
  }
1544
-
1545
- return (red, green, blue)
1546
- }
1547
864
  }
1548
865
 
866
+ // MARK: - SIMD & simd_quatf Extensions
1549
867
  public extension simd_float2 {
1550
- static func -(lhs: simd_float2, rhs: simd_float2) -> simd_float2 {
1551
- return simd_float2(lhs.x - rhs.x, lhs.y - rhs.y)
1552
- }
868
+ static func -(lhs: simd_float2, rhs: simd_float2) -> simd_float2 {
869
+ return simd_float2(lhs.x - rhs.x, lhs.y - rhs.y)
870
+ }
1553
871
  }
1554
-
1555
872
  public extension SIMD3 where Scalar == Float {
1556
- var isNaN: Bool {
1557
- return x.isNaN || y.isNaN || z.isNaN
1558
- }
873
+ var isNaN: Bool { x.isNaN || y.isNaN || z.isNaN }
1559
874
  }
1560
-
1561
875
  public extension simd_quatf {
1562
- var isNaN: Bool {
1563
- return vector.x.isNaN || vector.y.isNaN || vector.w.isNaN
1564
- }
876
+ var isNaN: Bool { vector.x.isNaN || vector.y.isNaN || vector.w.isNaN }
1565
877
  }